firebase log level

This commit is contained in:
oscarz
2024-08-29 18:25:13 +08:00
parent 8500300d18
commit 27c160beaf
1165 changed files with 122916 additions and 1 deletions

View File

@ -0,0 +1,96 @@
// 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.
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/// Policy for handling the case where there's an overflow of experiments for an installation
/// instance.
typedef NS_ENUM(int32_t, ABTExperimentPayloadExperimentOverflowPolicy) {
ABTExperimentPayloadExperimentOverflowPolicyUnrecognizedValue = 999,
ABTExperimentPayloadExperimentOverflowPolicyUnspecified = 0,
ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest = 1,
ABTExperimentPayloadExperimentOverflowPolicyIgnoreNewest = 2,
};
@interface ABTExperimentLite : NSObject
@property(nonatomic, readonly, copy) NSString *experimentId;
- (instancetype)initWithExperimentId:(NSString *)experimentId NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
@end
@interface ABTExperimentPayload : NSObject
/// Unique identifier for this experiment.
@property(nonatomic, readonly, copy) NSString *experimentId;
/// Unique identifier for the variant to which an installation instance has been assigned.
@property(nonatomic, readonly, copy) NSString *variantId;
/// Epoch time that represents when the experiment was started.
@property(nonatomic, readonly) int64_t experimentStartTimeMillis;
/// The event that triggers this experiment into ON state.
@property(nonatomic, nullable, readonly, copy) NSString *triggerEvent;
/// Duration in milliseconds for which the experiment can stay in STANDBY state (un-triggered).
@property(nonatomic, readonly) int64_t triggerTimeoutMillis;
/// Duration in milliseconds for which the experiment can stay in ON state (triggered).
@property(nonatomic, readonly) int64_t timeToLiveMillis;
/// The event logged when impact service sets the experiment.
@property(nonatomic, readonly, copy) NSString *setEventToLog;
/// The event logged when an experiment goes to the ON state.
@property(nonatomic, readonly, copy) NSString *activateEventToLog;
/// The event logged when an experiment is cleared.
@property(nonatomic, readonly, copy) NSString *clearEventToLog;
/// The event logged when an experiment times out after `triggerTimeoutMillis` milliseconds.
@property(nonatomic, readonly, copy) NSString *timeoutEventToLog;
/// The event logged when an experiment times out after `timeToLiveMillis` milliseconds.
@property(nonatomic, readonly, copy) NSString *ttlExpiryEventToLog;
@property(nonatomic, readonly) ABTExperimentPayloadExperimentOverflowPolicy overflowPolicy;
/// A list of all other ongoing (started, and not yet stopped) experiments at the time this
/// experiment was started. Does not include this experiment; only the others.
@property(nonatomic, readonly) NSArray<ABTExperimentLite *> *ongoingExperiments;
/// Parses an ABTExperimentPayload directly from JSON data.
/// @param data JSON object as NSData. Must be reconstructible as an NSDictionary<NSString* , id>.
+ (nullable instancetype)parseFromData:(NSData *)data;
/// Initializes an ABTExperimentPayload from a dictionary with experiment metadata.
- (instancetype)initWithDictionary:(NSDictionary<NSString *, id> *)dictionary
NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
/// Clears the trigger event associated with this payload.
- (void)clearTriggerEvent;
/// Checks if the overflow policy is a valid enum object.
- (BOOL)overflowPolicyIsValid;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,20 @@
// 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.
// An umbrella header, for any other libraries in this repo to access Firebase Public and Private
// headers. Any package manager complexity should be handled here.
#import <FirebaseABTesting/FirebaseABTesting.h>
#import "FirebaseABTesting/Sources/Private/ABTExperimentPayload.h"

View File

@ -0,0 +1,156 @@
/*
* Copyright 2017 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/FIRApp.h>
@class FIRComponentContainer;
@class FIRHeartbeatLogger;
@protocol FIRLibrary;
/**
* The internal interface to `FirebaseApp`. This is meant for first-party integrators, who need to
* receive `FirebaseApp` notifications, log info about the success or failure of their
* configuration, and access other internal functionality of `FirebaseApp`.
*/
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, FIRConfigType) {
FIRConfigTypeCore = 1,
FIRConfigTypeSDK = 2,
};
extern NSString *const kFIRDefaultAppName;
extern NSString *const kFIRAppReadyToConfigureSDKNotification;
extern NSString *const kFIRAppDeleteNotification;
extern NSString *const kFIRAppIsDefaultAppKey;
extern NSString *const kFIRAppNameKey;
extern NSString *const kFIRGoogleAppIDKey;
extern NSString *const kFirebaseCoreErrorDomain;
/**
* The format string for the `UserDefaults` key used for storing the data collection enabled flag.
* This includes formatting to append the `FirebaseApp`'s name.
*/
extern NSString *const kFIRGlobalAppDataCollectionEnabledDefaultsKeyFormat;
/**
* The plist key used for storing the data collection enabled flag.
*/
extern NSString *const kFIRGlobalAppDataCollectionEnabledPlistKey;
/** @var FirebaseAuthStateDidChangeInternalNotification
@brief The name of the @c NotificationCenter notification which is posted when the auth state
changes (e.g. a new token has been produced, a user logs in or out). The object parameter of
the notification is a dictionary possibly containing the key:
@c FirebaseAuthStateDidChangeInternalNotificationTokenKey (the new access token.) If it does not
contain this key it indicates a sign-out event took place.
*/
extern NSString *const FIRAuthStateDidChangeInternalNotification;
/** @var FirebaseAuthStateDidChangeInternalNotificationTokenKey
@brief A key present in the dictionary object parameter of the
@c FirebaseAuthStateDidChangeInternalNotification notification. The value associated with this
key will contain the new access token.
*/
extern NSString *const FIRAuthStateDidChangeInternalNotificationTokenKey;
/** @var FirebaseAuthStateDidChangeInternalNotificationAppKey
@brief A key present in the dictionary object parameter of the
@c FirebaseAuthStateDidChangeInternalNotification notification. The value associated with this
key will contain the FirebaseApp associated with the auth instance.
*/
extern NSString *const FIRAuthStateDidChangeInternalNotificationAppKey;
/** @var FirebaseAuthStateDidChangeInternalNotificationUIDKey
@brief A key present in the dictionary object parameter of the
@c FirebaseAuthStateDidChangeInternalNotification notification. The value associated with this
key will contain the new user's UID (or nil if there is no longer a user signed in).
*/
extern NSString *const FIRAuthStateDidChangeInternalNotificationUIDKey;
@interface FIRApp ()
/**
* A flag indicating if this is the default app (has the default app name).
*/
@property(nonatomic, readonly) BOOL isDefaultApp;
/**
* The container of interop SDKs for this app.
*/
@property(nonatomic) FIRComponentContainer *container;
/**
* The heartbeat logger associated with this app.
*
* Firebase apps have a 1:1 relationship with heartbeat loggers.
*/
@property(readonly) FIRHeartbeatLogger *heartbeatLogger;
/**
* Checks if the default app is configured without trying to configure it.
*/
+ (BOOL)isDefaultAppConfigured;
/**
* Registers a given third-party library with the given version number to be reported for
* analytics.
*
* @param name Name of the library.
* @param version Version of the library.
*/
+ (void)registerLibrary:(nonnull NSString *)name withVersion:(nonnull NSString *)version;
/**
* Registers a given internal library to be reported for analytics.
*
* @param library Optional parameter for component registration.
* @param name Name of the library.
*/
+ (void)registerInternalLibrary:(nonnull Class<FIRLibrary>)library
withName:(nonnull NSString *)name;
/**
* Registers a given internal library with the given version number to be reported for
* analytics. This should only be used for non-Firebase libraries that have their own versioning
* scheme.
*
* @param library Optional parameter for component registration.
* @param name Name of the library.
* @param version Version of the library.
*/
+ (void)registerInternalLibrary:(nonnull Class<FIRLibrary>)library
withName:(nonnull NSString *)name
withVersion:(nonnull NSString *)version;
/**
* A concatenated string representing all the third-party libraries and version numbers.
*/
+ (NSString *)firebaseUserAgent;
/**
* Can be used by the unit tests in each SDK to reset `FirebaseApp`. This method is thread unsafe.
*/
+ (void)resetApps;
/**
* Can be used by the unit tests in each SDK to set customized options.
*/
- (instancetype)initInstanceWithName:(NSString *)name options:(FIROptions *)options;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,84 @@
/*
* Copyright 2018 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>
@class FIRApp;
@class FIRComponentContainer;
NS_ASSUME_NONNULL_BEGIN
/// Provides a system to clean up cached instances returned from the component system.
NS_SWIFT_NAME(ComponentLifecycleMaintainer)
@protocol FIRComponentLifecycleMaintainer
/// The associated app will be deleted, clean up any resources as they are about to be deallocated.
- (void)appWillBeDeleted:(FIRApp *)app;
@end
typedef _Nullable id (^FIRComponentCreationBlock)(FIRComponentContainer *container,
BOOL *isCacheable)
NS_SWIFT_NAME(ComponentCreationBlock);
/// Describes the timing of instantiation. Note: new components should default to lazy unless there
/// is a strong reason to be eager.
typedef NS_ENUM(NSInteger, FIRInstantiationTiming) {
FIRInstantiationTimingLazy,
FIRInstantiationTimingAlwaysEager,
FIRInstantiationTimingEagerInDefaultApp
} NS_SWIFT_NAME(InstantiationTiming);
/// A component that can be used from other Firebase SDKs.
NS_SWIFT_NAME(Component)
@interface FIRComponent : NSObject
/// The protocol describing functionality provided from the `Component`.
@property(nonatomic, strong, readonly) Protocol *protocol;
/// The timing of instantiation.
@property(nonatomic, readonly) FIRInstantiationTiming instantiationTiming;
/// A block to instantiate an instance of the component with the appropriate dependencies.
@property(nonatomic, copy, readonly) FIRComponentCreationBlock creationBlock;
// There's an issue with long NS_SWIFT_NAMES that causes compilation to fail, disable clang-format
// for the next two methods.
// clang-format off
/// Creates a component with no dependencies that will be lazily initialized.
+ (instancetype)componentWithProtocol:(Protocol *)protocol
creationBlock:(FIRComponentCreationBlock)creationBlock
NS_SWIFT_NAME(init(_:creationBlock:));
/// Creates a component to be registered with the component container.
///
/// @param protocol - The protocol describing functionality provided by the component.
/// @param instantiationTiming - When the component should be initialized. Use .lazy unless there's
/// a good reason to be instantiated earlier.
/// @param creationBlock - A block to instantiate the component with a container, and if
/// @return A component that can be registered with the component container.
+ (instancetype)componentWithProtocol:(Protocol *)protocol
instantiationTiming:(FIRInstantiationTiming)instantiationTiming
creationBlock:(FIRComponentCreationBlock)creationBlock
NS_SWIFT_NAME(init(_:instantiationTiming:creationBlock:));
// clang-format on
/// Unavailable.
- (instancetype)init NS_UNAVAILABLE;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,45 @@
/*
* Copyright 2018 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
/// A type-safe macro to retrieve a component from a container. This should be used to retrieve
/// components instead of using the container directly.
#define FIR_COMPONENT(type, container) \
[FIRComponentType<id<type>> instanceForProtocol:@protocol(type) inContainer:container]
@class FIRApp;
/// A container that holds different components that are registered via the
/// `registerAsComponentRegistrant` call. These classes should conform to `ComponentRegistrant`
/// in order to properly register components for Core.
NS_SWIFT_NAME(FirebaseComponentContainer)
@interface FIRComponentContainer : NSObject
/// A weak reference to the app that an instance of the container belongs to.
@property(nonatomic, weak, readonly) FIRApp *app;
// TODO: See if we can get improved type safety here.
/// A Swift only API for fetching an instance since the top macro isn't available.
- (nullable id)__instanceForProtocol:(Protocol *)protocol NS_SWIFT_NAME(instance(for:));
/// Unavailable. Use the `container` property on `FirebaseApp`.
- (instancetype)init NS_UNAVAILABLE;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,35 @@
/*
* Copyright 2018 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>
@class FIRComponentContainer;
NS_ASSUME_NONNULL_BEGIN
/// Do not use directly. A placeholder type in order to provide a macro that will warn users of
/// mis-matched protocols.
NS_SWIFT_NAME(ComponentType)
@interface FIRComponentType<__covariant T> : NSObject
/// Do not use directly. A factory method to retrieve an instance that provides a specific
/// functionality.
+ (nullable T)instanceForProtocol:(Protocol *)protocol
inContainer:(FIRComponentContainer *)container;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,90 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
#ifndef FIREBASE_BUILD_CMAKE
@class FIRHeartbeatsPayload;
#endif // FIREBASE_BUILD_CMAKE
/// Enum representing different daily heartbeat codes.
/// This enum is only used by clients using platform logging V1. This is because
/// the V1 payload only supports a single daily heartbeat.
typedef NS_ENUM(NSInteger, FIRDailyHeartbeatCode) {
/// Represents the absence of a daily heartbeat.
FIRDailyHeartbeatCodeNone = 0,
/// Represents the presence of a daily heartbeat.
FIRDailyHeartbeatCodeSome = 2,
};
@protocol FIRHeartbeatLoggerProtocol <NSObject>
/// Asynchronously logs a heartbeat.
- (void)log;
#ifndef FIREBASE_BUILD_CMAKE
/// Return the headerValue for the HeartbeatLogger.
- (NSString *_Nullable)headerValue;
#endif // FIREBASE_BUILD_CMAKE
/// Gets the heartbeat code for today.
- (FIRDailyHeartbeatCode)heartbeatCodeForToday;
@end
#ifndef FIREBASE_BUILD_CMAKE
/// Returns a nullable string header value from a given heartbeats payload.
///
/// This API returns `nil` when the given heartbeats payload is considered empty.
///
/// @param heartbeatsPayload The heartbeats payload.
NSString *_Nullable FIRHeaderValueFromHeartbeatsPayload(FIRHeartbeatsPayload *heartbeatsPayload);
#endif // FIREBASE_BUILD_CMAKE
/// A thread safe, synchronized object that logs and flushes platform logging info.
@interface FIRHeartbeatLogger : NSObject <FIRHeartbeatLoggerProtocol>
/// Designated initializer.
///
/// @param appID The app ID that this heartbeat logger corresponds to.
- (instancetype)initWithAppID:(NSString *)appID;
/// Asynchronously logs a new heartbeat corresponding to the Firebase User Agent, if needed.
///
/// @note This API is thread-safe.
- (void)log;
#ifndef FIREBASE_BUILD_CMAKE
/// Flushes heartbeats from storage into a structured payload of heartbeats.
///
/// This API is for clients using platform logging V2.
///
/// @note This API is thread-safe.
/// @return A payload of heartbeats.
- (FIRHeartbeatsPayload *)flushHeartbeatsIntoPayload;
#endif // FIREBASE_BUILD_CMAKE
/// Gets today's corresponding heartbeat code.
///
/// This API is for clients using platform logging V1.
///
/// @note This API is thread-safe.
/// @return Heartbeat code indicating whether or not there is an unsent global heartbeat.
- (FIRDailyHeartbeatCode)heartbeatCodeForToday;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,39 @@
/*
* Copyright 2018 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.
*/
#ifndef FIRLibrary_h
#define FIRLibrary_h
#import <Foundation/Foundation.h>
@class FIRApp;
@class FIRComponent;
NS_ASSUME_NONNULL_BEGIN
/// Provide an interface to register a library for userAgent logging and availability to others.
NS_SWIFT_NAME(Library)
@protocol FIRLibrary
/// Returns one or more Components that will be registered in
/// FirebaseApp and participate in dependency resolution and injection.
+ (NSArray<FIRComponent *> *)componentsToRegister;
@end
NS_ASSUME_NONNULL_END
#endif /* FIRLibrary_h */

View File

@ -0,0 +1,148 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
#import <FirebaseCore/FIRLoggerLevel.h>
NS_ASSUME_NONNULL_BEGIN
/**
* The Firebase services used in Firebase logger.
*/
typedef NSString *const FIRLoggerService;
extern NSString *const kFIRLoggerAnalytics;
extern NSString *const kFIRLoggerCrash;
extern NSString *const kFIRLoggerCore;
extern NSString *const kFIRLoggerRemoteConfig;
/**
* The key used to store the logger's error count.
*/
extern NSString *const kFIRLoggerErrorCountKey;
/**
* The key used to store the logger's warning count.
*/
extern NSString *const kFIRLoggerWarningCountKey;
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
/**
* Enables or disables Analytics debug mode.
* If set to true, the logging level for Analytics will be set to FirebaseLoggerLevelDebug.
* Enabling the debug mode has no effect if the app is running from App Store.
* (required) analytics debug mode flag.
*/
void FIRSetAnalyticsDebugMode(BOOL analyticsDebugMode);
/**
* Gets the current FIRLoggerLevel.
*/
FIRLoggerLevel FIRGetLoggerLevel(void);
/**
* Changes the default logging level of FirebaseLoggerLevelNotice to a user-specified level.
* The default level cannot be set above FirebaseLoggerLevelNotice if the app is running from App
* Store. (required) log level (one of the FirebaseLoggerLevel enum values).
*/
void FIRSetLoggerLevel(FIRLoggerLevel loggerLevel);
/**
* Checks if the specified logger level is loggable given the current settings.
* (required) log level (one of the FirebaseLoggerLevel enum values).
* (required) whether or not this function is called from the Analytics component.
*/
BOOL FIRIsLoggableLevel(FIRLoggerLevel loggerLevel, BOOL analyticsComponent);
/**
* Logs a message to the Xcode console and the device log. If running from AppStore, will
* not log any messages with a level higher than FirebaseLoggerLevelNotice to avoid log spamming.
* (required) log level (one of the FirebaseLoggerLevel enum values).
* (required) service name of type FirebaseLoggerService.
* (required) message code starting with "I-" which means iOS, followed by a capitalized
* three-character service identifier and a six digit integer message ID that is unique
* within the service.
* An example of the message code is @"I-COR000001".
* (required) message string which can be a format string.
* (optional) variable arguments list obtained from calling va_start, used when message is a format
* string.
*/
extern void FIRLogBasic(FIRLoggerLevel level,
NSString *category,
NSString *messageCode,
NSString *message,
// On 64-bit simulators, va_list is not a pointer, so cannot be marked nullable
// See: http://stackoverflow.com/q/29095469
#if __LP64__ && TARGET_OS_SIMULATOR || TARGET_OS_OSX
va_list args_ptr
#else
va_list _Nullable args_ptr
#endif
);
/**
* The following functions accept the following parameters in order:
* (required) service name of type FirebaseLoggerService.
* (required) message code starting from "I-" which means iOS, followed by a capitalized
* three-character service identifier and a six digit integer message ID that is unique
* within the service.
* An example of the message code is @"I-COR000001".
* See go/firebase-log-proposal for details.
* (required) message string which can be a format string.
* (optional) the list of arguments to substitute into the format string.
* Example usage:
* FirebaseLogError(kFirebaseLoggerCore, @"I-COR000001", @"Configuration of %@ failed.", app.name);
*/
extern void FIRLogError(NSString *category, NSString *messageCode, NSString *message, ...)
NS_FORMAT_FUNCTION(3, 4);
extern void FIRLogWarning(NSString *category, NSString *messageCode, NSString *message, ...)
NS_FORMAT_FUNCTION(3, 4);
extern void FIRLogNotice(NSString *category, NSString *messageCode, NSString *message, ...)
NS_FORMAT_FUNCTION(3, 4);
extern void FIRLogInfo(NSString *category, NSString *messageCode, NSString *message, ...)
NS_FORMAT_FUNCTION(3, 4);
extern void FIRLogDebug(NSString *category, NSString *messageCode, NSString *message, ...)
NS_FORMAT_FUNCTION(3, 4);
#ifdef __cplusplus
} // extern "C"
#endif // __cplusplus
NS_SWIFT_NAME(FirebaseLogger)
@interface FIRLoggerWrapper : NSObject
/// Logs a given message at a given log level.
///
/// - Parameters:
/// - level: The log level to use (defined by `FirebaseLoggerLevel` enum values).
/// - service: The service name of type `FirebaseLoggerService`.
/// - code: The message code. Starting with "I-" which means iOS, followed by a capitalized
/// three-character service identifier and a six digit integer message ID that is unique within
/// the service. An example of the message code is @"I-COR000001".
/// - message: Formatted string to be used as the log's message.
+ (void)logWithLevel:(FIRLoggerLevel)level
service:(NSString *)category
code:(NSString *)code
message:(NSString *)message
__attribute__((__swift_name__("log(level:service:code:message:)")));
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,106 @@
/*
* Copyright 2017 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/FIROptions.h>
/**
* Keys for the strings in the plist file.
*/
extern NSString *const kFIRAPIKey;
extern NSString *const kFIRTrackingID;
extern NSString *const kFIRGoogleAppID;
extern NSString *const kFIRClientID;
extern NSString *const kFIRGCMSenderID;
extern NSString *const kFIRAndroidClientID;
extern NSString *const kFIRDatabaseURL;
extern NSString *const kFIRStorageBucket;
extern NSString *const kFIRBundleID;
extern NSString *const kFIRProjectID;
/**
* Keys for the plist file name
*/
extern NSString *const kServiceInfoFileName;
extern NSString *const kServiceInfoFileType;
/**
* This header file exposes the initialization of FirebaseOptions to internal use.
*/
@interface FIROptions ()
/**
* `resetDefaultOptions` and `initInternalWithOptionsDictionary` are exposed only for unit tests.
*/
+ (void)resetDefaultOptions;
/**
* Initializes the options with dictionary. The above strings are the keys of the dictionary.
* This is the designated initializer.
*/
- (instancetype)initInternalWithOptionsDictionary:(NSDictionary *)serviceInfoDictionary
NS_DESIGNATED_INITIALIZER;
/**
* `defaultOptions` and `defaultOptionsDictionary` are exposed in order to be used in FirebaseApp
* and other first party services.
*/
+ (FIROptions *)defaultOptions;
+ (NSDictionary *)defaultOptionsDictionary;
/**
* Indicates whether or not Analytics collection was explicitly enabled via a plist flag or at
* runtime.
*/
@property(nonatomic, readonly) BOOL isAnalyticsCollectionExplicitlySet;
/**
* Whether or not Analytics Collection was enabled. Analytics Collection is enabled unless
* explicitly disabled in GoogleService-Info.plist.
*/
@property(nonatomic, readonly) BOOL isAnalyticsCollectionEnabled;
/**
* Whether or not Analytics Collection was completely disabled. If true, then
* isAnalyticsCollectionEnabled will be false.
*/
@property(nonatomic, readonly) BOOL isAnalyticsCollectionDeactivated;
/**
* The version ID of the client library, e.g. @"1100000".
*/
@property(nonatomic, readonly, copy) NSString *libraryVersionID;
/**
* The flag indicating whether this object was constructed with the values in the default plist
* file.
*/
@property(nonatomic) BOOL usingOptionsFromDefaultPlist;
/**
* Whether or not Measurement was enabled. Measurement is enabled unless explicitly disabled in
* GoogleService-Info.plist.
*/
@property(nonatomic, readonly) BOOL isMeasurementEnabled;
/**
* Whether or not editing is locked. This should occur after `FirebaseOptions` has been set on a
* `FirebaseApp`.
*/
@property(nonatomic, getter=isEditingLocked) BOOL editingLocked;
@end

View File

@ -0,0 +1,24 @@
// 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.
@import FirebaseCore;
#import "FIRAppInternal.h"
#import "FIRComponent.h"
#import "FIRComponentContainer.h"
#import "FIRComponentType.h"
#import "FIRHeartbeatLogger.h"
#import "FIRLibrary.h"
#import "FIRLogger.h"
#import "FIROptionsInternal.h"

View File

@ -0,0 +1,19 @@
// 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.
// An umbrella header, for any other libraries in this repo to access Firebase
// Installations Public headers. Any package manager complexity should be
// handled here.
#import <FirebaseInstallations/FirebaseInstallations.h>

View File

@ -0,0 +1,91 @@
/*
* 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 "FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h"
#import "FirebaseCore/Extension/FirebaseCoreInternal.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h"
@implementation FIRRemoteConfigValue {
/// Data backing the config value.
NSData *_data;
FIRRemoteConfigSource _source;
}
/// Designated initializer
- (instancetype)initWithData:(NSData *)data source:(FIRRemoteConfigSource)source {
self = [super init];
if (self) {
_data = [data copy];
_source = source;
}
return self;
}
/// Superclass's designated initializer
- (instancetype)init {
return [self initWithData:nil source:FIRRemoteConfigSourceStatic];
}
/// The string is a UTF-8 representation of NSData.
- (nonnull NSString *)stringValue {
return [[NSString alloc] initWithData:_data encoding:NSUTF8StringEncoding] ?: @"";
}
/// Number representation of a UTF-8 string.
- (NSNumber *)numberValue {
return [NSNumber numberWithDouble:self.stringValue.doubleValue];
}
/// Internal representation of the FIRRemoteConfigValue as a NSData object.
- (NSData *)dataValue {
return _data;
}
/// Boolean representation of a UTF-8 string.
- (BOOL)boolValue {
return self.stringValue.boolValue;
}
/// Returns a foundation object (NSDictionary / NSArray) representation for JSON data.
- (id)JSONValue {
NSError *error;
if (!_data) {
return nil;
}
id JSONObject = [NSJSONSerialization JSONObjectWithData:_data options:kNilOptions error:&error];
if (error) {
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000065", @"Error parsing data as JSON.");
return nil;
}
return JSONObject;
}
/// Debug description showing the representations of all types.
- (NSString *)debugDescription {
NSString *content = [NSString
stringWithFormat:@"Boolean: %d, String: %@, Number: %@, JSON:%@, Data: %@, Source: %zd",
self.boolValue, self.stringValue, self.numberValue, self.JSONValue, _data,
(long)self.source];
return [NSString stringWithFormat:@"<%@: %p, %@>", [self class], self, content];
}
/// Copy method.
- (id)copyWithZone:(NSZone *)zone {
FIRRemoteConfigValue *value = [[[self class] allocWithZone:zone] initWithData:_data];
return value;
}
@end

View File

@ -0,0 +1,698 @@
/*
* 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 "FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h"
#import "FirebaseABTesting/Sources/Private/FirebaseABTestingInternal.h"
#import "FirebaseCore/Extension/FirebaseCoreInternal.h"
#import "FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.h"
#import "FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h"
#import "FirebaseRemoteConfig/Sources/Private/RCNConfigFetch.h"
#import "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigContent.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigExperiment.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigRealtime.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h"
#import "FirebaseRemoteConfig/Sources/RCNDevice.h"
#import "FirebaseRemoteConfig/Sources/RCNPersonalization.h"
/// Remote Config Error Domain.
/// TODO: Rename according to obj-c style for constants.
NSString *const FIRRemoteConfigErrorDomain = @"com.google.remoteconfig.ErrorDomain";
// Remote Config Realtime Error Domain
NSString *const FIRRemoteConfigUpdateErrorDomain = @"com.google.remoteconfig.update.ErrorDomain";
/// Remote Config Error Info End Time Seconds;
NSString *const FIRRemoteConfigThrottledEndTimeInSecondsKey = @"error_throttled_end_time_seconds";
/// Minimum required time interval between fetch requests made to the backend.
static NSString *const kRemoteConfigMinimumFetchIntervalKey = @"_rcn_minimum_fetch_interval";
/// Timeout value for waiting on a fetch response.
static NSString *const kRemoteConfigFetchTimeoutKey = @"_rcn_fetch_timeout";
/// Notification when config is successfully activated
const NSNotificationName FIRRemoteConfigActivateNotification =
@"FIRRemoteConfigActivateNotification";
static NSNotificationName FIRRolloutsStateDidChangeNotificationName =
@"FIRRolloutsStateDidChangeNotification";
/// Listener for the get methods.
typedef void (^FIRRemoteConfigListener)(NSString *_Nonnull, NSDictionary *_Nonnull);
@implementation FIRRemoteConfigSettings
- (instancetype)init {
self = [super init];
if (self) {
_minimumFetchInterval = RCNDefaultMinimumFetchInterval;
_fetchTimeout = RCNHTTPDefaultConnectionTimeout;
}
return self;
}
@end
@implementation FIRRemoteConfig {
/// All the config content.
RCNConfigContent *_configContent;
RCNConfigDBManager *_DBManager;
RCNConfigSettings *_settings;
RCNConfigFetch *_configFetch;
RCNConfigExperiment *_configExperiment;
RCNConfigRealtime *_configRealtime;
dispatch_queue_t _queue;
NSString *_appName;
NSMutableArray *_listeners;
}
static NSMutableDictionary<NSString *, NSMutableDictionary<NSString *, FIRRemoteConfig *> *>
*RCInstances;
+ (nonnull FIRRemoteConfig *)remoteConfigWithApp:(FIRApp *_Nonnull)firebaseApp {
return [FIRRemoteConfig
remoteConfigWithFIRNamespace:FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform
app:firebaseApp];
}
+ (nonnull FIRRemoteConfig *)remoteConfigWithFIRNamespace:(NSString *_Nonnull)firebaseNamespace {
if (![FIRApp isDefaultAppConfigured]) {
[NSException raise:@"FIRAppNotConfigured"
format:@"The default `FirebaseApp` instance must be configured before the "
@"default Remote Config instance can be initialized. One way to ensure this "
@"is to call `FirebaseApp.configure()` in the App Delegate's "
@"`application(_:didFinishLaunchingWithOptions:)` or the `@main` struct's "
@"initializer in SwiftUI."];
}
return [FIRRemoteConfig remoteConfigWithFIRNamespace:firebaseNamespace app:[FIRApp defaultApp]];
}
+ (nonnull FIRRemoteConfig *)remoteConfigWithFIRNamespace:(NSString *_Nonnull)firebaseNamespace
app:(FIRApp *_Nonnull)firebaseApp {
// Use the provider to generate and return instances of FIRRemoteConfig for this specific app and
// namespace. This will ensure the app is configured before Remote Config can return an instance.
id<FIRRemoteConfigProvider> provider =
FIR_COMPONENT(FIRRemoteConfigProvider, firebaseApp.container);
return [provider remoteConfigForNamespace:firebaseNamespace];
}
+ (FIRRemoteConfig *)remoteConfig {
// If the default app is not configured at this point, warn the developer.
if (![FIRApp isDefaultAppConfigured]) {
[NSException raise:@"FIRAppNotConfigured"
format:@"The default `FirebaseApp` instance must be configured before the "
@"default Remote Config instance can be initialized. One way to ensure this "
@"is to call `FirebaseApp.configure()` in the App Delegate's "
@"`application(_:didFinishLaunchingWithOptions:)` or the `@main` struct's "
@"initializer in SwiftUI."];
}
return [FIRRemoteConfig
remoteConfigWithFIRNamespace:FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform
app:[FIRApp defaultApp]];
}
/// Singleton instance of serial queue for queuing all incoming RC calls.
+ (dispatch_queue_t)sharedRemoteConfigSerialQueue {
static dispatch_once_t onceToken;
static dispatch_queue_t sharedRemoteConfigQueue;
dispatch_once(&onceToken, ^{
sharedRemoteConfigQueue =
dispatch_queue_create(RCNRemoteConfigQueueLabel, DISPATCH_QUEUE_SERIAL);
});
return sharedRemoteConfigQueue;
}
/// Designated initializer
- (instancetype)initWithAppName:(NSString *)appName
FIROptions:(FIROptions *)options
namespace:(NSString *)FIRNamespace
DBManager:(RCNConfigDBManager *)DBManager
configContent:(RCNConfigContent *)configContent
analytics:(nullable id<FIRAnalyticsInterop>)analytics {
self = [super init];
if (self) {
_appName = appName;
_DBManager = DBManager;
// The fully qualified Firebase namespace is namespace:firappname.
_FIRNamespace = [NSString stringWithFormat:@"%@:%@", FIRNamespace, appName];
// Initialize RCConfigContent if not already.
_configContent = configContent;
_settings = [[RCNConfigSettings alloc] initWithDatabaseManager:_DBManager
namespace:_FIRNamespace
firebaseAppName:appName
googleAppID:options.googleAppID];
FIRExperimentController *experimentController = [FIRExperimentController sharedInstance];
_configExperiment = [[RCNConfigExperiment alloc] initWithDBManager:_DBManager
experimentController:experimentController];
/// Serial queue for read and write lock.
_queue = [FIRRemoteConfig sharedRemoteConfigSerialQueue];
// Initialize with default config settings.
[self setDefaultConfigSettings];
_configFetch = [[RCNConfigFetch alloc] initWithContent:_configContent
DBManager:_DBManager
settings:_settings
analytics:analytics
experiment:_configExperiment
queue:_queue
namespace:_FIRNamespace
options:options];
_configRealtime = [[RCNConfigRealtime alloc] init:_configFetch
settings:_settings
namespace:_FIRNamespace
options:options];
[_settings loadConfigFromMetadataTable];
if (analytics) {
_listeners = [[NSMutableArray alloc] init];
RCNPersonalization *personalization =
[[RCNPersonalization alloc] initWithAnalytics:analytics];
[self addListener:^(NSString *key, NSDictionary *config) {
[personalization logArmActive:key config:config];
}];
}
}
return self;
}
// Initialize with default config settings.
- (void)setDefaultConfigSettings {
// Set the default config settings.
self->_settings.fetchTimeout = RCNHTTPDefaultConnectionTimeout;
self->_settings.minimumFetchInterval = RCNDefaultMinimumFetchInterval;
}
- (void)ensureInitializedWithCompletionHandler:
(nonnull FIRRemoteConfigInitializationCompletion)completionHandler {
__weak FIRRemoteConfig *weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
FIRRemoteConfig *strongSelf = weakSelf;
if (!strongSelf) {
return;
}
BOOL initializationSuccess = [self->_configContent initializationSuccessful];
NSError *error = nil;
if (!initializationSuccess) {
error = [[NSError alloc]
initWithDomain:FIRRemoteConfigErrorDomain
code:FIRRemoteConfigErrorInternalError
userInfo:@{NSLocalizedDescriptionKey : @"Timed out waiting for database load."}];
}
completionHandler(error);
});
}
/// Adds a listener that will be called whenever one of the get methods is called.
/// @param listener Function that takes in the parameter key and the config.
- (void)addListener:(nonnull FIRRemoteConfigListener)listener {
@synchronized(_listeners) {
[_listeners addObject:listener];
}
}
- (void)callListeners:(NSString *)key config:(NSDictionary *)config {
@synchronized(_listeners) {
for (FIRRemoteConfigListener listener in _listeners) {
dispatch_async(_queue, ^{
listener(key, config);
});
}
}
}
#pragma mark - fetch
- (void)fetchWithCompletionHandler:(FIRRemoteConfigFetchCompletion)completionHandler {
dispatch_async(_queue, ^{
[self fetchWithExpirationDuration:self->_settings.minimumFetchInterval
completionHandler:completionHandler];
});
}
- (void)fetchWithExpirationDuration:(NSTimeInterval)expirationDuration
completionHandler:(FIRRemoteConfigFetchCompletion)completionHandler {
FIRRemoteConfigFetchCompletion completionHandlerCopy = nil;
if (completionHandler) {
completionHandlerCopy = [completionHandler copy];
}
[_configFetch fetchConfigWithExpirationDuration:expirationDuration
completionHandler:completionHandlerCopy];
}
#pragma mark - fetchAndActivate
- (void)fetchAndActivateWithCompletionHandler:
(FIRRemoteConfigFetchAndActivateCompletion)completionHandler {
__weak FIRRemoteConfig *weakSelf = self;
FIRRemoteConfigFetchCompletion fetchCompletion =
^(FIRRemoteConfigFetchStatus fetchStatus, NSError *fetchError) {
FIRRemoteConfig *strongSelf = weakSelf;
if (!strongSelf) {
return;
}
// Fetch completed. We are being called on the main queue.
// If fetch is successful, try to activate the fetched config
if (fetchStatus == FIRRemoteConfigFetchStatusSuccess && !fetchError) {
[strongSelf activateWithCompletion:^(BOOL changed, NSError *_Nullable activateError) {
if (completionHandler) {
FIRRemoteConfigFetchAndActivateStatus status =
activateError ? FIRRemoteConfigFetchAndActivateStatusSuccessUsingPreFetchedData
: FIRRemoteConfigFetchAndActivateStatusSuccessFetchedFromRemote;
dispatch_async(dispatch_get_main_queue(), ^{
completionHandler(status, nil);
});
}
}];
} else if (completionHandler) {
FIRRemoteConfigFetchAndActivateStatus status =
fetchStatus == FIRRemoteConfigFetchStatusSuccess
? FIRRemoteConfigFetchAndActivateStatusSuccessUsingPreFetchedData
: FIRRemoteConfigFetchAndActivateStatusError;
dispatch_async(dispatch_get_main_queue(), ^{
completionHandler(status, fetchError);
});
}
};
[self fetchWithCompletionHandler:fetchCompletion];
}
#pragma mark - activate
typedef void (^FIRRemoteConfigActivateChangeCompletion)(BOOL changed, NSError *_Nullable error);
- (void)activateWithCompletion:(FIRRemoteConfigActivateChangeCompletion)completion {
__weak FIRRemoteConfig *weakSelf = self;
void (^applyBlock)(void) = ^(void) {
FIRRemoteConfig *strongSelf = weakSelf;
if (!strongSelf) {
NSError *error = [NSError errorWithDomain:FIRRemoteConfigErrorDomain
code:FIRRemoteConfigErrorInternalError
userInfo:@{@"ActivationFailureReason" : @"Internal Error."}];
if (completion) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
completion(NO, error);
});
}
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000068", @"Internal error activating config.");
return;
}
// Check if the last fetched config has already been activated. Fetches with no data change are
// ignored.
if (strongSelf->_settings.lastETagUpdateTime == 0 ||
strongSelf->_settings.lastETagUpdateTime <= strongSelf->_settings.lastApplyTimeInterval) {
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000069",
@"Most recently fetched config is already activated.");
if (completion) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
completion(NO, nil);
});
}
return;
}
[strongSelf->_configContent copyFromDictionary:self->_configContent.fetchedConfig
toSource:RCNDBSourceActive
forNamespace:self->_FIRNamespace];
strongSelf->_settings.lastApplyTimeInterval = [[NSDate date] timeIntervalSince1970];
// New config has been activated at this point
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000069", @"Config activated.");
[strongSelf->_configContent activatePersonalization];
// Update last active template version number in setting and userDefaults.
[strongSelf->_settings updateLastActiveTemplateVersion];
// Update activeRolloutMetadata
[strongSelf->_configContent activateRolloutMetadata:^(BOOL success) {
if (success) {
[self notifyRolloutsStateChange:strongSelf->_configContent.activeRolloutMetadata
versionNumber:strongSelf->_settings.lastActiveTemplateVersion];
}
}];
// Update experiments only for 3p namespace
NSString *namespace = [strongSelf->_FIRNamespace
substringToIndex:[strongSelf->_FIRNamespace rangeOfString:@":"].location];
if ([namespace isEqualToString:FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self notifyConfigHasActivated];
});
[strongSelf->_configExperiment updateExperimentsWithHandler:^(NSError *_Nullable error) {
if (completion) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
completion(YES, nil);
});
}
}];
} else {
if (completion) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
completion(YES, nil);
});
}
}
};
dispatch_async(_queue, applyBlock);
}
- (void)notifyConfigHasActivated {
// Need a valid google app name.
if (!_appName) {
return;
}
// The Remote Config Swift SDK will be listening for this notification so it can tell SwiftUI to
// update the UI.
NSDictionary *appInfoDict = @{kFIRAppNameKey : _appName};
[[NSNotificationCenter defaultCenter] postNotificationName:FIRRemoteConfigActivateNotification
object:self
userInfo:appInfoDict];
}
#pragma mark - helpers
- (NSString *)fullyQualifiedNamespace:(NSString *)namespace {
// If this is already a fully qualified namespace, return.
if ([namespace rangeOfString:@":"].location != NSNotFound) {
return namespace;
}
NSString *fullyQualifiedNamespace = [NSString stringWithFormat:@"%@:%@", namespace, _appName];
return fullyQualifiedNamespace;
}
- (FIRRemoteConfigValue *)defaultValueForFullyQualifiedNamespace:(NSString *)namespace
key:(NSString *)key {
FIRRemoteConfigValue *value = self->_configContent.defaultConfig[namespace][key];
if (!value) {
value = [[FIRRemoteConfigValue alloc]
initWithData:[NSData data]
source:(FIRRemoteConfigSource)FIRRemoteConfigSourceStatic];
}
return value;
}
#pragma mark - Get Config Result
- (FIRRemoteConfigValue *)objectForKeyedSubscript:(NSString *)key {
return [self configValueForKey:key];
}
- (FIRRemoteConfigValue *)configValueForKey:(NSString *)key {
if (!key) {
return [[FIRRemoteConfigValue alloc] initWithData:[NSData data]
source:FIRRemoteConfigSourceStatic];
}
NSString *FQNamespace = [self fullyQualifiedNamespace:_FIRNamespace];
__block FIRRemoteConfigValue *value;
dispatch_sync(_queue, ^{
value = self->_configContent.activeConfig[FQNamespace][key];
if (value) {
if (value.source != FIRRemoteConfigSourceRemote) {
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000001",
@"Key %@ should come from source:%zd instead coming from source: %zd.", key,
(long)FIRRemoteConfigSourceRemote, (long)value.source);
}
[self callListeners:key
config:[self->_configContent getConfigAndMetadataForNamespace:FQNamespace]];
return;
}
value = [self defaultValueForFullyQualifiedNamespace:FQNamespace key:key];
});
return value;
}
- (FIRRemoteConfigValue *)configValueForKey:(NSString *)key source:(FIRRemoteConfigSource)source {
if (!key) {
return [[FIRRemoteConfigValue alloc] initWithData:[NSData data]
source:FIRRemoteConfigSourceStatic];
}
NSString *FQNamespace = [self fullyQualifiedNamespace:_FIRNamespace];
__block FIRRemoteConfigValue *value;
dispatch_sync(_queue, ^{
if (source == FIRRemoteConfigSourceRemote) {
value = self->_configContent.activeConfig[FQNamespace][key];
} else if (source == FIRRemoteConfigSourceDefault) {
value = self->_configContent.defaultConfig[FQNamespace][key];
} else {
value = [[FIRRemoteConfigValue alloc] initWithData:[NSData data]
source:FIRRemoteConfigSourceStatic];
}
});
return value;
}
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
objects:(id __unsafe_unretained[])stackbuf
count:(NSUInteger)len {
__block NSUInteger localValue;
dispatch_sync(_queue, ^{
localValue =
[self->_configContent.activeConfig[self->_FIRNamespace] countByEnumeratingWithState:state
objects:stackbuf
count:len];
});
return localValue;
}
#pragma mark - Properties
/// Last fetch completion time.
- (NSDate *)lastFetchTime {
__block NSDate *fetchTime;
dispatch_sync(_queue, ^{
NSTimeInterval lastFetchTime = self->_settings.lastFetchTimeInterval;
fetchTime = [NSDate dateWithTimeIntervalSince1970:lastFetchTime];
});
return fetchTime;
}
- (FIRRemoteConfigFetchStatus)lastFetchStatus {
__block FIRRemoteConfigFetchStatus currentStatus;
dispatch_sync(_queue, ^{
currentStatus = self->_settings.lastFetchStatus;
});
return currentStatus;
}
- (NSArray *)allKeysFromSource:(FIRRemoteConfigSource)source {
__block NSArray *keys = [[NSArray alloc] init];
dispatch_sync(_queue, ^{
NSString *FQNamespace = [self fullyQualifiedNamespace:self->_FIRNamespace];
switch (source) {
case FIRRemoteConfigSourceDefault:
if (self->_configContent.defaultConfig[FQNamespace]) {
keys = [[self->_configContent.defaultConfig[FQNamespace] allKeys] copy];
}
break;
case FIRRemoteConfigSourceRemote:
if (self->_configContent.activeConfig[FQNamespace]) {
keys = [[self->_configContent.activeConfig[FQNamespace] allKeys] copy];
}
break;
default:
break;
}
});
return keys;
}
- (nonnull NSSet *)keysWithPrefix:(nullable NSString *)prefix {
__block NSMutableSet *keys = [[NSMutableSet alloc] init];
dispatch_sync(_queue, ^{
NSString *FQNamespace = [self fullyQualifiedNamespace:self->_FIRNamespace];
if (self->_configContent.activeConfig[FQNamespace]) {
NSArray *allKeys = [self->_configContent.activeConfig[FQNamespace] allKeys];
if (!prefix.length) {
keys = [NSMutableSet setWithArray:allKeys];
} else {
for (NSString *key in allKeys) {
if ([key hasPrefix:prefix]) {
[keys addObject:key];
}
}
}
}
});
return [keys copy];
}
#pragma mark - Defaults
- (void)setDefaults:(NSDictionary<NSString *, NSObject *> *)defaultConfig {
NSString *FQNamespace = [self fullyQualifiedNamespace:_FIRNamespace];
NSDictionary *defaultConfigCopy = [[NSDictionary alloc] init];
if (defaultConfig) {
defaultConfigCopy = [defaultConfig copy];
}
void (^setDefaultsBlock)(void) = ^(void) {
NSDictionary *namespaceToDefaults = @{FQNamespace : defaultConfigCopy};
[self->_configContent copyFromDictionary:namespaceToDefaults
toSource:RCNDBSourceDefault
forNamespace:FQNamespace];
self->_settings.lastSetDefaultsTimeInterval = [[NSDate date] timeIntervalSince1970];
};
dispatch_async(_queue, setDefaultsBlock);
}
- (FIRRemoteConfigValue *)defaultValueForKey:(NSString *)key {
NSString *FQNamespace = [self fullyQualifiedNamespace:_FIRNamespace];
__block FIRRemoteConfigValue *value;
dispatch_sync(_queue, ^{
NSDictionary *defaultConfig = self->_configContent.defaultConfig;
value = defaultConfig[FQNamespace][key];
if (value) {
if (value.source != FIRRemoteConfigSourceDefault) {
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000002",
@"Key %@ should come from source:%zd instead coming from source: %zd", key,
(long)FIRRemoteConfigSourceDefault, (long)value.source);
}
}
});
return value;
}
- (void)setDefaultsFromPlistFileName:(nullable NSString *)fileName {
if (!fileName || fileName.length == 0) {
FIRLogWarning(kFIRLoggerRemoteConfig, @"I-RCN000037",
@"The plist file '%@' could not be found by Remote Config.", fileName);
return;
}
NSArray *bundles = @[ [NSBundle mainBundle], [NSBundle bundleForClass:[self class]] ];
for (NSBundle *bundle in bundles) {
NSString *plistFile = [bundle pathForResource:fileName ofType:@"plist"];
// Use the first one we find.
if (plistFile) {
NSDictionary *defaultConfig = [[NSDictionary alloc] initWithContentsOfFile:plistFile];
if (defaultConfig) {
[self setDefaults:defaultConfig];
}
return;
}
}
FIRLogWarning(kFIRLoggerRemoteConfig, @"I-RCN000037",
@"The plist file '%@' could not be found by Remote Config.", fileName);
}
#pragma mark - custom variables
- (FIRRemoteConfigSettings *)configSettings {
__block NSTimeInterval minimumFetchInterval = RCNDefaultMinimumFetchInterval;
__block NSTimeInterval fetchTimeout = RCNHTTPDefaultConnectionTimeout;
dispatch_sync(_queue, ^{
minimumFetchInterval = self->_settings.minimumFetchInterval;
fetchTimeout = self->_settings.fetchTimeout;
});
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000066",
@"Successfully read configSettings. Minimum Fetch Interval:%f, "
@"Fetch timeout: %f",
minimumFetchInterval, fetchTimeout);
FIRRemoteConfigSettings *settings = [[FIRRemoteConfigSettings alloc] init];
settings.minimumFetchInterval = minimumFetchInterval;
settings.fetchTimeout = fetchTimeout;
/// The NSURLSession needs to be recreated whenever the fetch timeout may be updated.
[_configFetch recreateNetworkSession];
return settings;
}
- (void)setConfigSettings:(FIRRemoteConfigSettings *)configSettings {
void (^setConfigSettingsBlock)(void) = ^(void) {
if (!configSettings) {
return;
}
self->_settings.minimumFetchInterval = configSettings.minimumFetchInterval;
self->_settings.fetchTimeout = configSettings.fetchTimeout;
/// The NSURLSession needs to be recreated whenever the fetch timeout may be updated.
[self->_configFetch recreateNetworkSession];
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000067",
@"Successfully set configSettings. Minimum Fetch Interval:%f, "
@"Fetch timeout:%f",
configSettings.minimumFetchInterval, configSettings.fetchTimeout);
};
dispatch_async(_queue, setConfigSettingsBlock);
}
#pragma mark - Realtime
- (FIRConfigUpdateListenerRegistration *)addOnConfigUpdateListener:
(void (^_Nonnull)(FIRRemoteConfigUpdate *update, NSError *_Nullable error))listener {
return [self->_configRealtime addConfigUpdateListener:listener];
}
#pragma mark - Rollout
- (void)addRemoteConfigInteropSubscriber:(id<FIRRolloutsStateSubscriber>)subscriber {
[[NSNotificationCenter defaultCenter]
addObserverForName:FIRRolloutsStateDidChangeNotificationName
object:self
queue:nil
usingBlock:^(NSNotification *_Nonnull notification) {
FIRRolloutsState *rolloutsState =
notification.userInfo[FIRRolloutsStateDidChangeNotificationName];
[subscriber rolloutsStateDidChange:rolloutsState];
}];
// Send active rollout metadata stored in persistence while app launched if there is activeConfig
NSString *fullyQualifiedNamespace = [self fullyQualifiedNamespace:_FIRNamespace];
NSDictionary<NSString *, NSDictionary *> *activeConfig = self->_configContent.activeConfig;
if (activeConfig[fullyQualifiedNamespace] && activeConfig[fullyQualifiedNamespace].count > 0) {
[self notifyRolloutsStateChange:self->_configContent.activeRolloutMetadata
versionNumber:self->_settings.lastActiveTemplateVersion];
}
}
- (void)notifyRolloutsStateChange:(NSArray<NSDictionary *> *)rolloutMetadata
versionNumber:(NSString *)versionNumber {
NSArray<FIRRolloutAssignment *> *rolloutsAssignments =
[self rolloutsAssignmentsWith:rolloutMetadata versionNumber:versionNumber];
FIRRolloutsState *rolloutsState =
[[FIRRolloutsState alloc] initWithAssignmentList:rolloutsAssignments];
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000069",
@"Send rollouts state notification with name %@ to RemoteConfigInterop.",
FIRRolloutsStateDidChangeNotificationName);
[[NSNotificationCenter defaultCenter]
postNotificationName:FIRRolloutsStateDidChangeNotificationName
object:self
userInfo:@{FIRRolloutsStateDidChangeNotificationName : rolloutsState}];
}
- (NSArray<FIRRolloutAssignment *> *)rolloutsAssignmentsWith:
(NSArray<NSDictionary *> *)rolloutMetadata
versionNumber:(NSString *)versionNumber {
NSMutableArray<FIRRolloutAssignment *> *rolloutsAssignments = [[NSMutableArray alloc] init];
NSString *FQNamespace = [self fullyQualifiedNamespace:_FIRNamespace];
for (NSDictionary *metadata in rolloutMetadata) {
NSString *rolloutId = metadata[RCNFetchResponseKeyRolloutID];
NSString *variantID = metadata[RCNFetchResponseKeyVariantID];
NSArray<NSString *> *affectedParameterKeys = metadata[RCNFetchResponseKeyAffectedParameterKeys];
if (rolloutId && variantID && affectedParameterKeys) {
for (NSString *key in affectedParameterKeys) {
FIRRemoteConfigValue *value = self->_configContent.activeConfig[FQNamespace][key];
if (!value) {
value = [self defaultValueForFullyQualifiedNamespace:FQNamespace key:key];
}
FIRRolloutAssignment *assignment =
[[FIRRolloutAssignment alloc] initWithRolloutId:rolloutId
variantId:variantID
templateVersion:[versionNumber longLongValue]
parameterKey:key
parameterValue:value.stringValue];
[rolloutsAssignments addObject:assignment];
}
}
}
return rolloutsAssignments;
}
@end

View File

@ -0,0 +1,64 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
#import "FirebaseCore/Extension/FirebaseCoreInternal.h"
@import FirebaseRemoteConfigInterop;
@class FIRApp;
@class FIRRemoteConfig;
NS_ASSUME_NONNULL_BEGIN
/// Provides and creates instances of Remote Config based on the namespace provided. Used in the
/// interop registration process to keep track of RC instances for each `FIRApp` instance.
@protocol FIRRemoteConfigProvider
/// Cached instances of Remote Config objects.
@property(nonatomic, strong) NSMutableDictionary<NSString *, FIRRemoteConfig *> *instances;
/// Default method for retrieving a Remote Config instance, or creating one if it doesn't exist.
- (FIRRemoteConfig *)remoteConfigForNamespace:(NSString *)remoteConfigNamespace;
@end
/// A concrete implementation for FIRRemoteConfigInterop to create Remote Config instances and
/// register with Core's component system.
@interface FIRRemoteConfigComponent
: NSObject <FIRRemoteConfigProvider, FIRLibrary, FIRRemoteConfigInterop>
/// The FIRApp that instances will be set up with.
@property(nonatomic, weak, readonly) FIRApp *app;
/// Cached instances of Remote Config objects.
@property(nonatomic, strong) NSMutableDictionary<NSString *, FIRRemoteConfig *> *instances;
/// Clear all the component instances from the singleton which created previously, this is for
/// testing only
+ (void)clearAllComponentInstances;
/// Default method for retrieving a Remote Config instance, or creating one if it doesn't exist.
- (FIRRemoteConfig *)remoteConfigForNamespace:(NSString *)remoteConfigNamespace;
/// Default initializer.
- (instancetype)initWithApp:(FIRApp *)app NS_DESIGNATED_INITIALIZER;
- (instancetype)init __attribute__((unavailable("Use `initWithApp:`.")));
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,151 @@
/*
* 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 "FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.h"
#import "FirebaseCore/Extension/FirebaseCoreInternal.h"
#import "FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigContent.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h"
#import "Interop/Analytics/Public/FIRAnalyticsInterop.h"
@implementation FIRRemoteConfigComponent
// Because Component now need to register two protocols (provider and interop), we need a way to
// return the same component instance for both registered protocol, this singleton pattern allow us
// to return the same component object for both registration callback.
static NSMutableDictionary<NSString *, FIRRemoteConfigComponent *> *_componentInstances = nil;
+ (FIRRemoteConfigComponent *)getComponentForApp:(FIRApp *)app {
@synchronized(_componentInstances) {
// need to init the dictionary first
if (!_componentInstances) {
_componentInstances = [[NSMutableDictionary alloc] init];
}
if (![_componentInstances objectForKey:app.name]) {
_componentInstances[app.name] = [[self alloc] initWithApp:app];
}
return _componentInstances[app.name];
}
return nil;
}
+ (void)clearAllComponentInstances {
@synchronized(_componentInstances) {
[_componentInstances removeAllObjects];
}
}
/// Default method for retrieving a Remote Config instance, or creating one if it doesn't exist.
- (FIRRemoteConfig *)remoteConfigForNamespace:(NSString *)remoteConfigNamespace {
if (!remoteConfigNamespace) {
// TODO: Throw an error? Return nil? What do we want to do?
return nil;
}
// Validate the required information is available.
FIROptions *options = self.app.options;
NSString *errorPropertyName;
if (options.googleAppID.length == 0) {
errorPropertyName = @"googleAppID";
} else if (options.GCMSenderID.length == 0) {
errorPropertyName = @"GCMSenderID";
} else if (options.projectID.length == 0) {
errorPropertyName = @"projectID";
}
if (errorPropertyName) {
NSString *const kFirebaseConfigErrorDomain = @"com.firebase.config";
[NSException
raise:kFirebaseConfigErrorDomain
format:@"%@",
[NSString
stringWithFormat:
@"Firebase Remote Config is missing the required %@ property from the "
@"configured FirebaseApp and will not be able to function properly. Please "
@"fix this issue to ensure that Firebase is correctly configured.",
errorPropertyName]];
}
FIRRemoteConfig *instance = self.instances[remoteConfigNamespace];
if (!instance) {
FIRApp *app = self.app;
id<FIRAnalyticsInterop> analytics =
app.isDefaultApp ? FIR_COMPONENT(FIRAnalyticsInterop, app.container) : nil;
instance = [[FIRRemoteConfig alloc] initWithAppName:app.name
FIROptions:app.options
namespace:remoteConfigNamespace
DBManager:[RCNConfigDBManager sharedInstance]
configContent:[RCNConfigContent sharedInstance]
analytics:analytics];
self.instances[remoteConfigNamespace] = instance;
}
return instance;
}
/// Default initializer.
- (instancetype)initWithApp:(FIRApp *)app {
self = [super init];
if (self) {
_app = app;
_instances = [[NSMutableDictionary alloc] initWithCapacity:1];
}
return self;
}
#pragma mark - Lifecycle
+ (void)load {
// Register as an internal library to be part of the initialization process. The name comes from
// go/firebase-sdk-platform-info.
[FIRApp registerInternalLibrary:self withName:@"fire-rc"];
}
#pragma mark - Interoperability
+ (NSArray<FIRComponent *> *)componentsToRegister {
FIRComponent *rcProvider = [FIRComponent
componentWithProtocol:@protocol(FIRRemoteConfigProvider)
instantiationTiming:FIRInstantiationTimingAlwaysEager
creationBlock:^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) {
// Cache the component so instances of Remote Config are cached.
*isCacheable = YES;
return [FIRRemoteConfigComponent getComponentForApp:container.app];
}];
// Unlike provider needs to setup a hard dependency on remote config, interop allows an optional
// dependency on RC
FIRComponent *rcInterop = [FIRComponent
componentWithProtocol:@protocol(FIRRemoteConfigInterop)
instantiationTiming:FIRInstantiationTimingAlwaysEager
creationBlock:^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) {
// Cache the component so instances of Remote Config are cached.
*isCacheable = YES;
return [FIRRemoteConfigComponent getComponentForApp:container.app];
}];
return @[ rcProvider, rcInterop ];
}
#pragma mark - Remote Config Interop Protocol
- (void)registerRolloutsStateSubscriber:(id<FIRRolloutsStateSubscriber>)subscriber
for:(NSString * _Nonnull)namespace {
FIRRemoteConfig *instance = [self remoteConfigForNamespace:namespace];
[instance addRemoteConfigInteropSubscriber:subscriber];
}
@end

View File

@ -0,0 +1,33 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
#import "FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h"
@implementation FIRRemoteConfigUpdate {
NSSet<NSString *> *_updatedKeys;
}
- (instancetype)initWithUpdatedKeys:(NSSet<NSString *> *)updatedKeys {
self = [super init];
if (self) {
_updatedKeys = [updatedKeys copy];
}
return self;
}
@end

View File

@ -0,0 +1,87 @@
/*
* 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 <FirebaseRemoteConfig/FIRRemoteConfig.h>
#import "RCNConfigSettings.h" // This import is needed to expose settings for the Swift API tests.
@class FIROptions;
@class RCNConfigContent;
@class RCNConfigDBManager;
@class RCNConfigFetch;
@class RCNConfigRealtime;
@protocol FIRAnalyticsInterop;
@protocol FIRRolloutsStateSubscriber;
NS_ASSUME_NONNULL_BEGIN
@class RCNConfigSettings;
@interface FIRRemoteConfigUpdate ()
/// Designated initializer.
- (instancetype)initWithUpdatedKeys:(NSSet<NSString *> *)updatedKeys;
@end
@interface FIRRemoteConfig () {
NSString *_FIRNamespace;
}
/// Internal settings
@property(nonatomic, readonly, strong) RCNConfigSettings *settings;
/// Config settings are custom settings.
@property(nonatomic, readwrite, strong, nonnull) RCNConfigFetch *configFetch;
@property(nonatomic, readwrite, strong, nonnull) RCNConfigRealtime *configRealtime;
/// Returns the FIRRemoteConfig instance for your namespace and for the default Firebase App.
/// This singleton object contains the complete set of Remote Config parameter values available to
/// the app, including the Active Config and Default Config.. This object also caches values fetched
/// from the Remote Config Server until they are copied to the Active Config by calling
/// activateFetched. When you fetch values from the Remote Config Server using the default Firebase
/// namespace service, you should use this class method to create a shared instance of the
/// FIRRemoteConfig object to ensure that your app will function properly with the Remote Config
/// Server and the Firebase service. This API is used internally by 2P teams.
+ (FIRRemoteConfig *)remoteConfigWithFIRNamespace:(NSString *)remoteConfigNamespace
NS_SWIFT_NAME(remoteConfig(FIRNamespace:));
/// Returns the FIRRemoteConfig instance for your namespace and for the default 3P developer's app.
/// This singleton object contains the complete set of Remote Config parameter values available to
/// the app, including the Active Config and Default Config. This object also caches values fetched
/// from the Remote Config Server until they are copied to the Active Config by calling
/// activateFetched. When you fetch values from the Remote Config Server using the default Firebase
/// namespace service, you should use this class method to create a shared instance of the
/// FIRRemoteConfig object to ensure that your app will function properly with the Remote Config
/// Server and the Firebase service.
+ (FIRRemoteConfig *)remoteConfigWithFIRNamespace:(NSString *)remoteConfigNamespace
app:(FIRApp *)app
NS_SWIFT_NAME(remoteConfig(FIRNamespace:app:));
/// Initialize a FIRRemoteConfig instance with all the required parameters directly. This exists so
/// tests can create FIRRemoteConfig objects without needing FIRApp.
- (instancetype)initWithAppName:(NSString *)appName
FIROptions:(FIROptions *)options
namespace:(NSString *)FIRNamespace
DBManager:(RCNConfigDBManager *)DBManager
configContent:(RCNConfigContent *)configContent
analytics:(nullable id<FIRAnalyticsInterop>)analytics;
/// Register RolloutsStateSubcriber to FIRRemoteConfig instance
- (void)addRemoteConfigInteropSubscriber:(id<FIRRolloutsStateSubscriber> _Nonnull)subscriber;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,76 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
#import "FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h"
#import "Interop/Analytics/Public/FIRAnalyticsInterop.h"
@class FIROptions;
@class RCNConfigContent;
@class RCNConfigSettings;
@class RCNConfigExperiment;
@class RCNConfigDBManager;
NS_ASSUME_NONNULL_BEGIN
/// Completion handler invoked by NSSessionFetcher.
typedef void (^RCNConfigFetcherCompletion)(NSData *data, NSURLResponse *response, NSError *error);
/// Completion handler invoked after a fetch that contains the updated keys
typedef void (^RCNConfigFetchCompletion)(FIRRemoteConfigFetchStatus status,
FIRRemoteConfigUpdate *update,
NSError *error);
@interface RCNConfigFetch : NSObject
- (instancetype)init NS_UNAVAILABLE;
/// Designated initializer
- (instancetype)initWithContent:(RCNConfigContent *)content
DBManager:(RCNConfigDBManager *)DBManager
settings:(RCNConfigSettings *)settings
analytics:(nullable id<FIRAnalyticsInterop>)analytics
experiment:(nullable RCNConfigExperiment *)experiment
queue:(dispatch_queue_t)queue
namespace:(NSString *)firebaseNamespace
options:(FIROptions *)firebaseOptions NS_DESIGNATED_INITIALIZER;
/// Fetches config data keyed by namespace. Completion block will be called on the main queue.
/// @param expirationDuration Expiration duration, in seconds.
/// @param completionHandler Callback handler.
- (void)fetchConfigWithExpirationDuration:(NSTimeInterval)expirationDuration
completionHandler:(FIRRemoteConfigFetchCompletion)completionHandler;
/// Fetches config data immediately, keyed by namespace. Completion block will be called on the main
/// queue.
/// @param fetchAttemptNumber The number of the fetch attempt.
/// @param completionHandler Callback handler.
- (void)realtimeFetchConfigWithNoExpirationDuration:(NSInteger)fetchAttemptNumber
completionHandler:(RCNConfigFetchCompletion)completionHandler;
/// Add the ability to update NSURLSession's timeout after a session has already been created.
- (void)recreateNetworkSession;
/// Provide fetchSession for tests to override.
@property(nonatomic, readwrite, strong, nonnull) NSURLSession *fetchSession;
/// Provide config template version number for Realtime config client.
@property(nonatomic, copy, nonnull) NSString *templateVersionNumber;
NS_ASSUME_NONNULL_END
@end

View File

@ -0,0 +1,152 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
#import <FirebaseRemoteConfig/FIRRemoteConfig.h>
@class RCNConfigDBManager;
/// This internal class contains a set of variables that are unique among all the config instances.
/// It also handles all metadata and internal metadata. This class is not thread safe and does not
/// inherently allow for synchronized access. Callers are responsible for synchronization
/// (currently using serial dispatch queues).
@interface RCNConfigSettings : NSObject
/// The time interval that config data stays fresh.
@property(nonatomic, readwrite, assign) NSTimeInterval minimumFetchInterval;
/// The timeout to set for outgoing fetch requests.
@property(nonatomic, readwrite, assign) NSTimeInterval fetchTimeout;
// The Google App ID of the configured FIRApp.
@property(nonatomic, readwrite, copy) NSString *googleAppID;
#pragma mark - Data required by config request.
/// Device authentication ID required by config request.
@property(nonatomic, copy) NSString *deviceAuthID;
/// Secret Token required by config request.
@property(nonatomic, copy) NSString *secretToken;
/// Device data version of checkin information.
@property(nonatomic, copy) NSString *deviceDataVersion;
/// InstallationsID.
@property(nonatomic, copy) NSString *configInstallationsIdentifier;
/// Installations token.
@property(nonatomic, copy) NSString *configInstallationsToken;
/// A list of successful fetch timestamps in milliseconds.
/// TODO Not used anymore. Safe to remove.
@property(nonatomic, readonly, copy) NSArray *successFetchTimes;
/// A list of failed fetch timestamps in milliseconds.
@property(nonatomic, readonly, copy) NSArray *failureFetchTimes;
/// Custom variable (aka App context digest). This is the pending custom variables request before
/// fetching.
@property(nonatomic, copy) NSDictionary *customVariables;
/// Cached internal metadata from internal metadata table. It contains customized information such
/// as HTTP connection timeout, HTTP read timeout, success/failure throttling rate and time
/// interval. Client has the default value of each parameters, they are only saved in
/// internalMetadata if they have been customize by developers.
@property(nonatomic, readonly, copy) NSDictionary *internalMetadata;
/// Device conditions since last successful fetch from the backend. Device conditions including
/// app
/// version, iOS version, device localte, language, GMP project ID and Game project ID. Used for
/// determing whether to throttle.
@property(nonatomic, readonly, copy) NSDictionary *deviceContext;
/// Bundle Identifier
@property(nonatomic, readonly, copy) NSString *bundleIdentifier;
/// The time of last successful config fetch.
@property(nonatomic, readonly, assign) NSTimeInterval lastFetchTimeInterval;
/// Last fetch status.
@property(nonatomic, readwrite, assign) FIRRemoteConfigFetchStatus lastFetchStatus;
/// The reason that last fetch failed.
@property(nonatomic, readwrite, assign) FIRRemoteConfigError lastFetchError;
/// The time of last apply timestamp.
@property(nonatomic, readwrite, assign) NSTimeInterval lastApplyTimeInterval;
/// The time of last setDefaults timestamp.
@property(nonatomic, readwrite, assign) NSTimeInterval lastSetDefaultsTimeInterval;
/// The latest eTag value stored from the last successful response.
@property(nonatomic, readwrite, assign) NSString *lastETag;
/// The timestamp of the last eTag update.
@property(nonatomic, readwrite, assign) NSTimeInterval lastETagUpdateTime;
/// Last fetched template version.
@property(nonatomic, readwrite, assign) NSString *lastFetchedTemplateVersion;
/// Last active template version.
@property(nonatomic, readwrite, assign) NSString *lastActiveTemplateVersion;
#pragma mark Throttling properties
/// Throttling intervals are based on https://cloud.google.com/storage/docs/exponential-backoff
/// Returns true if client has fetched config and has not got back from server. This is used to
/// determine whether there is another config task infight when fetching.
@property(atomic, readwrite, assign) BOOL isFetchInProgress;
/// Returns the current retry interval in seconds set for exponential backoff.
@property(nonatomic, readwrite, assign) double exponentialBackoffRetryInterval;
/// Returns the time in seconds until the next request is allowed while in exponential backoff mode.
@property(nonatomic, readonly, assign) NSTimeInterval exponentialBackoffThrottleEndTime;
/// Returns the current retry interval in seconds set for exponential backoff for the Realtime
/// service.
@property(nonatomic, readwrite, assign) double realtimeExponentialBackoffRetryInterval;
/// Returns the time in seconds until the next request is allowed while in exponential backoff mode
/// for the Realtime service.
@property(nonatomic, readonly, assign) NSTimeInterval realtimeExponentialBackoffThrottleEndTime;
/// Realtime connection attempts.
@property(nonatomic, readwrite, assign) int realtimeRetryCount;
#pragma mark Throttling Methods
/// Designated initializer.
- (instancetype)initWithDatabaseManager:(RCNConfigDBManager *)manager
namespace:(NSString *)FIRNamespace
firebaseAppName:(NSString *)appName
googleAppID:(NSString *)googleAppID;
/// Returns a fetch request with the latest device and config change.
/// Whenever user issues a fetch api call, collect the latest request.
/// @param userProperties User properties to set to config request.
/// @return Config fetch request string
- (NSString *)nextRequestWithUserProperties:(NSDictionary *)userProperties;
/// Returns metadata from metadata table.
- (NSDictionary *)loadConfigFromMetadataTable;
/// Updates internal content with the latest successful config response.
- (void)updateInternalContentWithResponse:(NSDictionary *)response;
/// Updates the metadata table with the current fetch status.
/// @param fetchSuccess True if fetch was successful.
- (void)updateMetadataWithFetchSuccessStatus:(BOOL)fetchSuccess
templateVersion:(NSString *)templateVersion;
/// Increases the throttling time. Should only be called if the fetch error indicates a server
/// issue.
- (void)updateExponentialBackoffTime;
/// Increases the throttling time for Realtime. Should only be called if the Realtime error
/// indicates a server issue.
- (void)updateRealtimeExponentialBackoffTime;
/// Update last active template version from last fetched template version.
- (void)updateLastActiveTemplateVersion;
/// Returns the difference between the Realtime backoff end time and the current time in a
/// NSTimeInterval format.
- (NSTimeInterval)getRealtimeBackoffInterval;
/// Returns true if we are in exponential backoff mode and it is not yet the next request time.
- (BOOL)shouldThrottle;
/// Returns true if the last fetch is outside the minimum fetch interval supplied.
- (BOOL)hasMinimumFetchIntervalElapsed:(NSTimeInterval)minimumFetchInterval;
@end

View File

@ -0,0 +1,360 @@
/*
* 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>
@class FIRApp;
/// The Firebase Remote Config service default namespace, to be used if the API method does not
/// specify a different namespace. Use the default namespace if configuring from the Google Firebase
/// service.
extern NSString *const _Nonnull FIRNamespaceGoogleMobilePlatform NS_SWIFT_NAME(
NamespaceGoogleMobilePlatform);
/// Key used to manage throttling in NSError user info when the refreshing of Remote Config
/// parameter values (data) is throttled. The value of this key is the elapsed time since 1970,
/// measured in seconds.
extern NSString *const _Nonnull FIRRemoteConfigThrottledEndTimeInSecondsKey NS_SWIFT_NAME(
RemoteConfigThrottledEndTimeInSecondsKey);
/**
* Listener registration returned by `addOnConfigUpdateListener`. Calling its method `remove` stops
* the associated listener from receiving config updates and unregisters itself.
*
* If remove is called and no other listener registrations remain, the connection to the real-time
* RC backend is closed. Subsequently calling `addOnConfigUpdateListener` will re-open the
* connection.
*/
NS_SWIFT_NAME(ConfigUpdateListenerRegistration)
@interface FIRConfigUpdateListenerRegistration : NSObject
/**
* Removes the listener associated with this `ConfigUpdateListenerRegistration`. After the
* initial call, subsequent calls have no effect.
*/
- (void)remove;
@end
/// Indicates whether updated data was successfully fetched.
typedef NS_ENUM(NSInteger, FIRRemoteConfigFetchStatus) {
/// Config has never been fetched.
FIRRemoteConfigFetchStatusNoFetchYet,
/// Config fetch succeeded.
FIRRemoteConfigFetchStatusSuccess,
/// Config fetch failed.
FIRRemoteConfigFetchStatusFailure,
/// Config fetch was throttled.
FIRRemoteConfigFetchStatusThrottled,
} NS_SWIFT_NAME(RemoteConfigFetchStatus);
/// Indicates whether updated data was successfully fetched and activated.
typedef NS_ENUM(NSInteger, FIRRemoteConfigFetchAndActivateStatus) {
/// The remote fetch succeeded and fetched data was activated.
FIRRemoteConfigFetchAndActivateStatusSuccessFetchedFromRemote,
/// The fetch and activate succeeded from already fetched but yet unexpired config data. You can
/// control this using minimumFetchInterval property in FIRRemoteConfigSettings.
FIRRemoteConfigFetchAndActivateStatusSuccessUsingPreFetchedData,
/// The fetch and activate failed.
FIRRemoteConfigFetchAndActivateStatusError
} NS_SWIFT_NAME(RemoteConfigFetchAndActivateStatus);
/// Remote Config error domain that handles errors when fetching data from the service.
extern NSString *const _Nonnull FIRRemoteConfigErrorDomain NS_SWIFT_NAME(RemoteConfigErrorDomain);
/// Firebase Remote Config service fetch error.
typedef NS_ERROR_ENUM(FIRRemoteConfigErrorDomain, FIRRemoteConfigError){
/// Unknown or no error.
FIRRemoteConfigErrorUnknown = 8001,
/// Frequency of fetch requests exceeds throttled limit.
FIRRemoteConfigErrorThrottled = 8002,
/// Internal error that covers all internal HTTP errors.
FIRRemoteConfigErrorInternalError = 8003,
} NS_SWIFT_NAME(RemoteConfigError);
/// Remote Config error domain that handles errors for the real-time config update service.
extern NSString *const _Nonnull FIRRemoteConfigUpdateErrorDomain NS_SWIFT_NAME(RemoteConfigUpdateErrorDomain);
/// Firebase Remote Config real-time config update service error.
typedef NS_ERROR_ENUM(FIRRemoteConfigUpdateErrorDomain, FIRRemoteConfigUpdateError){
/// Unable to make a connection to the Remote Config backend.
FIRRemoteConfigUpdateErrorStreamError = 8001,
/// Unable to fetch the latest version of the config.
FIRRemoteConfigUpdateErrorNotFetched = 8002,
/// The ConfigUpdate message was unparsable.
FIRRemoteConfigUpdateErrorMessageInvalid = 8003,
/// The Remote Config real-time config update service is unavailable.
FIRRemoteConfigUpdateErrorUnavailable = 8004,
} NS_SWIFT_NAME(RemoteConfigUpdateError);
/// Enumerated value that indicates the source of Remote Config data. Data can come from
/// the Remote Config service, the DefaultConfig that is available when the app is first installed,
/// or a static initialized value if data is not available from the service or DefaultConfig.
typedef NS_ENUM(NSInteger, FIRRemoteConfigSource) {
FIRRemoteConfigSourceRemote, ///< The data source is the Remote Config service.
FIRRemoteConfigSourceDefault, ///< The data source is the DefaultConfig defined for this app.
FIRRemoteConfigSourceStatic, ///< The data doesn't exist, return a static initialized value.
} NS_SWIFT_NAME(RemoteConfigSource);
/// Completion handler invoked by fetch methods when they get a response from the server.
///
/// @param status Config fetching status.
/// @param error Error message on failure.
typedef void (^FIRRemoteConfigFetchCompletion)(FIRRemoteConfigFetchStatus status,
NSError *_Nullable error)
NS_SWIFT_UNAVAILABLE("Use Swift's closure syntax instead.");
/// Completion handler invoked by activate method upon completion.
/// @param error Error message on failure. Nil if activation was successful.
typedef void (^FIRRemoteConfigActivateCompletion)(NSError *_Nullable error)
NS_SWIFT_UNAVAILABLE("Use Swift's closure syntax instead.");
/// Completion handler invoked upon completion of Remote Config initialization.
///
/// @param initializationError nil if initialization succeeded.
typedef void (^FIRRemoteConfigInitializationCompletion)(NSError *_Nullable initializationError)
NS_SWIFT_UNAVAILABLE("Use Swift's closure syntax instead.");
/// Completion handler invoked by the fetchAndActivate method. Used to convey status of fetch and,
/// if successful, resultant activate call
/// @param status Config fetching status.
/// @param error Error message on failure of config fetch
typedef void (^FIRRemoteConfigFetchAndActivateCompletion)(
FIRRemoteConfigFetchAndActivateStatus status, NSError *_Nullable error)
NS_SWIFT_UNAVAILABLE("Use Swift's closure syntax instead.");
#pragma mark - FIRRemoteConfigValue
/// This class provides a wrapper for Remote Config parameter values, with methods to get parameter
/// values as different data types.
NS_SWIFT_NAME(RemoteConfigValue)
@interface FIRRemoteConfigValue : NSObject <NSCopying>
/// Gets the value as a string.
@property(nonatomic, readonly, nonnull) NSString *stringValue;
/// Gets the value as a number value.
@property(nonatomic, readonly, nonnull) NSNumber *numberValue;
/// Gets the value as a NSData object.
@property(nonatomic, readonly, nonnull) NSData *dataValue;
/// Gets the value as a boolean.
@property(nonatomic, readonly) BOOL boolValue;
/// Gets a foundation object (NSDictionary / NSArray) by parsing the value as JSON. This method uses
/// NSJSONSerialization's JSONObjectWithData method with an options value of 0.
@property(nonatomic, readonly, nullable) id JSONValue NS_SWIFT_NAME(jsonValue);
/// Identifies the source of the fetched value.
@property(nonatomic, readonly) FIRRemoteConfigSource source;
@end
#pragma mark - FIRRemoteConfigSettings
/// Firebase Remote Config settings.
NS_SWIFT_NAME(RemoteConfigSettings)
@interface FIRRemoteConfigSettings : NSObject
/// Indicates the default value in seconds to set for the minimum interval that needs to elapse
/// before a fetch request can again be made to the Remote Config backend. After a fetch request to
/// the backend has succeeded, no additional fetch requests to the backend will be allowed until the
/// minimum fetch interval expires. Note that you can override this default on a per-fetch request
/// basis using `RemoteConfig.fetch(withExpirationDuration:)`. For example, setting
/// the expiration duration to 0 in the fetch request will override the `minimumFetchInterval` and
/// allow the request to proceed.
@property(nonatomic, assign) NSTimeInterval minimumFetchInterval;
/// Indicates the default value in seconds to abandon a pending fetch request made to the backend.
/// This value is set for outgoing requests as the `timeoutIntervalForRequest` as well as the
/// `timeoutIntervalForResource` on the `NSURLSession`'s configuration.
@property(nonatomic, assign) NSTimeInterval fetchTimeout;
@end
#pragma mark - FIRRemoteConfigUpdate
/// Used by Remote Config real-time config update service, this class represents changes between the
/// newly fetched config and the current one. An instance of this class is passed to
/// `FIRRemoteConfigUpdateCompletion` when a new config version has been automatically fetched.
NS_SWIFT_NAME(RemoteConfigUpdate)
@interface FIRRemoteConfigUpdate : NSObject
/// Parameter keys whose values have been updated from the currently activated values. Includes
/// keys that are added, deleted, and whose value, value source, or metadata has changed.
@property(nonatomic, readonly, nonnull) NSSet<NSString *> *updatedKeys;
@end
#pragma mark - FIRRemoteConfig
/// Firebase Remote Config class. The class method `remoteConfig()` can be used
/// to fetch, activate and read config results and set default config results on the default
/// Remote Config instance.
NS_SWIFT_NAME(RemoteConfig)
@interface FIRRemoteConfig : NSObject <NSFastEnumeration>
/// Last successful fetch completion time.
@property(nonatomic, readonly, strong, nullable) NSDate *lastFetchTime;
/// Last fetch status. The status can be any enumerated value from `RemoteConfigFetchStatus`.
@property(nonatomic, readonly, assign) FIRRemoteConfigFetchStatus lastFetchStatus;
/// Config settings are custom settings.
@property(nonatomic, readwrite, strong, nonnull) FIRRemoteConfigSettings *configSettings;
/// Returns the `RemoteConfig` instance configured for the default Firebase app. This singleton
/// object contains the complete set of Remote Config parameter values available to the app,
/// including the Active Config and Default Config. This object also caches values fetched from the
/// Remote Config server until they are copied to the Active Config by calling `activate()`. When
/// you fetch values from the Remote Config server using the default Firebase app, you should use
/// this class method to create and reuse a shared instance of `RemoteConfig`.
+ (nonnull FIRRemoteConfig *)remoteConfig NS_SWIFT_NAME(remoteConfig());
/// Returns the `RemoteConfig` instance for your (non-default) Firebase appID. Note that Firebase
/// analytics does not work for non-default app instances. This singleton object contains the
/// complete set of Remote Config parameter values available to the app, including the Active Config
/// and Default Config. This object also caches values fetched from the Remote Config Server until
/// they are copied to the Active Config by calling `activate())`. When you fetch values
/// from the Remote Config Server using the non-default Firebase app, you should use this
/// class method to create and reuse shared instance of `RemoteConfig`.
+ (nonnull FIRRemoteConfig *)remoteConfigWithApp:(nonnull FIRApp *)app
NS_SWIFT_NAME(remoteConfig(app:));
/// Unavailable. Use +remoteConfig instead.
- (nonnull instancetype)init __attribute__((unavailable("Use +remoteConfig instead.")));
/// Ensures initialization is complete and clients can begin querying for Remote Config values.
/// @param completionHandler Initialization complete callback with error parameter.
- (void)ensureInitializedWithCompletionHandler:
(void (^_Nonnull)(NSError *_Nullable initializationError))completionHandler;
#pragma mark - Fetch
/// Fetches Remote Config data with a callback. Call `activate()` to make fetched data
/// available to your app.
///
/// Note: This method uses a Firebase Installations token to identify the app instance, and once
/// it's called, it periodically sends data to the Firebase backend. (see
/// `Installations.authToken(completion:)`).
/// To stop the periodic sync, call `Installations.delete(completion:)`
/// and avoid calling this method again.
///
/// @param completionHandler Fetch operation callback with status and error parameters.
- (void)fetchWithCompletionHandler:(void (^_Nullable)(FIRRemoteConfigFetchStatus status,
NSError *_Nullable error))completionHandler;
/// Fetches Remote Config data and sets a duration that specifies how long config data lasts.
/// Call `activateWithCompletion:` to make fetched data available to your app.
///
/// Note: This method uses a Firebase Installations token to identify the app instance, and once
/// it's called, it periodically sends data to the Firebase backend. (see
/// `Installations.authToken(completion:)`).
/// To stop the periodic sync, call `Installations.delete(completion:)`
/// and avoid calling this method again.
///
/// @param expirationDuration Override the (default or optionally set `minimumFetchInterval`
/// property in RemoteConfigSettings) `minimumFetchInterval` for only the current request, in
/// seconds. Setting a value of 0 seconds will force a fetch to the backend.
/// @param completionHandler Fetch operation callback with status and error parameters.
- (void)fetchWithExpirationDuration:(NSTimeInterval)expirationDuration
completionHandler:(void (^_Nullable)(FIRRemoteConfigFetchStatus status,
NSError *_Nullable error))completionHandler;
/// Fetches Remote Config data and if successful, activates fetched data. Optional completion
/// handler callback is invoked after the attempted activation of data, if the fetch call succeeded.
///
/// Note: This method uses a Firebase Installations token to identify the app instance, and once
/// it's called, it periodically sends data to the Firebase backend. (see
/// `Installations.authToken(completion:)`).
/// To stop the periodic sync, call `Installations.delete(completion:)`
/// and avoid calling this method again.
///
/// @param completionHandler Fetch operation callback with status and error parameters.
- (void)fetchAndActivateWithCompletionHandler:
(void (^_Nullable)(FIRRemoteConfigFetchAndActivateStatus status,
NSError *_Nullable error))completionHandler;
#pragma mark - Apply
/// Applies Fetched Config data to the Active Config, causing updates to the behavior and appearance
/// of the app to take effect (depending on how config data is used in the app).
/// @param completion Activate operation callback with changed and error parameters.
- (void)activateWithCompletion:(void (^_Nullable)(BOOL changed,
NSError *_Nullable error))completion;
#pragma mark - Get Config
/// Enables access to configuration values by using object subscripting syntax.
/// For example:
/// let config = RemoteConfig.remoteConfig()
/// let value = config["yourKey"]
/// let boolValue = value.boolValue
/// let number = config["yourKey"].numberValue
- (nonnull FIRRemoteConfigValue *)objectForKeyedSubscript:(nonnull NSString *)key;
/// Gets the config value.
/// @param key Config key.
- (nonnull FIRRemoteConfigValue *)configValueForKey:(nullable NSString *)key;
/// Gets the config value of a given source from the default namespace.
/// @param key Config key.
/// @param source Config value source.
- (nonnull FIRRemoteConfigValue *)configValueForKey:(nullable NSString *)key
source:(FIRRemoteConfigSource)source;
/// Gets all the parameter keys of a given source from the default namespace.
///
/// @param source The config data source.
/// @return An array of keys under the given source.
- (nonnull NSArray<NSString *> *)allKeysFromSource:(FIRRemoteConfigSource)source;
/// Returns the set of parameter keys that start with the given prefix, from the default namespace
/// in the active config.
///
/// @param prefix The key prefix to look for. If prefix is nil or empty, returns all the
/// keys.
/// @return The set of parameter keys that start with the specified prefix.
- (nonnull NSSet<NSString *> *)keysWithPrefix:(nullable NSString *)prefix;
#pragma mark - Defaults
/// Sets config defaults for parameter keys and values in the default namespace config.
/// @param defaults A dictionary mapping a NSString * key to a NSObject * value.
- (void)setDefaults:(nullable NSDictionary<NSString *, NSObject *> *)defaults;
/// Sets default configs from plist for default namespace.
///
/// @param fileName The plist file name, with no file name extension. For example, if the plist file
/// is named `defaultSamples.plist`:
/// `RemoteConfig.remoteConfig().setDefaults(fromPlist: "defaultSamples")`
- (void)setDefaultsFromPlistFileName:(nullable NSString *)fileName
NS_SWIFT_NAME(setDefaults(fromPlist:));
/// Returns the default value of a given key from the default config.
///
/// @param key The parameter key of default config.
/// @return Returns the default value of the specified key. Returns
/// nil if the key doesn't exist in the default config.
- (nullable FIRRemoteConfigValue *)defaultValueForKey:(nullable NSString *)key;
#pragma mark - Real-time Config Updates
/// Completion handler invoked by `addOnConfigUpdateListener` when there is an update to
/// the config from the backend.
///
/// @param configUpdate An instance of `FIRRemoteConfigUpdate` that contains information on which
/// key's values have changed.
/// @param error Error message on failure.
typedef void (^FIRRemoteConfigUpdateCompletion)(FIRRemoteConfigUpdate *_Nullable configUpdate,
NSError *_Nullable error)
NS_SWIFT_UNAVAILABLE("Use Swift's closure syntax instead.");
/// Start listening for real-time config updates from the Remote Config backend and automatically
/// fetch updates when they're available.
///
/// If a connection to the Remote Config backend is not already open, calling this method will
/// open it. Multiple listeners can be added by calling this method again, but subsequent calls
/// re-use the same connection to the backend.
///
/// Note: Real-time Remote Config requires the Firebase Remote Config Realtime API. See Get started
/// with Firebase Remote Config at https://firebase.google.com/docs/remote-config/get-started for
/// more information.
///
/// @param listener The configured listener that is called for every config update.
/// @return Returns a registration representing the listener. The registration contains
/// a remove method, which can be used to stop receiving updates for the provided listener.
- (FIRConfigUpdateListenerRegistration *_Nonnull)addOnConfigUpdateListener:
(FIRRemoteConfigUpdateCompletion _Nonnull)listener
NS_SWIFT_NAME(addOnConfigUpdateListener(remoteConfigUpdateCompletion:));
@end

View File

@ -0,0 +1,17 @@
/*
* 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 "FIRRemoteConfig.h"

View File

@ -0,0 +1,72 @@
/*
* 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>
#define RCN_SEC_PER_MIN 60
#define RCN_MSEC_PER_SEC 1000
/// Key prefix applied to all the packages (bundle IDs) in internal metadata.
static NSString *const RCNInternalMetadataAllPackagesPrefix = @"all_packages";
/// HTTP connection default timeout in seconds.
static const NSTimeInterval RCNHTTPDefaultConnectionTimeout = 60;
/// Default duration of how long config data lasts to stay fresh.
static const NSTimeInterval RCNDefaultMinimumFetchInterval = 43200;
/// Label for serial queue for read/write lock on ivars.
static const char *RCNRemoteConfigQueueLabel = "com.google.GoogleConfigService.FIRRemoteConfig";
/// Constants for key names in the fetch response.
/// Key that includes an array of template entries.
static NSString *const RCNFetchResponseKeyEntries = @"entries";
/// Key that includes data for experiment descriptions in ABT.
static NSString *const RCNFetchResponseKeyExperimentDescriptions = @"experimentDescriptions";
/// Key that includes data for Personalization metadata.
static NSString *const RCNFetchResponseKeyPersonalizationMetadata = @"personalizationMetadata";
/// Key that includes data for Rollout metadata.
static NSString *const RCNFetchResponseKeyRolloutMetadata = @"rolloutMetadata";
/// Key that indicates rollout id in Rollout metadata.
static NSString *const RCNFetchResponseKeyRolloutID = @"rolloutId";
/// Key that indicates variant id in Rollout metadata.
static NSString *const RCNFetchResponseKeyVariantID = @"variantId";
/// Key that indicates affected parameter keys in Rollout Metadata.
static NSString *const RCNFetchResponseKeyAffectedParameterKeys = @"affectedParameterKeys";
/// Error key.
static NSString *const RCNFetchResponseKeyError = @"error";
/// Error code.
static NSString *const RCNFetchResponseKeyErrorCode = @"code";
/// Error status.
static NSString *const RCNFetchResponseKeyErrorStatus = @"status";
/// Error message.
static NSString *const RCNFetchResponseKeyErrorMessage = @"message";
/// The current state of the backend template.
static NSString *const RCNFetchResponseKeyState = @"state";
/// Default state (when not set).
static NSString *const RCNFetchResponseKeyStateUnspecified = @"INSTANCE_STATE_UNSPECIFIED";
/// Config key/value map and/or ABT experiment list differs from last fetch.
/// TODO: Migrate to the new HTTP error codes once available in the backend. b/117182055
static NSString *const RCNFetchResponseKeyStateUpdate = @"UPDATE";
/// No template fetched.
static NSString *const RCNFetchResponseKeyStateNoTemplate = @"NO_TEMPLATE";
/// Config key/value map and ABT experiment list both match last fetch.
static NSString *const RCNFetchResponseKeyStateNoChange = @"NO_CHANGE";
/// Template found, but evaluates to empty (e.g. all keys omitted).
static NSString *const RCNFetchResponseKeyStateEmptyConfig = @"EMPTY_CONFIG";
/// Fetched Template Version key
static NSString *const RCNFetchResponseKeyTemplateVersion = @"templateVersion";
/// Active Template Version key
static NSString *const RCNActiveKeyTemplateVersion = @"activeTemplateVersion";

View File

@ -0,0 +1,76 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
#import "FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h"
typedef NS_ENUM(NSInteger, RCNDBSource) {
RCNDBSourceActive,
RCNDBSourceDefault,
RCNDBSourceFetched,
};
@class RCNConfigDBManager;
/// This class handles all the config content that is fetched from the server, cached in local
/// config or persisted in database.
@interface RCNConfigContent : NSObject
/// Shared Singleton Instance
+ (instancetype)sharedInstance;
/// Fetched config (aka pending config) data that is latest data from server that might or might
/// not be applied.
@property(nonatomic, readonly, copy) NSDictionary *fetchedConfig;
/// Active config that is available to external users;
@property(nonatomic, readonly, copy) NSDictionary *activeConfig;
/// Local default config that is provided by external users;
@property(nonatomic, readonly, copy) NSDictionary *defaultConfig;
/// Active Rollout metadata that is currently used.
@property(nonatomic, readonly, copy) NSArray<NSDictionary *> *activeRolloutMetadata;
- (instancetype)init NS_UNAVAILABLE;
/// Designated initializer;
- (instancetype)initWithDBManager:(RCNConfigDBManager *)DBManager NS_DESIGNATED_INITIALIZER;
/// Returns true if initialization succeeded.
- (BOOL)initializationSuccessful;
/// Update config content from fetch response in JSON format.
- (void)updateConfigContentWithResponse:(NSDictionary *)response
forNamespace:(NSString *)FIRNamespace;
/// Copy from a given dictionary to one of the data source.
/// @param fromDictionary The data to copy from.
/// @param source The data source to copy to(pending/active/default).
- (void)copyFromDictionary:(NSDictionary *)fromDictionary
toSource:(RCNDBSource)source
forNamespace:(NSString *)FIRNamespace;
/// Sets the fetched Personalization metadata to active.
- (void)activatePersonalization;
/// Gets the active config and Personalization metadata.
- (NSDictionary *)getConfigAndMetadataForNamespace:(NSString *)FIRNamespace;
/// Sets the fetched rollout metadata to active with a success completion handler.
- (void)activateRolloutMetadata:(void (^)(BOOL success))completionHandler;
/// Returns the updated parameters between fetched and active config.
- (FIRRemoteConfigUpdate *)getConfigUpdateForNamespace:(NSString *)FIRNamespace;
@end

View File

@ -0,0 +1,526 @@
/*
* 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 "FirebaseRemoteConfig/Sources/RCNConfigContent.h"
#import "FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h"
#import "FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigDefines.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h"
#import "FirebaseCore/Extension/FirebaseCoreInternal.h"
@implementation RCNConfigContent {
/// Active config data that is currently used.
NSMutableDictionary *_activeConfig;
/// Pending config (aka Fetched config) data that is latest data from server that might or might
/// not be applied.
NSMutableDictionary *_fetchedConfig;
/// Default config provided by user.
NSMutableDictionary *_defaultConfig;
/// Active Personalization metadata that is currently used.
NSDictionary *_activePersonalization;
/// Pending Personalization metadata that is latest data from server that might or might not be
/// applied.
NSDictionary *_fetchedPersonalization;
/// Active Rollout metadata that is currently used.
NSArray<NSDictionary *> *_activeRolloutMetadata;
/// Pending Rollout metadata that is latest data from server that might or might not be applied.
NSArray<NSDictionary *> *_fetchedRolloutMetadata;
/// DBManager
RCNConfigDBManager *_DBManager;
/// Current bundle identifier;
NSString *_bundleIdentifier;
/// Blocks all config reads until we have read from the database. This only
/// potentially blocks on the first read. Should be a no-wait for all subsequent reads once we
/// have data read into memory from the database.
dispatch_group_t _dispatch_group;
/// Boolean indicating if initial DB load of fetched,active and default config has succeeded.
BOOL _isConfigLoadFromDBCompleted;
/// Boolean indicating that the load from database has initiated at least once.
BOOL _isDatabaseLoadAlreadyInitiated;
}
/// Default timeout when waiting to read data from database.
const NSTimeInterval kDatabaseLoadTimeoutSecs = 30.0;
/// Singleton instance of RCNConfigContent.
+ (instancetype)sharedInstance {
static dispatch_once_t onceToken;
static RCNConfigContent *sharedInstance;
dispatch_once(&onceToken, ^{
sharedInstance =
[[RCNConfigContent alloc] initWithDBManager:[RCNConfigDBManager sharedInstance]];
});
return sharedInstance;
}
- (instancetype)init {
NSAssert(NO, @"Invalid initializer.");
return nil;
}
/// Designated initializer
- (instancetype)initWithDBManager:(RCNConfigDBManager *)DBManager {
self = [super init];
if (self) {
_activeConfig = [[NSMutableDictionary alloc] init];
_fetchedConfig = [[NSMutableDictionary alloc] init];
_defaultConfig = [[NSMutableDictionary alloc] init];
_activePersonalization = [[NSDictionary alloc] init];
_fetchedPersonalization = [[NSDictionary alloc] init];
_activeRolloutMetadata = [[NSArray alloc] init];
_fetchedRolloutMetadata = [[NSArray alloc] init];
_bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
if (!_bundleIdentifier) {
FIRLogNotice(kFIRLoggerRemoteConfig, @"I-RCN000038",
@"Main bundle identifier is missing. Remote Config might not work properly.");
_bundleIdentifier = @"";
}
_DBManager = DBManager;
// Waits for both config and Personalization data to load.
_dispatch_group = dispatch_group_create();
[self loadConfigFromMainTable];
}
return self;
}
// Blocking call that returns true/false once database load completes / times out.
// @return Initialization status.
- (BOOL)initializationSuccessful {
RCN_MUST_NOT_BE_MAIN_THREAD();
BOOL isDatabaseLoadSuccessful = [self checkAndWaitForInitialDatabaseLoad];
return isDatabaseLoadSuccessful;
}
#pragma mark - database
/// This method is only meant to be called at init time. The underlying logic will need to be
/// reevaluated if the assumption changes at a later time.
- (void)loadConfigFromMainTable {
if (!_DBManager) {
return;
}
NSAssert(!_isDatabaseLoadAlreadyInitiated, @"Database load has already been initiated");
_isDatabaseLoadAlreadyInitiated = true;
dispatch_group_enter(_dispatch_group);
[_DBManager loadMainWithBundleIdentifier:_bundleIdentifier
completionHandler:^(
BOOL success, NSDictionary *fetchedConfig, NSDictionary *activeConfig,
NSDictionary *defaultConfig, NSDictionary *rolloutMetadata) {
self->_fetchedConfig = [fetchedConfig mutableCopy];
self->_activeConfig = [activeConfig mutableCopy];
self->_defaultConfig = [defaultConfig mutableCopy];
self->_fetchedRolloutMetadata =
[rolloutMetadata[@RCNRolloutTableKeyFetchedMetadata] copy];
self->_activeRolloutMetadata =
[rolloutMetadata[@RCNRolloutTableKeyActiveMetadata] copy];
dispatch_group_leave(self->_dispatch_group);
}];
// TODO(karenzeng): Refactor personalization to be returned in loadMainWithBundleIdentifier above
dispatch_group_enter(_dispatch_group);
[_DBManager
loadPersonalizationWithCompletionHandler:^(
BOOL success, NSDictionary *fetchedPersonalization, NSDictionary *activePersonalization,
NSDictionary *defaultConfig, NSDictionary *rolloutMetadata) {
self->_fetchedPersonalization = [fetchedPersonalization copy];
self->_activePersonalization = [activePersonalization copy];
dispatch_group_leave(self->_dispatch_group);
}];
}
/// Update the current config result to main table.
/// @param values Values in a row to write to the table.
/// @param source The source the config data is coming from. It determines which table to write to.
- (void)updateMainTableWithValues:(NSArray *)values fromSource:(RCNDBSource)source {
[_DBManager insertMainTableWithValues:values fromSource:source completionHandler:nil];
}
#pragma mark - update
/// This function is for copying dictionary when user set up a default config or when user clicks
/// activate. For now the DBSource can only be Active or Default.
- (void)copyFromDictionary:(NSDictionary *)fromDict
toSource:(RCNDBSource)DBSource
forNamespace:(NSString *)FIRNamespace {
// Make sure database load has completed.
[self checkAndWaitForInitialDatabaseLoad];
NSMutableDictionary *toDict;
if (!fromDict) {
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000007",
@"The source dictionary to copy from does not exist.");
return;
}
FIRRemoteConfigSource source = FIRRemoteConfigSourceRemote;
switch (DBSource) {
case RCNDBSourceDefault:
toDict = _defaultConfig;
source = FIRRemoteConfigSourceDefault;
break;
case RCNDBSourceFetched:
FIRLogWarning(kFIRLoggerRemoteConfig, @"I-RCN000008",
@"This shouldn't happen. Destination dictionary should never be pending type.");
return;
case RCNDBSourceActive:
toDict = _activeConfig;
source = FIRRemoteConfigSourceRemote;
[toDict removeObjectForKey:FIRNamespace];
break;
default:
toDict = _activeConfig;
source = FIRRemoteConfigSourceRemote;
[toDict removeObjectForKey:FIRNamespace];
break;
}
// Completely wipe out DB first.
[_DBManager deleteRecordFromMainTableWithNamespace:FIRNamespace
bundleIdentifier:_bundleIdentifier
fromSource:DBSource];
toDict[FIRNamespace] = [[NSMutableDictionary alloc] init];
NSDictionary *config = fromDict[FIRNamespace];
for (NSString *key in config) {
if (DBSource == FIRRemoteConfigSourceDefault) {
NSObject *value = config[key];
NSData *valueData;
if ([value isKindOfClass:[NSData class]]) {
valueData = (NSData *)value;
} else if ([value isKindOfClass:[NSString class]]) {
valueData = [(NSString *)value dataUsingEncoding:NSUTF8StringEncoding];
} else if ([value isKindOfClass:[NSNumber class]]) {
NSString *strValue = [(NSNumber *)value stringValue];
valueData = [(NSString *)strValue dataUsingEncoding:NSUTF8StringEncoding];
} else if ([value isKindOfClass:[NSDate class]]) {
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
NSString *strValue = [dateFormatter stringFromDate:(NSDate *)value];
valueData = [(NSString *)strValue dataUsingEncoding:NSUTF8StringEncoding];
} else if ([value isKindOfClass:[NSArray class]]) {
NSError *error;
valueData = [NSJSONSerialization dataWithJSONObject:value options:0 error:&error];
if (error) {
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000076", @"Invalid array value for key '%@'",
key);
}
} else if ([value isKindOfClass:[NSDictionary class]]) {
NSError *error;
valueData = [NSJSONSerialization dataWithJSONObject:value options:0 error:&error];
if (error) {
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000077",
@"Invalid dictionary value for key '%@'", key);
}
} else {
continue;
}
toDict[FIRNamespace][key] = [[FIRRemoteConfigValue alloc] initWithData:valueData
source:source];
NSArray *values = @[ _bundleIdentifier, FIRNamespace, key, valueData ];
[self updateMainTableWithValues:values fromSource:DBSource];
} else {
FIRRemoteConfigValue *value = config[key];
toDict[FIRNamespace][key] = [[FIRRemoteConfigValue alloc] initWithData:value.dataValue
source:source];
NSArray *values = @[ _bundleIdentifier, FIRNamespace, key, value.dataValue ];
[self updateMainTableWithValues:values fromSource:DBSource];
}
}
}
- (void)updateConfigContentWithResponse:(NSDictionary *)response
forNamespace:(NSString *)currentNamespace {
// Make sure database load has completed.
[self checkAndWaitForInitialDatabaseLoad];
NSString *state = response[RCNFetchResponseKeyState];
if (!state) {
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000049", @"State field in fetch response is nil.");
return;
}
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000059",
@"Updating config content from Response for namespace:%@ with state: %@",
currentNamespace, response[RCNFetchResponseKeyState]);
if ([state isEqualToString:RCNFetchResponseKeyStateNoChange]) {
[self handleNoChangeStateForConfigNamespace:currentNamespace];
return;
}
/// Handle empty config state
if ([state isEqualToString:RCNFetchResponseKeyStateEmptyConfig]) {
[self handleEmptyConfigStateForConfigNamespace:currentNamespace];
return;
}
/// Handle no template state.
if ([state isEqualToString:RCNFetchResponseKeyStateNoTemplate]) {
[self handleNoTemplateStateForConfigNamespace:currentNamespace];
return;
}
/// Handle update state
if ([state isEqualToString:RCNFetchResponseKeyStateUpdate]) {
[self handleUpdateStateForConfigNamespace:currentNamespace
withEntries:response[RCNFetchResponseKeyEntries]];
[self handleUpdatePersonalization:response[RCNFetchResponseKeyPersonalizationMetadata]];
[self handleUpdateRolloutFetchedMetadata:response[RCNFetchResponseKeyRolloutMetadata]];
return;
}
}
- (void)activatePersonalization {
_activePersonalization = _fetchedPersonalization;
[_DBManager insertOrUpdatePersonalizationConfig:_activePersonalization
fromSource:RCNDBSourceActive];
}
- (void)activateRolloutMetadata:(void (^)(BOOL success))completionHandler {
_activeRolloutMetadata = _fetchedRolloutMetadata;
[_DBManager insertOrUpdateRolloutTableWithKey:@RCNRolloutTableKeyActiveMetadata
value:_activeRolloutMetadata
completionHandler:^(BOOL success, NSDictionary *result) {
completionHandler(success);
}];
}
#pragma mark State handling
- (void)handleNoChangeStateForConfigNamespace:(NSString *)currentNamespace {
if (!_fetchedConfig[currentNamespace]) {
_fetchedConfig[currentNamespace] = [[NSMutableDictionary alloc] init];
}
}
- (void)handleEmptyConfigStateForConfigNamespace:(NSString *)currentNamespace {
if (_fetchedConfig[currentNamespace]) {
[_fetchedConfig[currentNamespace] removeAllObjects];
} else {
// If namespace has empty status and it doesn't exist in _fetchedConfig, we will
// still add an entry for that namespace. Even if it will not be persisted in database.
// TODO: Add generics for all collection types.
_fetchedConfig[currentNamespace] = [[NSMutableDictionary alloc] init];
}
[_DBManager deleteRecordFromMainTableWithNamespace:currentNamespace
bundleIdentifier:_bundleIdentifier
fromSource:RCNDBSourceFetched];
}
- (void)handleNoTemplateStateForConfigNamespace:(NSString *)currentNamespace {
// Remove the namespace.
[_fetchedConfig removeObjectForKey:currentNamespace];
[_DBManager deleteRecordFromMainTableWithNamespace:currentNamespace
bundleIdentifier:_bundleIdentifier
fromSource:RCNDBSourceFetched];
}
- (void)handleUpdateStateForConfigNamespace:(NSString *)currentNamespace
withEntries:(NSDictionary *)entries {
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000058", @"Update config in DB for namespace:%@",
currentNamespace);
// Clear before updating
[_DBManager deleteRecordFromMainTableWithNamespace:currentNamespace
bundleIdentifier:_bundleIdentifier
fromSource:RCNDBSourceFetched];
if ([_fetchedConfig objectForKey:currentNamespace]) {
[_fetchedConfig[currentNamespace] removeAllObjects];
} else {
_fetchedConfig[currentNamespace] = [[NSMutableDictionary alloc] init];
}
// Store the fetched config values.
for (NSString *key in entries) {
NSData *valueData = [entries[key] dataUsingEncoding:NSUTF8StringEncoding];
if (!valueData) {
continue;
}
_fetchedConfig[currentNamespace][key] =
[[FIRRemoteConfigValue alloc] initWithData:valueData source:FIRRemoteConfigSourceRemote];
NSArray *values = @[ _bundleIdentifier, currentNamespace, key, valueData ];
[self updateMainTableWithValues:values fromSource:RCNDBSourceFetched];
}
}
- (void)handleUpdatePersonalization:(NSDictionary *)metadata {
if (!metadata) {
return;
}
_fetchedPersonalization = metadata;
[_DBManager insertOrUpdatePersonalizationConfig:metadata fromSource:RCNDBSourceFetched];
}
- (void)handleUpdateRolloutFetchedMetadata:(NSArray<NSDictionary *> *)metadata {
if (!metadata) {
metadata = [[NSArray alloc] init];
}
_fetchedRolloutMetadata = metadata;
[_DBManager insertOrUpdateRolloutTableWithKey:@RCNRolloutTableKeyFetchedMetadata
value:metadata
completionHandler:nil];
}
#pragma mark - getter/setter
- (NSDictionary *)fetchedConfig {
/// If this is the first time reading the fetchedConfig, we might still be reading it from the
/// database.
[self checkAndWaitForInitialDatabaseLoad];
return _fetchedConfig;
}
- (NSDictionary *)activeConfig {
/// If this is the first time reading the activeConfig, we might still be reading it from the
/// database.
[self checkAndWaitForInitialDatabaseLoad];
return _activeConfig;
}
- (NSDictionary *)defaultConfig {
/// If this is the first time reading the fetchedConfig, we might still be reading it from the
/// database.
[self checkAndWaitForInitialDatabaseLoad];
return _defaultConfig;
}
- (NSDictionary *)activePersonalization {
[self checkAndWaitForInitialDatabaseLoad];
return _activePersonalization;
}
- (NSArray<NSDictionary *> *)activeRolloutMetadata {
[self checkAndWaitForInitialDatabaseLoad];
return _activeRolloutMetadata;
}
- (NSDictionary *)getConfigAndMetadataForNamespace:(NSString *)FIRNamespace {
/// If this is the first time reading the active metadata, we might still be reading it from the
/// database.
[self checkAndWaitForInitialDatabaseLoad];
return @{
RCNFetchResponseKeyEntries : _activeConfig[FIRNamespace],
RCNFetchResponseKeyPersonalizationMetadata : _activePersonalization
};
}
/// We load the database async at init time. Block all further calls to active/fetched/default
/// configs until load is done.
/// @return Database load completion status.
- (BOOL)checkAndWaitForInitialDatabaseLoad {
/// Wait until load is done. This should be a no-op for subsequent calls.
if (!_isConfigLoadFromDBCompleted) {
intptr_t isErrorOrTimeout = dispatch_group_wait(
_dispatch_group,
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kDatabaseLoadTimeoutSecs * NSEC_PER_SEC)));
if (isErrorOrTimeout) {
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000048",
@"Timed out waiting for fetched config to be loaded from DB");
return false;
}
_isConfigLoadFromDBCompleted = true;
}
return true;
}
// Compare fetched config with active config and output what has changed
- (FIRRemoteConfigUpdate *)getConfigUpdateForNamespace:(NSString *)FIRNamespace {
// TODO: handle diff in experiment metadata
FIRRemoteConfigUpdate *configUpdate;
NSMutableSet<NSString *> *updatedKeys = [[NSMutableSet alloc] init];
NSDictionary *fetchedConfig =
_fetchedConfig[FIRNamespace] ? _fetchedConfig[FIRNamespace] : [[NSDictionary alloc] init];
NSDictionary *activeConfig =
_activeConfig[FIRNamespace] ? _activeConfig[FIRNamespace] : [[NSDictionary alloc] init];
NSDictionary *fetchedP13n = _fetchedPersonalization;
NSDictionary *activeP13n = _activePersonalization;
NSArray<NSDictionary *> *fetchedRolloutMetadata = _fetchedRolloutMetadata;
NSArray<NSDictionary *> *activeRolloutMetadata = _activeRolloutMetadata;
// add new/updated params
for (NSString *key in [fetchedConfig allKeys]) {
if (activeConfig[key] == nil ||
![[activeConfig[key] stringValue] isEqualToString:[fetchedConfig[key] stringValue]]) {
[updatedKeys addObject:key];
}
}
// add deleted params
for (NSString *key in [activeConfig allKeys]) {
if (fetchedConfig[key] == nil) {
[updatedKeys addObject:key];
}
}
// add params with new/updated p13n metadata
for (NSString *key in [fetchedP13n allKeys]) {
if (activeP13n[key] == nil || ![activeP13n[key] isEqualToDictionary:fetchedP13n[key]]) {
[updatedKeys addObject:key];
}
}
// add params with deleted p13n metadata
for (NSString *key in [activeP13n allKeys]) {
if (fetchedP13n[key] == nil) {
[updatedKeys addObject:key];
}
}
NSDictionary<NSString *, NSDictionary *> *fetchedRollouts =
[self getParameterKeyToRolloutMetadata:fetchedRolloutMetadata];
NSDictionary<NSString *, NSDictionary *> *activeRollouts =
[self getParameterKeyToRolloutMetadata:activeRolloutMetadata];
// add params with new/updated rollout metadata
for (NSString *key in [fetchedRollouts allKeys]) {
if (activeRollouts[key] == nil ||
![activeRollouts[key] isEqualToDictionary:fetchedRollouts[key]]) {
[updatedKeys addObject:key];
}
}
// add params with deleted rollout metadata
for (NSString *key in [activeRollouts allKeys]) {
if (fetchedRollouts[key] == nil) {
[updatedKeys addObject:key];
}
}
configUpdate = [[FIRRemoteConfigUpdate alloc] initWithUpdatedKeys:updatedKeys];
return configUpdate;
}
- (NSDictionary<NSString *, NSDictionary *> *)getParameterKeyToRolloutMetadata:
(NSArray<NSDictionary *> *)rolloutMetadata {
NSMutableDictionary<NSString *, NSMutableDictionary *> *result =
[[NSMutableDictionary alloc] init];
for (NSDictionary *metadata in rolloutMetadata) {
NSString *rolloutId = metadata[RCNFetchResponseKeyRolloutID];
NSString *variantId = metadata[RCNFetchResponseKeyVariantID];
NSArray<NSString *> *affectedKeys = metadata[RCNFetchResponseKeyAffectedParameterKeys];
if (rolloutId && variantId && affectedKeys) {
for (NSString *key in affectedKeys) {
if (result[key]) {
NSMutableDictionary *rolloutIdToVariantId = result[key];
[rolloutIdToVariantId setValue:variantId forKey:rolloutId];
} else {
NSMutableDictionary *rolloutIdToVariantId = [@{rolloutId : variantId} mutableCopy];
[result setValue:rolloutIdToVariantId forKey:key];
}
}
}
}
return [result copy];
}
@end

View File

@ -0,0 +1,142 @@
/*
* Copyright 2019 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
#import "FirebaseRemoteConfig/Sources/RCNConfigContent.h"
typedef NS_ENUM(NSInteger, RCNUpdateOption) {
RCNUpdateOptionApplyTime,
RCNUpdateOptionDefaultTime,
RCNUpdateOptionFetchStatus,
};
/// Column names in metadata table
static NSString *const RCNKeyBundleIdentifier = @"bundle_identifier";
static NSString *const RCNKeyNamespace = @"namespace";
static NSString *const RCNKeyFetchTime = @"fetch_time";
static NSString *const RCNKeyDigestPerNamespace = @"digest_per_ns";
static NSString *const RCNKeyDeviceContext = @"device_context";
static NSString *const RCNKeyAppContext = @"app_context";
static NSString *const RCNKeySuccessFetchTime = @"success_fetch_time";
static NSString *const RCNKeyFailureFetchTime = @"failure_fetch_time";
static NSString *const RCNKeyLastFetchStatus = @"last_fetch_status";
static NSString *const RCNKeyLastFetchError = @"last_fetch_error";
static NSString *const RCNKeyLastApplyTime = @"last_apply_time";
static NSString *const RCNKeyLastSetDefaultsTime = @"last_set_defaults_time";
/// Persist config data in sqlite database on device. Managing data read/write from/to database.
@interface RCNConfigDBManager : NSObject
/// Shared Singleton Instance
+ (instancetype)sharedInstance;
/// Database Operation Completion callback.
/// @param success Decide whether the DB operation succeeds.
/// @param result Return operation result data.
typedef void (^RCNDBCompletion)(BOOL success, NSDictionary *result);
/// Database Load Operation Completion callback.
/// @param success Decide whether the DB operation succeeds.
/// @param fetchedConfig Return fetchedConfig loaded from DB
/// @param activeConfig Return activeConfig loaded from DB
/// @param defaultConfig Return defaultConfig loaded from DB
/// @param rolloutMetadata Return fetched and active RolloutMetadata loaded from DB
typedef void (^RCNDBLoadCompletion)(BOOL success,
NSDictionary *fetchedConfig,
NSDictionary *activeConfig,
NSDictionary *defaultConfig,
NSDictionary *rolloutMetadata);
/// Returns the current version of the Remote Config database.
+ (NSString *)remoteConfigPathForDatabase;
/// Load config content from main table to cached memory during app start.
- (void)loadMainWithBundleIdentifier:(NSString *)bundleIdentifier
completionHandler:(RCNDBLoadCompletion)handler;
/// Load config settings for a given namespace from metadata table to cached memory during app
/// start. Config settings include success/failure fetch times, device contenxt, app context, etc.
- (NSDictionary *)loadMetadataWithBundleIdentifier:(NSString *)bundleIdentifier
namespace:(NSString *)namespace;
/// Load internal metadata from internal metadata table, such as customized HTTP connection/read
/// timeout, throttling time interval and number limit of throttling, etc.
/// This call needs to be blocking to ensure throttling works during apps starts.
- (NSDictionary *)loadInternalMetadataTable;
/// Load experiment from experiment table.
/// @param handler The callback when reading from DB is complete.
- (void)loadExperimentWithCompletionHandler:(RCNDBCompletion)handler;
/// Load Personalization from table.
/// @param handler The callback when reading from DB is complete.
- (void)loadPersonalizationWithCompletionHandler:(RCNDBLoadCompletion)handler;
/// Insert a record in metadata table.
/// @param columnNameToValue The column name and its value to be inserted in metadata table.
/// @param handler The callback.
- (void)insertMetadataTableWithValues:(NSDictionary *)columnNameToValue
completionHandler:(RCNDBCompletion)handler;
/// Insert a record in main table.
/// @param values Values to be inserted.
- (void)insertMainTableWithValues:(NSArray *)values
fromSource:(RCNDBSource)source
completionHandler:(RCNDBCompletion)handler;
/// Insert a record in internal metadata table.
/// @param values Values to be inserted.
- (void)insertInternalMetadataTableWithValues:(NSArray *)values
completionHandler:(RCNDBCompletion)handler;
/// Insert experiment data in experiment table.
/// @param key The key of experiment data belongs to, which are defined in
/// RCNConfigDefines.h.
/// @param value The value that experiment.
/// @param handler The callback.
- (void)insertExperimentTableWithKey:(NSString *)key
value:(NSData *)value
completionHandler:(RCNDBCompletion)handler;
- (void)updateMetadataWithOption:(RCNUpdateOption)option
namespace:(NSString *)namespace
values:(NSArray *)values
completionHandler:(RCNDBCompletion)handler;
/// Insert or update the data in Personalization config.
- (BOOL)insertOrUpdatePersonalizationConfig:(NSDictionary *)metadata fromSource:(RCNDBSource)source;
/// Insert rollout metadata in rollout table.
/// @param key Key indicating whether rollout metadata is fetched or active and defined in
/// RCNConfigDefines.h.
/// @param metadataList The metadata info for each rollout entry .
/// @param handler The callback.
- (void)insertOrUpdateRolloutTableWithKey:(NSString *)key
value:(NSArray<NSDictionary *> *)metadataList
completionHandler:(RCNDBCompletion)handler;
/// Clear the record of given namespace and package name
/// before updating the table.
- (void)deleteRecordFromMainTableWithNamespace:(NSString *)namespace_p
bundleIdentifier:(NSString *)bundleIdentifier
fromSource:(RCNDBSource)source;
/// Remove all the records of given package name and namespace from metadata/internal metadata DB
/// before updating new values from response.
- (void)deleteRecordWithBundleIdentifier:(NSString *)bundlerIdentifier
namespace:(NSString *)namespace
isInternalDB:(BOOL)isInternalDB;
/// Remove all the records from a config content table.
- (void)deleteAllRecordsFromTableWithSource:(RCNDBSource)source;
/// Remove all the records from experiment table with given key.
/// @param key The key of experiment data belongs to, which are defined in RCNConfigDefines.h.
- (void)deleteExperimentTableForKey:(NSString *)key;
/// Returns true if this a new install of the Config database.
- (BOOL)isNewDatabase;
@end

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,37 @@
/*
* 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.
*/
#ifndef RCNConfigDefines_h
#define RCNConfigDefines_h
#if defined(DEBUG)
#define RCN_MUST_NOT_BE_MAIN_THREAD() \
do { \
NSAssert(![NSThread isMainThread], @"Must not be executing on the main thread."); \
} while (0);
#else
#define RCN_MUST_NOT_BE_MAIN_THREAD() \
do { \
} while (0);
#endif
#define RCNExperimentTableKeyPayload "experiment_payload"
#define RCNExperimentTableKeyMetadata "experiment_metadata"
#define RCNExperimentTableKeyActivePayload "experiment_active_payload"
#define RCNRolloutTableKeyActiveMetadata "active_rollout_metadata"
#define RCNRolloutTableKeyFetchedMetadata "fetched_rollout_metadata"
#endif

View File

@ -0,0 +1,38 @@
/*
* 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>
@class FIRExperimentController;
@class RCNConfigDBManager;
/// Handles experiment information update and persistence.
@interface RCNConfigExperiment : NSObject
/// Designated initializer;
- (nonnull instancetype)initWithDBManager:(RCNConfigDBManager *_Nullable)DBManager
experimentController:(FIRExperimentController *_Nullable)controller
NS_DESIGNATED_INITIALIZER;
/// Use `initWithDBManager:` instead.
- (nonnull instancetype)init NS_UNAVAILABLE;
/// Update/Persist experiment information from config fetch response.
- (void)updateExperimentsWithResponse:(NSArray<NSDictionary<NSString *, id> *> *_Nullable)response;
/// Update experiments to Firebase Analytics when `activateWithCompletion:` happens.
- (void)updateExperimentsWithHandler:(nullable void (^)(NSError *_Nullable error))handler;
@end

View File

@ -0,0 +1,200 @@
/*
* 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 "FirebaseRemoteConfig/Sources/RCNConfigExperiment.h"
#import "FirebaseABTesting/Sources/Private/FirebaseABTestingInternal.h"
#import "FirebaseCore/Extension/FirebaseCoreInternal.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigDefines.h"
static NSString *const kExperimentMetadataKeyLastStartTime = @"last_experiment_start_time";
static NSString *const kServiceOrigin = @"frc";
static NSString *const kMethodNameLatestStartTime =
@"latestExperimentStartTimestampBetweenTimestamp:andPayloads:";
@interface RCNConfigExperiment ()
@property(nonatomic, strong)
NSMutableArray<NSData *> *experimentPayloads; ///< Experiment payloads.
@property(nonatomic, strong)
NSMutableDictionary<NSString *, id> *experimentMetadata; ///< Experiment metadata
@property(nonatomic, strong)
NSMutableArray<NSData *> *activeExperimentPayloads; ///< Activated experiment payloads.
@property(nonatomic, strong) RCNConfigDBManager *DBManager; ///< Database Manager.
@property(nonatomic, strong) FIRExperimentController *experimentController;
@property(nonatomic, strong) NSDateFormatter *experimentStartTimeDateFormatter;
@end
@implementation RCNConfigExperiment
/// Designated initializer
- (instancetype)initWithDBManager:(RCNConfigDBManager *)DBManager
experimentController:(FIRExperimentController *)controller {
self = [super init];
if (self) {
_experimentPayloads = [[NSMutableArray alloc] init];
_experimentMetadata = [[NSMutableDictionary alloc] init];
_activeExperimentPayloads = [[NSMutableArray alloc] init];
_experimentStartTimeDateFormatter = [[NSDateFormatter alloc] init];
[_experimentStartTimeDateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"];
[_experimentStartTimeDateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
// Locale needs to be hardcoded. See
// https://developer.apple.com/library/ios/#qa/qa1480/_index.html for more details.
[_experimentStartTimeDateFormatter
setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];
[_experimentStartTimeDateFormatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]];
_DBManager = DBManager;
_experimentController = controller;
[self loadExperimentFromTable];
}
return self;
}
- (void)loadExperimentFromTable {
if (!_DBManager) {
return;
}
__weak RCNConfigExperiment *weakSelf = self;
RCNDBCompletion completionHandler = ^(BOOL success, NSDictionary<NSString *, id> *result) {
RCNConfigExperiment *strongSelf = weakSelf;
if (strongSelf == nil) {
return;
}
if (result[@RCNExperimentTableKeyPayload]) {
[strongSelf->_experimentPayloads removeAllObjects];
for (NSData *experiment in result[@RCNExperimentTableKeyPayload]) {
NSError *error;
id experimentPayloadJSON = [NSJSONSerialization JSONObjectWithData:experiment
options:kNilOptions
error:&error];
if (!experimentPayloadJSON || error) {
FIRLogWarning(kFIRLoggerRemoteConfig, @"I-RCN000031",
@"Experiment payload could not be parsed as JSON.");
} else {
[strongSelf->_experimentPayloads addObject:experiment];
}
}
}
if (result[@RCNExperimentTableKeyMetadata]) {
strongSelf->_experimentMetadata = [result[@RCNExperimentTableKeyMetadata] mutableCopy];
}
/// Load activated experiments payload and metadata.
if (result[@RCNExperimentTableKeyActivePayload]) {
[strongSelf->_activeExperimentPayloads removeAllObjects];
for (NSData *experiment in result[@RCNExperimentTableKeyActivePayload]) {
NSError *error;
id experimentPayloadJSON = [NSJSONSerialization JSONObjectWithData:experiment
options:kNilOptions
error:&error];
if (!experimentPayloadJSON || error) {
FIRLogWarning(kFIRLoggerRemoteConfig, @"I-RCN000031",
@"Activated experiment payload could not be parsed as JSON.");
} else {
[strongSelf->_activeExperimentPayloads addObject:experiment];
}
}
}
};
[_DBManager loadExperimentWithCompletionHandler:completionHandler];
}
- (void)updateExperimentsWithResponse:(NSArray<NSDictionary<NSString *, id> *> *)response {
// cache fetched experiment payloads.
[_experimentPayloads removeAllObjects];
[_DBManager deleteExperimentTableForKey:@RCNExperimentTableKeyPayload];
for (NSDictionary<NSString *, id> *experiment in response) {
NSError *error;
NSData *JSONPayload = [NSJSONSerialization dataWithJSONObject:experiment
options:kNilOptions
error:&error];
if (!JSONPayload || error) {
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000030",
@"Invalid experiment payload to be serialized.");
} else {
[_experimentPayloads addObject:JSONPayload];
[_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyPayload
value:JSONPayload
completionHandler:nil];
}
}
}
- (void)updateExperimentsWithHandler:(void (^)(NSError *_Nullable))handler {
FIRLifecycleEvents *lifecycleEvent = [[FIRLifecycleEvents alloc] init];
// Get the last experiment start time prior to the latest payload.
NSTimeInterval lastStartTime =
[_experimentMetadata[kExperimentMetadataKeyLastStartTime] doubleValue];
// Update the last experiment start time with the latest payload.
[self updateExperimentStartTime];
[self.experimentController
updateExperimentsWithServiceOrigin:kServiceOrigin
events:lifecycleEvent
policy:ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest
lastStartTime:lastStartTime
payloads:_experimentPayloads
completionHandler:handler];
/// Update activated experiments payload and metadata in DB.
[self updateActiveExperimentsInDB];
}
- (void)updateExperimentStartTime {
NSTimeInterval existingLastStartTime =
[_experimentMetadata[kExperimentMetadataKeyLastStartTime] doubleValue];
NSTimeInterval latestStartTime =
[self latestStartTimeWithExistingLastStartTime:existingLastStartTime];
_experimentMetadata[kExperimentMetadataKeyLastStartTime] = @(latestStartTime);
if (![NSJSONSerialization isValidJSONObject:_experimentMetadata]) {
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000028",
@"Invalid fetched experiment metadata to be serialized.");
return;
}
NSError *error;
NSData *serializedExperimentMetadata =
[NSJSONSerialization dataWithJSONObject:_experimentMetadata
options:NSJSONWritingPrettyPrinted
error:&error];
[_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyMetadata
value:serializedExperimentMetadata
completionHandler:nil];
}
- (void)updateActiveExperimentsInDB {
/// Put current fetched experiment payloads into activated experiment DB.
[_activeExperimentPayloads removeAllObjects];
[_DBManager deleteExperimentTableForKey:@RCNExperimentTableKeyActivePayload];
for (NSData *experiment in _experimentPayloads) {
[_activeExperimentPayloads addObject:experiment];
[_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyActivePayload
value:experiment
completionHandler:nil];
}
}
- (NSTimeInterval)latestStartTimeWithExistingLastStartTime:(NSTimeInterval)existingLastStartTime {
return [self.experimentController
latestExperimentStartTimestampBetweenTimestamp:existingLastStartTime
andPayloads:_experimentPayloads];
}
@end

View File

@ -0,0 +1,691 @@
/*
* 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 "FirebaseRemoteConfig/Sources/Private/RCNConfigFetch.h"
#import "FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h"
#import <GoogleUtilities/GULNSData+zlib.h>
#import "FirebaseCore/Extension/FirebaseCoreInternal.h"
#import "FirebaseInstallations/Source/Library/Private/FirebaseInstallationsInternal.h"
#import "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigContent.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigExperiment.h"
#import "FirebaseRemoteConfig/Sources/RCNDevice.h"
@import FirebaseRemoteConfigInterop;
#ifdef RCN_STAGING_SERVER
static NSString *const kServerURLDomain =
@"https://staging-firebaseremoteconfig.sandbox.googleapis.com";
#else
static NSString *const kServerURLDomain = @"https://firebaseremoteconfig.googleapis.com";
#endif
static NSString *const kServerURLVersion = @"/v1";
static NSString *const kServerURLProjects = @"/projects/";
static NSString *const kServerURLNamespaces = @"/namespaces/";
static NSString *const kServerURLQuery = @":fetch?";
static NSString *const kServerURLKey = @"key=";
static NSString *const kRequestJSONKeyAppID = @"app_id";
static NSString *const kHTTPMethodPost = @"POST"; ///< HTTP request method config fetch using
static NSString *const kContentTypeHeaderName = @"Content-Type"; ///< HTTP Header Field Name
static NSString *const kContentEncodingHeaderName =
@"Content-Encoding"; ///< HTTP Header Field Name
static NSString *const kAcceptEncodingHeaderName = @"Accept-Encoding"; ///< HTTP Header Field Name
static NSString *const kETagHeaderName = @"etag"; ///< HTTP Header Field Name
static NSString *const kIfNoneMatchETagHeaderName = @"if-none-match"; ///< HTTP Header Field Name
static NSString *const kInstallationsAuthTokenHeaderName = @"x-goog-firebase-installations-auth";
// Sends the bundle ID. Refer to b/130301479 for details.
static NSString *const kiOSBundleIdentifierHeaderName =
@"X-Ios-Bundle-Identifier"; ///< HTTP Header Field Name
static NSString *const kFetchTypeHeaderName =
@"X-Firebase-RC-Fetch-Type"; ///< Custom Http header key to identify the fetch type
static NSString *const kBaseFetchType = @"BASE"; ///< Fetch identifier for Base Fetch
static NSString *const kRealtimeFetchType = @"REALTIME"; ///< Fetch identifier for Realtime Fetch
/// Config HTTP request content type proto buffer
static NSString *const kContentTypeValueJSON = @"application/json";
/// HTTP status codes. Ref: https://cloud.google.com/apis/design/errors#error_retries
static NSInteger const kRCNFetchResponseHTTPStatusCodeOK = 200;
static NSInteger const kRCNFetchResponseHTTPStatusTooManyRequests = 429;
static NSInteger const kRCNFetchResponseHTTPStatusCodeInternalError = 500;
static NSInteger const kRCNFetchResponseHTTPStatusCodeServiceUnavailable = 503;
static NSInteger const kRCNFetchResponseHTTPStatusCodeGatewayTimeout = 504;
#pragma mark - RCNConfig
@implementation RCNConfigFetch {
RCNConfigContent *_content;
RCNConfigSettings *_settings;
id<FIRAnalyticsInterop> _analytics;
RCNConfigExperiment *_experiment;
dispatch_queue_t _lockQueue; /// Guard the read/write operation.
NSURLSession *_fetchSession; /// Managed internally by the fetch instance.
NSString *_FIRNamespace;
FIROptions *_options;
NSString *_templateVersionNumber;
}
- (instancetype)init {
NSAssert(NO, @"Invalid initializer.");
return nil;
}
/// Designated initializer
- (instancetype)initWithContent:(RCNConfigContent *)content
DBManager:(RCNConfigDBManager *)DBManager
settings:(RCNConfigSettings *)settings
analytics:(nullable id<FIRAnalyticsInterop>)analytics
experiment:(RCNConfigExperiment *)experiment
queue:(dispatch_queue_t)queue
namespace:(NSString *)FIRNamespace
options:(FIROptions *)options {
self = [super init];
if (self) {
_FIRNamespace = FIRNamespace;
_settings = settings;
_analytics = analytics;
_experiment = experiment;
_lockQueue = queue;
_content = content;
_fetchSession = [self newFetchSession];
_options = options;
_templateVersionNumber = [self->_settings lastFetchedTemplateVersion];
}
return self;
}
/// Force a new NSURLSession creation for updated config.
- (void)recreateNetworkSession {
if (_fetchSession) {
[_fetchSession invalidateAndCancel];
}
_fetchSession = [self newFetchSession];
}
/// Return the current session. (Tests).
- (NSURLSession *)currentNetworkSession {
return _fetchSession;
}
- (void)dealloc {
[_fetchSession invalidateAndCancel];
}
#pragma mark - Fetch Config API
- (void)fetchConfigWithExpirationDuration:(NSTimeInterval)expirationDuration
completionHandler:(FIRRemoteConfigFetchCompletion)completionHandler {
// Note: We expect the googleAppID to always be available.
BOOL hasDeviceContextChanged =
FIRRemoteConfigHasDeviceContextChanged(_settings.deviceContext, _options.googleAppID);
__weak RCNConfigFetch *weakSelf = self;
dispatch_async(_lockQueue, ^{
RCNConfigFetch *strongSelf = weakSelf;
if (strongSelf == nil) {
return;
}
// Check whether we are outside of the minimum fetch interval.
if (![strongSelf->_settings hasMinimumFetchIntervalElapsed:expirationDuration] &&
!hasDeviceContextChanged) {
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000051", @"Returning cached data.");
return [strongSelf reportCompletionOnHandler:completionHandler
withStatus:FIRRemoteConfigFetchStatusSuccess
withError:nil];
}
// Check if a fetch is already in progress.
if (strongSelf->_settings.isFetchInProgress) {
// Check if we have some fetched data.
if (strongSelf->_settings.lastFetchTimeInterval > 0) {
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000052",
@"A fetch is already in progress. Using previous fetch results.");
return [strongSelf reportCompletionOnHandler:completionHandler
withStatus:strongSelf->_settings.lastFetchStatus
withError:nil];
} else {
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000053",
@"A fetch is already in progress. Ignoring duplicate request.");
return [strongSelf reportCompletionOnHandler:completionHandler
withStatus:FIRRemoteConfigFetchStatusFailure
withError:nil];
}
}
// Check whether cache data is within throttle limit.
if ([strongSelf->_settings shouldThrottle] && !hasDeviceContextChanged) {
// Must set lastFetchStatus before FailReason.
strongSelf->_settings.lastFetchStatus = FIRRemoteConfigFetchStatusThrottled;
strongSelf->_settings.lastFetchError = FIRRemoteConfigErrorThrottled;
NSTimeInterval throttledEndTime = strongSelf->_settings.exponentialBackoffThrottleEndTime;
NSError *error =
[NSError errorWithDomain:FIRRemoteConfigErrorDomain
code:FIRRemoteConfigErrorThrottled
userInfo:@{
FIRRemoteConfigThrottledEndTimeInSecondsKey : @(throttledEndTime)
}];
return [strongSelf reportCompletionOnHandler:completionHandler
withStatus:strongSelf->_settings.lastFetchStatus
withError:error];
}
strongSelf->_settings.isFetchInProgress = YES;
NSString *fetchTypeHeader = [NSString stringWithFormat:@"%@/1", kBaseFetchType];
[strongSelf refreshInstallationsTokenWithFetchHeader:fetchTypeHeader
completionHandler:completionHandler
updateCompletionHandler:nil];
});
}
#pragma mark - Fetch helpers
- (void)realtimeFetchConfigWithNoExpirationDuration:(NSInteger)fetchAttemptNumber
completionHandler:(RCNConfigFetchCompletion)completionHandler {
// Note: We expect the googleAppID to always be available.
BOOL hasDeviceContextChanged =
FIRRemoteConfigHasDeviceContextChanged(_settings.deviceContext, _options.googleAppID);
__weak RCNConfigFetch *weakSelf = self;
dispatch_async(_lockQueue, ^{
RCNConfigFetch *strongSelf = weakSelf;
if (strongSelf == nil) {
return;
}
// Check whether cache data is within throttle limit.
if ([strongSelf->_settings shouldThrottle] && !hasDeviceContextChanged) {
// Must set lastFetchStatus before FailReason.
strongSelf->_settings.lastFetchStatus = FIRRemoteConfigFetchStatusThrottled;
strongSelf->_settings.lastFetchError = FIRRemoteConfigErrorThrottled;
NSTimeInterval throttledEndTime = strongSelf->_settings.exponentialBackoffThrottleEndTime;
NSError *error =
[NSError errorWithDomain:FIRRemoteConfigErrorDomain
code:FIRRemoteConfigErrorThrottled
userInfo:@{
FIRRemoteConfigThrottledEndTimeInSecondsKey : @(throttledEndTime)
}];
return [strongSelf reportCompletionWithStatus:FIRRemoteConfigFetchStatusFailure
withUpdate:nil
withError:error
completionHandler:nil
updateCompletionHandler:completionHandler];
}
strongSelf->_settings.isFetchInProgress = YES;
NSString *fetchTypeHeader =
[NSString stringWithFormat:@"%@/%ld", kRealtimeFetchType, (long)fetchAttemptNumber];
[strongSelf refreshInstallationsTokenWithFetchHeader:fetchTypeHeader
completionHandler:nil
updateCompletionHandler:completionHandler];
});
}
- (NSString *)FIRAppNameFromFullyQualifiedNamespace {
return [[_FIRNamespace componentsSeparatedByString:@":"] lastObject];
}
/// Refresh installation ID token before fetching config. installation ID is now mandatory for fetch
/// requests to work.(b/14751422).
- (void)refreshInstallationsTokenWithFetchHeader:(NSString *)fetchTypeHeader
completionHandler:(FIRRemoteConfigFetchCompletion)completionHandler
updateCompletionHandler:(RCNConfigFetchCompletion)updateCompletionHandler {
FIRInstallations *installations = [FIRInstallations
installationsWithApp:[FIRApp appNamed:[self FIRAppNameFromFullyQualifiedNamespace]]];
if (!installations || !_options.GCMSenderID) {
NSString *errorDescription = @"Failed to get GCMSenderID";
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000074", @"%@",
[NSString stringWithFormat:@"%@", errorDescription]);
self->_settings.isFetchInProgress = NO;
return [self
reportCompletionOnHandler:completionHandler
withStatus:FIRRemoteConfigFetchStatusFailure
withError:[NSError errorWithDomain:FIRRemoteConfigErrorDomain
code:FIRRemoteConfigErrorInternalError
userInfo:@{
NSLocalizedDescriptionKey : errorDescription
}]];
}
__weak RCNConfigFetch *weakSelf = self;
FIRInstallationsTokenHandler installationsTokenHandler = ^(
FIRInstallationsAuthTokenResult *tokenResult, NSError *error) {
RCNConfigFetch *strongSelf = weakSelf;
if (strongSelf == nil) {
return;
}
if (!tokenResult || !tokenResult.authToken || error) {
NSString *errorDescription =
[NSString stringWithFormat:@"Failed to get installations token. Error : %@.", error];
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000073", @"%@",
[NSString stringWithFormat:@"%@", errorDescription]);
strongSelf->_settings.isFetchInProgress = NO;
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
userInfo[NSLocalizedDescriptionKey] = errorDescription;
userInfo[NSUnderlyingErrorKey] = error.userInfo[NSUnderlyingErrorKey];
return [strongSelf
reportCompletionOnHandler:completionHandler
withStatus:FIRRemoteConfigFetchStatusFailure
withError:[NSError errorWithDomain:FIRRemoteConfigErrorDomain
code:FIRRemoteConfigErrorInternalError
userInfo:userInfo]];
}
// We have a valid token. Get the backing installationID.
[installations installationIDWithCompletion:^(NSString *_Nullable identifier,
NSError *_Nullable error) {
RCNConfigFetch *strongSelf = weakSelf;
if (strongSelf == nil) {
return;
}
// Dispatch to the RC serial queue to update settings on the queue.
dispatch_async(strongSelf->_lockQueue, ^{
RCNConfigFetch *strongSelfQueue = weakSelf;
if (strongSelfQueue == nil) {
return;
}
// Update config settings with the IID and token.
strongSelfQueue->_settings.configInstallationsToken = tokenResult.authToken;
strongSelfQueue->_settings.configInstallationsIdentifier = identifier;
if (!identifier || error) {
NSString *errorDescription =
[NSString stringWithFormat:@"Error getting iid : %@.", error];
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
userInfo[NSLocalizedDescriptionKey] = errorDescription;
userInfo[NSUnderlyingErrorKey] = error.userInfo[NSUnderlyingErrorKey];
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000055", @"%@",
[NSString stringWithFormat:@"%@", errorDescription]);
strongSelfQueue->_settings.isFetchInProgress = NO;
return [strongSelfQueue
reportCompletionOnHandler:completionHandler
withStatus:FIRRemoteConfigFetchStatusFailure
withError:[NSError errorWithDomain:FIRRemoteConfigErrorDomain
code:FIRRemoteConfigErrorInternalError
userInfo:userInfo]];
}
FIRLogInfo(kFIRLoggerRemoteConfig, @"I-RCN000022", @"Success to get iid : %@.",
strongSelfQueue->_settings.configInstallationsIdentifier);
[strongSelf doFetchCall:fetchTypeHeader
completionHandler:completionHandler
updateCompletionHandler:updateCompletionHandler];
});
}];
};
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000039", @"Starting requesting token.");
[installations authTokenWithCompletion:installationsTokenHandler];
}
- (void)doFetchCall:(NSString *)fetchTypeHeader
completionHandler:(FIRRemoteConfigFetchCompletion)completionHandler
updateCompletionHandler:(RCNConfigFetchCompletion)updateCompletionHandler {
[self getAnalyticsUserPropertiesWithCompletionHandler:^(NSDictionary *userProperties) {
dispatch_async(self->_lockQueue, ^{
[self fetchWithUserProperties:userProperties
fetchTypeHeader:fetchTypeHeader
completionHandler:completionHandler
updateCompletionHandler:updateCompletionHandler];
});
}];
}
- (void)getAnalyticsUserPropertiesWithCompletionHandler:
(FIRAInteropUserPropertiesCallback)completionHandler {
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000060", @"Fetch with user properties completed.");
id<FIRAnalyticsInterop> analytics = self->_analytics;
if (analytics == nil) {
completionHandler(@{});
} else {
[analytics getUserPropertiesWithCallback:completionHandler];
}
}
- (void)reportCompletionOnHandler:(FIRRemoteConfigFetchCompletion)completionHandler
withStatus:(FIRRemoteConfigFetchStatus)status
withError:(NSError *)error {
[self reportCompletionWithStatus:status
withUpdate:nil
withError:error
completionHandler:completionHandler
updateCompletionHandler:nil];
}
- (void)reportCompletionWithStatus:(FIRRemoteConfigFetchStatus)status
withUpdate:(FIRRemoteConfigUpdate *)update
withError:(NSError *)error
completionHandler:(FIRRemoteConfigFetchCompletion)completionHandler
updateCompletionHandler:(RCNConfigFetchCompletion)updateCompletionHandler {
if (completionHandler) {
dispatch_async(dispatch_get_main_queue(), ^{
completionHandler(status, error);
});
}
// if completion handler expects a config update response
if (updateCompletionHandler) {
dispatch_async(dispatch_get_main_queue(), ^{
updateCompletionHandler(status, update, error);
});
}
}
- (void)fetchWithUserProperties:(NSDictionary *)userProperties
fetchTypeHeader:(NSString *)fetchTypeHeader
completionHandler:(FIRRemoteConfigFetchCompletion)completionHandler
updateCompletionHandler:(RCNConfigFetchCompletion)updateCompletionHandler {
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000061", @"Fetch with user properties initiated.");
NSString *postRequestString = [_settings nextRequestWithUserProperties:userProperties];
// Get POST request content.
NSData *content = [postRequestString dataUsingEncoding:NSUTF8StringEncoding];
NSError *compressionError;
NSData *compressedContent = [NSData gul_dataByGzippingData:content error:&compressionError];
if (compressionError) {
NSString *errString = [NSString stringWithFormat:@"Failed to compress the config request."];
FIRLogWarning(kFIRLoggerRemoteConfig, @"I-RCN000033", @"%@", errString);
NSError *error = [NSError errorWithDomain:FIRRemoteConfigErrorDomain
code:FIRRemoteConfigErrorInternalError
userInfo:@{NSLocalizedDescriptionKey : errString}];
self->_settings.isFetchInProgress = NO;
return [self reportCompletionWithStatus:FIRRemoteConfigFetchStatusFailure
withUpdate:nil
withError:error
completionHandler:completionHandler
updateCompletionHandler:updateCompletionHandler];
}
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000040", @"Start config fetch.");
__weak RCNConfigFetch *weakSelf = self;
RCNConfigFetcherCompletion fetcherCompletion = ^(NSData *data, NSURLResponse *response,
NSError *error) {
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000050",
@"config fetch completed. Error: %@ StatusCode: %ld", (error ? error : @"nil"),
(long)[((NSHTTPURLResponse *)response) statusCode]);
RCNConfigFetch *fetcherCompletionSelf = weakSelf;
if (fetcherCompletionSelf == nil) {
return;
}
// The fetch has completed.
fetcherCompletionSelf->_settings.isFetchInProgress = NO;
dispatch_async(fetcherCompletionSelf->_lockQueue, ^{
RCNConfigFetch *strongSelf = weakSelf;
if (strongSelf == nil) {
return;
}
NSInteger statusCode = [((NSHTTPURLResponse *)response) statusCode];
if (error || (statusCode != kRCNFetchResponseHTTPStatusCodeOK)) {
// Update metadata about fetch failure.
[strongSelf->_settings updateMetadataWithFetchSuccessStatus:NO templateVersion:nil];
if (error) {
if (strongSelf->_settings.lastFetchStatus == FIRRemoteConfigFetchStatusSuccess) {
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000025",
@"RCN Fetch failure: %@. Using cached config result.", error);
} else {
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000026",
@"RCN Fetch failure: %@. No cached config result.", error);
}
}
if (statusCode != kRCNFetchResponseHTTPStatusCodeOK) {
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000026",
@"RCN Fetch failure. Response http error code: %ld", (long)statusCode);
// Response error code 429, 500, 503 will trigger exponential backoff mode.
// TODO: check error code in helper
if (statusCode == kRCNFetchResponseHTTPStatusTooManyRequests ||
statusCode == kRCNFetchResponseHTTPStatusCodeInternalError ||
statusCode == kRCNFetchResponseHTTPStatusCodeServiceUnavailable ||
statusCode == kRCNFetchResponseHTTPStatusCodeGatewayTimeout) {
[strongSelf->_settings updateExponentialBackoffTime];
if ([strongSelf->_settings shouldThrottle]) {
// Must set lastFetchStatus before FailReason.
strongSelf->_settings.lastFetchStatus = FIRRemoteConfigFetchStatusThrottled;
strongSelf->_settings.lastFetchError = FIRRemoteConfigErrorThrottled;
NSTimeInterval throttledEndTime =
strongSelf->_settings.exponentialBackoffThrottleEndTime;
NSError *error = [NSError
errorWithDomain:FIRRemoteConfigErrorDomain
code:FIRRemoteConfigErrorThrottled
userInfo:@{
FIRRemoteConfigThrottledEndTimeInSecondsKey : @(throttledEndTime)
}];
return [strongSelf reportCompletionWithStatus:strongSelf->_settings.lastFetchStatus
withUpdate:nil
withError:error
completionHandler:completionHandler
updateCompletionHandler:updateCompletionHandler];
}
}
}
// Return back the received error.
// Must set lastFetchStatus before setting Fetch Error.
strongSelf->_settings.lastFetchStatus = FIRRemoteConfigFetchStatusFailure;
strongSelf->_settings.lastFetchError = FIRRemoteConfigErrorInternalError;
NSMutableDictionary<NSErrorUserInfoKey, id> *userInfo = [NSMutableDictionary dictionary];
userInfo[NSUnderlyingErrorKey] = error;
userInfo[NSLocalizedDescriptionKey] =
error.localizedDescription
?: [NSString
stringWithFormat:@"Internal Error. Status code: %ld", (long)statusCode];
return [strongSelf
reportCompletionWithStatus:FIRRemoteConfigFetchStatusFailure
withUpdate:nil
withError:[NSError errorWithDomain:FIRRemoteConfigErrorDomain
code:FIRRemoteConfigErrorInternalError
userInfo:userInfo]
completionHandler:completionHandler
updateCompletionHandler:updateCompletionHandler];
}
// Fetch was successful. Check if we have data.
NSError *retError;
if (!data) {
FIRLogInfo(kFIRLoggerRemoteConfig, @"I-RCN000043", @"RCN Fetch: No data in fetch response");
// There may still be a difference between fetched and active config
FIRRemoteConfigUpdate *update =
[strongSelf->_content getConfigUpdateForNamespace:strongSelf->_FIRNamespace];
return [strongSelf reportCompletionWithStatus:FIRRemoteConfigFetchStatusSuccess
withUpdate:update
withError:nil
completionHandler:completionHandler
updateCompletionHandler:updateCompletionHandler];
}
// Config fetch succeeded.
// JSONObjectWithData is always expected to return an NSDictionary in our case
NSMutableDictionary *fetchedConfig =
[NSJSONSerialization JSONObjectWithData:data
options:NSJSONReadingMutableContainers
error:&retError];
if (retError) {
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000042",
@"RCN Fetch failure: %@. Could not parse response data as JSON", error);
}
// Check and log if we received an error from the server
if (fetchedConfig && fetchedConfig.count == 1 && fetchedConfig[RCNFetchResponseKeyError]) {
NSString *errStr = [NSString stringWithFormat:@"RCN Fetch Failure: Server returned error:"];
NSDictionary *errDict = fetchedConfig[RCNFetchResponseKeyError];
if (errDict[RCNFetchResponseKeyErrorCode]) {
errStr = [errStr
stringByAppendingString:[NSString
stringWithFormat:@"code: %@",
errDict[RCNFetchResponseKeyErrorCode]]];
}
if (errDict[RCNFetchResponseKeyErrorStatus]) {
errStr = [errStr stringByAppendingString:
[NSString stringWithFormat:@". Status: %@",
errDict[RCNFetchResponseKeyErrorStatus]]];
}
if (errDict[RCNFetchResponseKeyErrorMessage]) {
errStr =
[errStr stringByAppendingString:
[NSString stringWithFormat:@". Message: %@",
errDict[RCNFetchResponseKeyErrorMessage]]];
}
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000044", @"%@.", errStr);
NSError *error = [NSError errorWithDomain:FIRRemoteConfigErrorDomain
code:FIRRemoteConfigErrorInternalError
userInfo:@{NSLocalizedDescriptionKey : errStr}];
return [strongSelf reportCompletionWithStatus:FIRRemoteConfigFetchStatusFailure
withUpdate:nil
withError:error
completionHandler:completionHandler
updateCompletionHandler:updateCompletionHandler];
}
// Add the fetched config to the database.
if (fetchedConfig) {
// Update config content to cache and DB.
[strongSelf->_content updateConfigContentWithResponse:fetchedConfig
forNamespace:strongSelf->_FIRNamespace];
// Update experiments only for 3p namespace
NSString *namespace = [strongSelf->_FIRNamespace
substringToIndex:[strongSelf->_FIRNamespace rangeOfString:@":"].location];
if ([namespace isEqualToString:FIRRemoteConfigConstants.FIRNamespaceGoogleMobilePlatform]) {
[strongSelf->_experiment updateExperimentsWithResponse:
fetchedConfig[RCNFetchResponseKeyExperimentDescriptions]];
}
strongSelf->_templateVersionNumber = [strongSelf getTemplateVersionNumber:fetchedConfig];
} else {
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000063",
@"Empty response with no fetched config.");
}
// We had a successful fetch. Update the current eTag in settings if different.
NSString *latestETag = ((NSHTTPURLResponse *)response).allHeaderFields[kETagHeaderName];
if (!strongSelf->_settings.lastETag ||
!([strongSelf->_settings.lastETag isEqualToString:latestETag])) {
strongSelf->_settings.lastETag = latestETag;
}
// Compute config update after successful fetch
FIRRemoteConfigUpdate *update =
[strongSelf->_content getConfigUpdateForNamespace:strongSelf->_FIRNamespace];
[strongSelf->_settings
updateMetadataWithFetchSuccessStatus:YES
templateVersion:strongSelf->_templateVersionNumber];
return [strongSelf reportCompletionWithStatus:FIRRemoteConfigFetchStatusSuccess
withUpdate:update
withError:nil
completionHandler:completionHandler
updateCompletionHandler:updateCompletionHandler];
});
};
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000061", @"Making remote config fetch.");
NSURLSessionDataTask *dataTask = [self URLSessionDataTaskWithContent:compressedContent
fetchTypeHeader:fetchTypeHeader
completionHandler:fetcherCompletion];
[dataTask resume];
}
- (NSString *)constructServerURL {
NSString *serverURLStr = [[NSString alloc] initWithString:kServerURLDomain];
serverURLStr = [serverURLStr stringByAppendingString:kServerURLVersion];
serverURLStr = [serverURLStr stringByAppendingString:kServerURLProjects];
serverURLStr = [serverURLStr stringByAppendingString:_options.projectID];
serverURLStr = [serverURLStr stringByAppendingString:kServerURLNamespaces];
// Get the namespace from the fully qualified namespace string of "namespace:FIRAppName".
NSString *namespace =
[_FIRNamespace substringToIndex:[_FIRNamespace rangeOfString:@":"].location];
serverURLStr = [serverURLStr stringByAppendingString:namespace];
serverURLStr = [serverURLStr stringByAppendingString:kServerURLQuery];
if (_options.APIKey) {
serverURLStr = [serverURLStr stringByAppendingString:kServerURLKey];
serverURLStr = [serverURLStr stringByAppendingString:_options.APIKey];
} else {
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000071",
@"Missing `APIKey` from `FirebaseOptions`, please ensure the configured "
@"`FirebaseApp` is configured with `FirebaseOptions` that contains an `APIKey`.");
}
return serverURLStr;
}
- (NSURLSession *)newFetchSession {
NSURLSessionConfiguration *config =
[[NSURLSessionConfiguration defaultSessionConfiguration] copy];
config.timeoutIntervalForRequest = _settings.fetchTimeout;
config.timeoutIntervalForResource = _settings.fetchTimeout;
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
return session;
}
- (NSURLSessionDataTask *)URLSessionDataTaskWithContent:(NSData *)content
fetchTypeHeader:(NSString *)fetchTypeHeader
completionHandler:
(RCNConfigFetcherCompletion)fetcherCompletion {
NSURL *URL = [NSURL URLWithString:[self constructServerURL]];
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000046", @"%@",
[NSString stringWithFormat:@"Making config request: %@", [URL absoluteString]]);
NSTimeInterval timeoutInterval = _fetchSession.configuration.timeoutIntervalForResource;
NSMutableURLRequest *URLRequest =
[[NSMutableURLRequest alloc] initWithURL:URL
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:timeoutInterval];
URLRequest.HTTPMethod = kHTTPMethodPost;
[URLRequest setValue:kContentTypeValueJSON forHTTPHeaderField:kContentTypeHeaderName];
[URLRequest setValue:_settings.configInstallationsToken
forHTTPHeaderField:kInstallationsAuthTokenHeaderName];
[URLRequest setValue:[[NSBundle mainBundle] bundleIdentifier]
forHTTPHeaderField:kiOSBundleIdentifierHeaderName];
[URLRequest setValue:@"gzip" forHTTPHeaderField:kContentEncodingHeaderName];
[URLRequest setValue:@"gzip" forHTTPHeaderField:kAcceptEncodingHeaderName];
[URLRequest setValue:fetchTypeHeader forHTTPHeaderField:kFetchTypeHeaderName];
// Set the eTag from the last successful fetch, if available.
if (_settings.lastETag) {
[URLRequest setValue:_settings.lastETag forHTTPHeaderField:kIfNoneMatchETagHeaderName];
}
[URLRequest setHTTPBody:content];
return [_fetchSession dataTaskWithRequest:URLRequest completionHandler:fetcherCompletion];
}
- (NSString *)getTemplateVersionNumber:(NSDictionary *)fetchedConfig {
if (fetchedConfig != nil && [fetchedConfig objectForKey:RCNFetchResponseKeyTemplateVersion] &&
[[fetchedConfig objectForKey:RCNFetchResponseKeyTemplateVersion]
isKindOfClass:[NSString class]]) {
return (NSString *)[fetchedConfig objectForKey:RCNFetchResponseKeyTemplateVersion];
}
return @"0";
}
@end

View File

@ -0,0 +1,40 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
#import "FirebaseRemoteConfig/Sources/Private/RCNConfigFetch.h"
#import "FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h"
@class RCNConfigSettings;
@interface RCNConfigRealtime : NSObject <NSURLSessionDataDelegate>
/// Completion handler invoked by config update methods when they get a response from the server.
///
/// @param error Error message on failure.
typedef void (^RCNConfigUpdateCompletion)(FIRRemoteConfigUpdate *_Nullable configUpdate,
NSError *_Nullable error);
- (instancetype _Nonnull)init:(RCNConfigFetch *_Nonnull)configFetch
settings:(RCNConfigSettings *_Nonnull)settings
namespace:(NSString *_Nonnull)namespace
options:(FIROptions *_Nonnull)options;
- (FIRConfigUpdateListenerRegistration *_Nonnull)addConfigUpdateListener:
(RCNConfigUpdateCompletion _Nonnull)listener;
- (void)removeConfigUpdateListener:(RCNConfigUpdateCompletion _Nonnull)listener;
@end

View File

@ -0,0 +1,732 @@
/*
* 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 "FirebaseRemoteConfig/Sources/RCNConfigRealtime.h"
#import <Foundation/Foundation.h>
#import <GoogleUtilities/GULNSData+zlib.h>
#import "FirebaseCore/Extension/FirebaseCoreInternal.h"
#import "FirebaseInstallations/Source/Library/Private/FirebaseInstallationsInternal.h"
#import "FirebaseRemoteConfig/Sources/Private/RCNConfigFetch.h"
#import "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h"
#import "FirebaseRemoteConfig/Sources/RCNDevice.h"
/// URL params
static NSString *const kServerURLDomain = @"https://firebaseremoteconfigrealtime.googleapis.com";
static NSString *const kServerURLVersion = @"/v1";
static NSString *const kServerURLProjects = @"/projects/";
static NSString *const kServerURLNamespaces = @"/namespaces/";
static NSString *const kServerURLQuery = @":streamFetchInvalidations?";
static NSString *const kServerURLKey = @"key=";
/// Realtime API enablement
static NSString *const kServerForbiddenStatusCode = @"\"code\": 403";
/// Header names
static NSString *const kHTTPMethodPost = @"POST"; ///< HTTP request method config fetch using
static NSString *const kContentTypeHeaderName = @"Content-Type"; ///< HTTP Header Field Name
static NSString *const kContentEncodingHeaderName =
@"Content-Encoding"; ///< HTTP Header Field Name
static NSString *const kAcceptEncodingHeaderName = @"Accept"; ///< HTTP Header Field Name
static NSString *const kETagHeaderName = @"etag"; ///< HTTP Header Field Name
static NSString *const kIfNoneMatchETagHeaderName = @"if-none-match"; ///< HTTP Header Field Name
static NSString *const kInstallationsAuthTokenHeaderName = @"x-goog-firebase-installations-auth";
// Sends the bundle ID. Refer to b/130301479 for details.
static NSString *const kiOSBundleIdentifierHeaderName =
@"X-Ios-Bundle-Identifier"; ///< HTTP Header Field Name
/// Retryable HTTP status code.
static NSInteger const kRCNFetchResponseHTTPStatusOk = 200;
static NSInteger const kRCNFetchResponseHTTPStatusClientTimeout = 429;
static NSInteger const kRCNFetchResponseHTTPStatusTooManyRequests = 429;
static NSInteger const kRCNFetchResponseHTTPStatusCodeBadGateway = 502;
static NSInteger const kRCNFetchResponseHTTPStatusCodeServiceUnavailable = 503;
static NSInteger const kRCNFetchResponseHTTPStatusCodeGatewayTimeout = 504;
/// Invalidation message field names.
static NSString *const kTemplateVersionNumberKey = @"latestTemplateVersionNumber";
static NSString *const kIsFeatureDisabled = @"featureDisabled";
static NSTimeInterval gTimeoutSeconds = 330;
static NSInteger const gFetchAttempts = 3;
// Retry parameters
static NSInteger const gMaxRetries = 7;
@interface FIRConfigUpdateListenerRegistration ()
@property(strong, atomic, nonnull) RCNConfigUpdateCompletion completionHandler;
@end
@implementation FIRConfigUpdateListenerRegistration {
RCNConfigRealtime *_realtimeClient;
}
- (instancetype)initWithClient:(RCNConfigRealtime *)realtimeClient
completionHandler:(RCNConfigUpdateCompletion)completionHandler {
self = [super init];
if (self) {
_realtimeClient = realtimeClient;
_completionHandler = completionHandler;
}
return self;
}
- (void)remove {
[self->_realtimeClient removeConfigUpdateListener:_completionHandler];
}
@end
@interface RCNConfigRealtime ()
@property(strong, atomic, nonnull) NSMutableSet<RCNConfigUpdateCompletion> *listeners;
@property(strong, atomic, nonnull) dispatch_queue_t realtimeLockQueue;
@property(strong, atomic, nonnull) NSNotificationCenter *notificationCenter;
@property(strong, atomic) NSURLSession *session;
@property(strong, atomic) NSURLSessionDataTask *dataTask;
@property(strong, atomic) NSMutableURLRequest *request;
@end
@implementation RCNConfigRealtime {
RCNConfigFetch *_configFetch;
RCNConfigSettings *_settings;
FIROptions *_options;
NSString *_namespace;
NSInteger _remainingRetryCount;
bool _isRequestInProgress;
bool _isInBackground;
bool _isRealtimeDisabled;
}
- (instancetype)init:(RCNConfigFetch *)configFetch
settings:(RCNConfigSettings *)settings
namespace:(NSString *)namespace
options:(FIROptions *)options {
self = [super init];
if (self) {
_listeners = [[NSMutableSet alloc] init];
_realtimeLockQueue = [RCNConfigRealtime realtimeRemoteConfigSerialQueue];
_notificationCenter = [NSNotificationCenter defaultCenter];
_configFetch = configFetch;
_settings = settings;
_options = options;
_namespace = namespace;
_remainingRetryCount = MAX(gMaxRetries - [_settings realtimeRetryCount], 1);
_isRequestInProgress = false;
_isRealtimeDisabled = false;
_isInBackground = false;
[self setUpHttpRequest];
[self setUpHttpSession];
[self backgroundChangeListener];
}
return self;
}
/// Singleton instance of serial queue for queuing all incoming RC calls.
+ (dispatch_queue_t)realtimeRemoteConfigSerialQueue {
static dispatch_once_t onceToken;
static dispatch_queue_t realtimeRemoteConfigQueue;
dispatch_once(&onceToken, ^{
realtimeRemoteConfigQueue =
dispatch_queue_create(RCNRemoteConfigQueueLabel, DISPATCH_QUEUE_SERIAL);
});
return realtimeRemoteConfigQueue;
}
- (void)propogateErrors:(NSError *)error {
__weak RCNConfigRealtime *weakSelf = self;
dispatch_async(_realtimeLockQueue, ^{
__strong RCNConfigRealtime *strongSelf = weakSelf;
for (RCNConfigUpdateCompletion listener in strongSelf->_listeners) {
listener(nil, error);
}
});
}
#pragma mark - Test Only Helpers
// TESTING ONLY
- (void)triggerListenerForTesting:(void (^_Nonnull)(FIRRemoteConfigUpdate *configUpdate,
NSError *_Nullable error))listener {
listener([[FIRRemoteConfigUpdate alloc] init], nil);
}
#pragma mark - Http Helpers
- (NSString *)constructServerURL {
NSString *serverURLStr = [[NSString alloc] initWithString:kServerURLDomain];
serverURLStr = [serverURLStr stringByAppendingString:kServerURLVersion];
serverURLStr = [serverURLStr stringByAppendingString:kServerURLProjects];
serverURLStr = [serverURLStr stringByAppendingString:_options.GCMSenderID];
serverURLStr = [serverURLStr stringByAppendingString:kServerURLNamespaces];
/// Get the namespace from the fully qualified namespace string of "namespace:FIRAppName".
NSString *namespace = [_namespace substringToIndex:[_namespace rangeOfString:@":"].location];
serverURLStr = [serverURLStr stringByAppendingString:namespace];
serverURLStr = [serverURLStr stringByAppendingString:kServerURLQuery];
if (_options.APIKey) {
serverURLStr = [serverURLStr stringByAppendingString:kServerURLKey];
serverURLStr = [serverURLStr stringByAppendingString:_options.APIKey];
} else {
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000071",
@"Missing `APIKey` from `FirebaseOptions`, please ensure the configured "
@"`FirebaseApp` is configured with `FirebaseOptions` that contains an `APIKey`.");
}
return serverURLStr;
}
- (NSString *)FIRAppNameFromFullyQualifiedNamespace {
return [[_namespace componentsSeparatedByString:@":"] lastObject];
}
- (void)reportCompletionOnHandler:(FIRRemoteConfigFetchCompletion)completionHandler
withStatus:(FIRRemoteConfigFetchStatus)status
withError:(NSError *)error {
if (completionHandler) {
dispatch_async(_realtimeLockQueue, ^{
completionHandler(status, error);
});
}
}
/// Refresh installation ID token before fetching config. installation ID is now mandatory for fetch
/// requests to work.(b/14751422).
- (void)refreshInstallationsTokenWithCompletionHandler:
(FIRRemoteConfigFetchCompletion)completionHandler {
FIRInstallations *installations = [FIRInstallations
installationsWithApp:[FIRApp appNamed:[self FIRAppNameFromFullyQualifiedNamespace]]];
if (!installations || !_options.GCMSenderID) {
NSString *errorDescription = @"Failed to get GCMSenderID";
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000074", @"%@",
[NSString stringWithFormat:@"%@", errorDescription]);
return [self
reportCompletionOnHandler:completionHandler
withStatus:FIRRemoteConfigFetchStatusFailure
withError:[NSError errorWithDomain:FIRRemoteConfigErrorDomain
code:FIRRemoteConfigErrorInternalError
userInfo:@{
NSLocalizedDescriptionKey : errorDescription
}]];
}
__weak RCNConfigRealtime *weakSelf = self;
FIRInstallationsTokenHandler installationsTokenHandler = ^(
FIRInstallationsAuthTokenResult *tokenResult, NSError *error) {
RCNConfigRealtime *strongSelf = weakSelf;
if (strongSelf == nil) {
return;
}
if (!tokenResult || !tokenResult.authToken || error) {
NSString *errorDescription =
[NSString stringWithFormat:@"Failed to get installations token. Error : %@.", error];
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000073", @"%@",
[NSString stringWithFormat:@"%@", errorDescription]);
return [strongSelf
reportCompletionOnHandler:completionHandler
withStatus:FIRRemoteConfigFetchStatusFailure
withError:[NSError errorWithDomain:FIRRemoteConfigErrorDomain
code:FIRRemoteConfigErrorInternalError
userInfo:@{
NSLocalizedDescriptionKey : errorDescription
}]];
}
/// We have a valid token. Get the backing installationID.
[installations installationIDWithCompletion:^(NSString *_Nullable identifier,
NSError *_Nullable error) {
RCNConfigRealtime *strongSelf = weakSelf;
if (strongSelf == nil) {
return;
}
// Dispatch to the RC serial queue to update settings on the queue.
dispatch_async(strongSelf->_realtimeLockQueue, ^{
RCNConfigRealtime *strongSelfQueue = weakSelf;
if (strongSelfQueue == nil) {
return;
}
/// Update config settings with the IID and token.
strongSelfQueue->_settings.configInstallationsToken = tokenResult.authToken;
strongSelfQueue->_settings.configInstallationsIdentifier = identifier;
if (!identifier || error) {
NSString *errorDescription =
[NSString stringWithFormat:@"Error getting iid : %@.", error];
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000055", @"%@",
[NSString stringWithFormat:@"%@", errorDescription]);
strongSelfQueue->_settings.isFetchInProgress = NO;
return [strongSelfQueue
reportCompletionOnHandler:completionHandler
withStatus:FIRRemoteConfigFetchStatusFailure
withError:[NSError
errorWithDomain:FIRRemoteConfigErrorDomain
code:FIRRemoteConfigErrorInternalError
userInfo:@{
NSLocalizedDescriptionKey : errorDescription
}]];
}
FIRLogInfo(kFIRLoggerRemoteConfig, @"I-RCN000022", @"Success to get iid : %@.",
strongSelfQueue->_settings.configInstallationsIdentifier);
return [strongSelfQueue reportCompletionOnHandler:completionHandler
withStatus:FIRRemoteConfigFetchStatusNoFetchYet
withError:nil];
});
}];
};
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000039", @"Starting requesting token.");
[installations authTokenWithCompletion:installationsTokenHandler];
}
- (void)createRequestBodyWithCompletion:(void (^)(NSData *_Nonnull requestBody))completion {
__weak __typeof(self) weakSelf = self;
[self refreshInstallationsTokenWithCompletionHandler:^(FIRRemoteConfigFetchStatus status,
NSError *_Nullable error) {
__strong __typeof(self) strongSelf = weakSelf;
if (!strongSelf) return;
if (![strongSelf->_settings.configInstallationsIdentifier length]) {
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000013",
@"Installation token retrieval failed. Realtime connection will not include "
@"valid installations token.");
}
[strongSelf.request setValue:strongSelf->_settings.configInstallationsToken
forHTTPHeaderField:kInstallationsAuthTokenHeaderName];
if (strongSelf->_settings.lastETag) {
[strongSelf.request setValue:strongSelf->_settings.lastETag
forHTTPHeaderField:kIfNoneMatchETagHeaderName];
}
NSString *namespace = [strongSelf->_namespace
substringToIndex:[strongSelf->_namespace rangeOfString:@":"].location];
NSString *postBody = [NSString
stringWithFormat:@"{project:'%@', namespace:'%@', lastKnownVersionNumber:'%@', appId:'%@', "
@"sdkVersion:'%@', appInstanceId:'%@'}",
[strongSelf->_options GCMSenderID], namespace,
strongSelf->_configFetch.templateVersionNumber,
strongSelf->_options.googleAppID, FIRRemoteConfigPodVersion(),
strongSelf->_settings.configInstallationsIdentifier];
NSData *postData = [postBody dataUsingEncoding:NSUTF8StringEncoding];
NSError *compressionError;
completion([NSData gul_dataByGzippingData:postData error:&compressionError]);
}];
}
/// Creates request.
- (void)setUpHttpRequest {
NSString *address = [self constructServerURL];
_request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:address]
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:gTimeoutSeconds];
[_request setHTTPMethod:kHTTPMethodPost];
[_request setValue:@"application/json" forHTTPHeaderField:kContentTypeHeaderName];
[_request setValue:@"application/json" forHTTPHeaderField:kAcceptEncodingHeaderName];
[_request setValue:@"gzip" forHTTPHeaderField:kContentEncodingHeaderName];
[_request setValue:@"true" forHTTPHeaderField:@"X-Google-GFE-Can-Retry"];
[_request setValue:[_options APIKey] forHTTPHeaderField:@"X-Goog-Api-Key"];
[_request setValue:[[NSBundle mainBundle] bundleIdentifier]
forHTTPHeaderField:kiOSBundleIdentifierHeaderName];
}
/// Makes call to create session.
- (void)setUpHttpSession {
NSURLSessionConfiguration *sessionConfig =
[[NSURLSessionConfiguration defaultSessionConfiguration] copy];
[sessionConfig setTimeoutIntervalForResource:gTimeoutSeconds];
[sessionConfig setTimeoutIntervalForRequest:gTimeoutSeconds];
_session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:[NSOperationQueue mainQueue]];
}
#pragma mark - Retry Helpers
- (BOOL)canMakeConnection {
BOOL noRunningConnection =
self->_dataTask == nil || self->_dataTask.state != NSURLSessionTaskStateRunning;
BOOL canMakeConnection = noRunningConnection && [self->_listeners count] > 0 &&
!self->_isInBackground && !self->_isRealtimeDisabled;
return canMakeConnection;
}
// Retry mechanism for HTTP connections
- (void)retryHTTPConnection {
__weak RCNConfigRealtime *weakSelf = self;
dispatch_async(_realtimeLockQueue, ^{
__strong RCNConfigRealtime *strongSelf = weakSelf;
if (!strongSelf || strongSelf->_isInBackground) {
return;
}
if ([strongSelf canMakeConnection] && strongSelf->_remainingRetryCount > 0) {
NSTimeInterval backOffInterval = self->_settings.getRealtimeBackoffInterval;
strongSelf->_remainingRetryCount--;
[strongSelf->_settings setRealtimeRetryCount:[strongSelf->_settings realtimeRetryCount] + 1];
dispatch_time_t executionDelay =
dispatch_time(DISPATCH_TIME_NOW, (backOffInterval * NSEC_PER_SEC));
dispatch_after(executionDelay, strongSelf->_realtimeLockQueue, ^{
[strongSelf beginRealtimeStream];
});
} else {
NSError *error = [NSError
errorWithDomain:FIRRemoteConfigUpdateErrorDomain
code:FIRRemoteConfigUpdateErrorStreamError
userInfo:@{
NSLocalizedDescriptionKey :
@"Unable to connect to the server. Check your connection and try again."
}];
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000014", @"Cannot establish connection. Error: %@",
error);
[self propogateErrors:error];
}
});
}
- (void)backgroundChangeListener {
[_notificationCenter addObserver:self
selector:@selector(isInForeground)
name:@"UIApplicationWillEnterForegroundNotification"
object:nil];
[_notificationCenter addObserver:self
selector:@selector(isInBackground)
name:@"UIApplicationDidEnterBackgroundNotification"
object:nil];
}
- (void)isInForeground {
__weak RCNConfigRealtime *weakSelf = self;
dispatch_async(_realtimeLockQueue, ^{
__strong RCNConfigRealtime *strongSelf = weakSelf;
strongSelf->_isInBackground = false;
[strongSelf beginRealtimeStream];
});
}
- (void)isInBackground {
__weak RCNConfigRealtime *weakSelf = self;
dispatch_async(_realtimeLockQueue, ^{
__strong RCNConfigRealtime *strongSelf = weakSelf;
[strongSelf pauseRealtimeStream];
strongSelf->_isInBackground = true;
});
}
#pragma mark - Autofetch Helpers
- (void)fetchLatestConfig:(NSInteger)remainingAttempts targetVersion:(NSInteger)targetVersion {
__weak RCNConfigRealtime *weakSelf = self;
dispatch_async(_realtimeLockQueue, ^{
__strong RCNConfigRealtime *strongSelf = weakSelf;
NSInteger attempts = remainingAttempts - 1;
[strongSelf->_configFetch
realtimeFetchConfigWithNoExpirationDuration:gFetchAttempts - attempts
completionHandler:^(FIRRemoteConfigFetchStatus status,
FIRRemoteConfigUpdate *update,
NSError *error) {
if (error != nil) {
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000010",
@"Failed to retrieve config due to fetch error. "
@"Error: %@",
error);
return [self propogateErrors:error];
}
if (status == FIRRemoteConfigFetchStatusSuccess) {
if ([strongSelf->_configFetch.templateVersionNumber
integerValue] >= targetVersion) {
// only notify listeners if there is a change
if ([update updatedKeys].count > 0) {
for (RCNConfigUpdateCompletion listener in strongSelf
->_listeners) {
listener(update, nil);
}
}
} else {
FIRLogDebug(
kFIRLoggerRemoteConfig, @"I-RCN000016",
@"Fetched config's template version is outdated, "
@"re-fetching");
[strongSelf autoFetch:attempts targetVersion:targetVersion];
}
} else {
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000016",
@"Fetched config's template version is "
@"outdated, re-fetching");
[strongSelf autoFetch:attempts targetVersion:targetVersion];
}
}];
});
}
- (void)scheduleFetch:(NSInteger)remainingAttempts targetVersion:(NSInteger)targetVersion {
/// Needs fetch to occur between 0 - 3 seconds. Randomize to not cause DDoS alerts in backend
dispatch_time_t executionDelay =
dispatch_time(DISPATCH_TIME_NOW, arc4random_uniform(4) * NSEC_PER_SEC);
dispatch_after(executionDelay, _realtimeLockQueue, ^{
[self fetchLatestConfig:remainingAttempts targetVersion:targetVersion];
});
}
/// Perform fetch and handle developers callbacks
- (void)autoFetch:(NSInteger)remainingAttempts targetVersion:(NSInteger)targetVersion {
__weak RCNConfigRealtime *weakSelf = self;
dispatch_async(_realtimeLockQueue, ^{
__strong RCNConfigRealtime *strongSelf = weakSelf;
if (remainingAttempts == 0) {
NSError *error = [NSError errorWithDomain:FIRRemoteConfigUpdateErrorDomain
code:FIRRemoteConfigUpdateErrorNotFetched
userInfo:@{
NSLocalizedDescriptionKey :
@"Unable to fetch the latest version of the template."
}];
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000011",
@"Ran out of fetch attempts, cannot find target config version.");
[self propogateErrors:error];
return;
}
[strongSelf scheduleFetch:remainingAttempts targetVersion:targetVersion];
});
}
#pragma mark - NSURLSession Delegates
- (void)evaluateStreamResponse:(NSDictionary *)response error:(NSError *)dataError {
NSInteger updateTemplateVersion = 1;
if (dataError == nil) {
if ([response objectForKey:kTemplateVersionNumberKey]) {
updateTemplateVersion = [[response objectForKey:kTemplateVersionNumberKey] integerValue];
}
if ([response objectForKey:kIsFeatureDisabled]) {
self->_isRealtimeDisabled = [response objectForKey:kIsFeatureDisabled];
}
if (self->_isRealtimeDisabled) {
[self pauseRealtimeStream];
NSError *error = [NSError
errorWithDomain:FIRRemoteConfigUpdateErrorDomain
code:FIRRemoteConfigUpdateErrorUnavailable
userInfo:@{
NSLocalizedDescriptionKey :
@"The server is temporarily unavailable. Try again in a few minutes."
}];
[self propogateErrors:error];
} else {
NSInteger clientTemplateVersion = [_configFetch.templateVersionNumber integerValue];
if (updateTemplateVersion > clientTemplateVersion) {
[self autoFetch:gFetchAttempts targetVersion:updateTemplateVersion];
}
}
} else {
NSError *error =
[NSError errorWithDomain:FIRRemoteConfigUpdateErrorDomain
code:FIRRemoteConfigUpdateErrorMessageInvalid
userInfo:@{NSLocalizedDescriptionKey : @"Unable to parse ConfigUpdate."}];
[self propogateErrors:error];
}
}
/// Delegate to asynchronously handle every new notification that comes over the wire. Auto-fetches
/// and runs callback for each new notification
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data {
NSError *dataError;
NSString *strData = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
/// If response data contains the API enablement link, return the entire message to the user in
/// the form of a error.
if ([strData containsString:kServerForbiddenStatusCode]) {
NSError *error = [NSError errorWithDomain:FIRRemoteConfigUpdateErrorDomain
code:FIRRemoteConfigUpdateErrorStreamError
userInfo:@{NSLocalizedDescriptionKey : strData}];
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000021", @"Cannot establish connection. %@", error);
[self propogateErrors:error];
return;
}
NSRange endRange = [strData rangeOfString:@"}"];
NSRange beginRange = [strData rangeOfString:@"{"];
if (beginRange.location != NSNotFound && endRange.location != NSNotFound) {
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000015",
@"Received config update message on stream.");
NSRange msgRange =
NSMakeRange(beginRange.location, endRange.location - beginRange.location + 1);
strData = [strData substringWithRange:msgRange];
data = [strData dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *response = [NSJSONSerialization JSONObjectWithData:data
options:NSJSONReadingMutableContainers
error:&dataError];
[self evaluateStreamResponse:response error:dataError];
}
}
/// Check if response code is retryable
- (bool)isStatusCodeRetryable:(NSInteger)statusCode {
return statusCode == kRCNFetchResponseHTTPStatusClientTimeout ||
statusCode == kRCNFetchResponseHTTPStatusTooManyRequests ||
statusCode == kRCNFetchResponseHTTPStatusCodeServiceUnavailable ||
statusCode == kRCNFetchResponseHTTPStatusCodeBadGateway ||
statusCode == kRCNFetchResponseHTTPStatusCodeGatewayTimeout;
}
/// Delegate to handle initial reply from the server
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
_isRequestInProgress = false;
NSHTTPURLResponse *_httpURLResponse = (NSHTTPURLResponse *)response;
NSInteger statusCode = [_httpURLResponse statusCode];
if (statusCode == 403) {
completionHandler(NSURLSessionResponseAllow);
return;
}
if (statusCode != kRCNFetchResponseHTTPStatusOk) {
[self->_settings updateRealtimeExponentialBackoffTime];
[self pauseRealtimeStream];
if ([self isStatusCodeRetryable:statusCode]) {
[self retryHTTPConnection];
} else {
NSError *error = [NSError
errorWithDomain:FIRRemoteConfigUpdateErrorDomain
code:FIRRemoteConfigUpdateErrorStreamError
userInfo:@{
NSLocalizedDescriptionKey :
[NSString stringWithFormat:@"Unable to connect to the server. Try again in "
@"a few minutes. Http Status code: %@",
[@(statusCode) stringValue]]
}];
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000021", @"Cannot establish connection. Error: %@",
error);
}
} else {
/// on success reset retry parameters
_remainingRetryCount = gMaxRetries;
[self->_settings setRealtimeRetryCount:0];
}
completionHandler(NSURLSessionResponseAllow);
}
/// Delegate to handle data task completion
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error {
_isRequestInProgress = false;
if (error != nil && [error code] != NSURLErrorCancelled) {
[self->_settings updateRealtimeExponentialBackoffTime];
}
[self pauseRealtimeStream];
[self retryHTTPConnection];
}
/// Delegate to handle session invalidation
- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(NSError *)error {
if (!_isRequestInProgress) {
if (error != nil) {
[self->_settings updateRealtimeExponentialBackoffTime];
}
[self pauseRealtimeStream];
[self retryHTTPConnection];
}
}
#pragma mark - Top level methods
- (void)beginRealtimeStream {
__weak __typeof(self) weakSelf = self;
dispatch_async(_realtimeLockQueue, ^{
__strong __typeof(self) strongSelf = weakSelf;
if (strongSelf->_settings.getRealtimeBackoffInterval > 0) {
[strongSelf retryHTTPConnection];
return;
}
if ([strongSelf canMakeConnection]) {
__weak __typeof(self) weakSelf = strongSelf;
[strongSelf createRequestBodyWithCompletion:^(NSData *_Nonnull requestBody) {
__strong __typeof(self) strongSelf = weakSelf;
if (!strongSelf) return;
strongSelf->_isRequestInProgress = true;
[strongSelf->_request setHTTPBody:requestBody];
strongSelf->_dataTask = [strongSelf->_session dataTaskWithRequest:strongSelf->_request];
[strongSelf->_dataTask resume];
}];
}
});
}
- (void)pauseRealtimeStream {
__weak RCNConfigRealtime *weakSelf = self;
dispatch_async(_realtimeLockQueue, ^{
__strong RCNConfigRealtime *strongSelf = weakSelf;
if (strongSelf->_dataTask != nil) {
[strongSelf->_dataTask cancel];
strongSelf->_dataTask = nil;
}
});
}
- (FIRConfigUpdateListenerRegistration *)addConfigUpdateListener:
(void (^_Nonnull)(FIRRemoteConfigUpdate *configUpdate, NSError *_Nullable error))listener {
if (listener == nil) {
return nil;
}
__block id listenerCopy = listener;
__weak RCNConfigRealtime *weakSelf = self;
dispatch_async(_realtimeLockQueue, ^{
__strong RCNConfigRealtime *strongSelf = weakSelf;
[strongSelf->_listeners addObject:listenerCopy];
[strongSelf beginRealtimeStream];
});
return [[FIRConfigUpdateListenerRegistration alloc] initWithClient:self
completionHandler:listenerCopy];
}
- (void)removeConfigUpdateListener:(void (^_Nonnull)(FIRRemoteConfigUpdate *configUpdate,
NSError *_Nullable error))listener {
__weak RCNConfigRealtime *weakSelf = self;
dispatch_async(_realtimeLockQueue, ^{
__strong RCNConfigRealtime *strongSelf = weakSelf;
[strongSelf->_listeners removeObject:listener];
if (strongSelf->_listeners.count == 0) {
[strongSelf pauseRealtimeStream];
}
});
}
@end

View File

@ -0,0 +1,537 @@
/*
* 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 "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h"
#import "FirebaseRemoteConfig/Sources/RCNDevice.h"
#import "FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h"
#import <GoogleUtilities/GULAppEnvironmentUtil.h>
#import "FirebaseCore/Extension/FirebaseCoreInternal.h"
static NSString *const kRCNGroupPrefix = @"frc.group.";
static NSString *const kRCNUserDefaultsKeyNamelastETag = @"lastETag";
static NSString *const kRCNUserDefaultsKeyNameLastSuccessfulFetchTime = @"lastSuccessfulFetchTime";
static NSString *const kRCNAnalyticsFirstOpenTimePropertyName = @"_fot";
static const int kRCNExponentialBackoffMinimumInterval = 60 * 2; // 2 mins.
static const int kRCNExponentialBackoffMaximumInterval = 60 * 60 * 4; // 4 hours.
@interface RCNConfigSettings () {
/// A list of successful fetch timestamps in seconds.
NSMutableArray *_successFetchTimes;
/// A list of failed fetch timestamps in seconds.
NSMutableArray *_failureFetchTimes;
/// Device conditions since last successful fetch from the backend. Device conditions including
/// app
/// version, iOS version, device localte, language, GMP project ID and Game project ID. Used for
/// determing whether to throttle.
NSMutableDictionary *_deviceContext;
/// Custom variables (aka App context digest). This is the pending custom variables request before
/// fetching.
NSMutableDictionary *_customVariables;
/// Cached internal metadata from internal metadata table. It contains customized information such
/// as HTTP connection timeout, HTTP read timeout, success/failure throttling rate and time
/// interval. Client has the default value of each parameters, they are only saved in
/// internalMetadata if they have been customize by developers.
NSMutableDictionary *_internalMetadata;
/// Last fetch status.
FIRRemoteConfigFetchStatus _lastFetchStatus;
/// Last fetch Error.
FIRRemoteConfigError _lastFetchError;
/// The time of last apply timestamp.
NSTimeInterval _lastApplyTimeInterval;
/// The time of last setDefaults timestamp.
NSTimeInterval _lastSetDefaultsTimeInterval;
/// The database manager.
RCNConfigDBManager *_DBManager;
// The namespace for this instance.
NSString *_FIRNamespace;
// The Google App ID of the configured FIRApp.
NSString *_googleAppID;
/// The user defaults manager scoped to this RC instance of FIRApp and namespace.
RCNUserDefaultsManager *_userDefaultsManager;
/// The timestamp of last eTag update.
NSTimeInterval _lastETagUpdateTime;
}
@end
@implementation RCNConfigSettings
- (instancetype)initWithDatabaseManager:(RCNConfigDBManager *)manager
namespace:(NSString *)FIRNamespace
firebaseAppName:(NSString *)appName
googleAppID:(NSString *)googleAppID {
self = [super init];
if (self) {
_FIRNamespace = FIRNamespace;
_googleAppID = googleAppID;
_bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
if (!_bundleIdentifier) {
FIRLogNotice(kFIRLoggerRemoteConfig, @"I-RCN000038",
@"Main bundle identifier is missing. Remote Config might not work properly.");
_bundleIdentifier = @"";
}
_minimumFetchInterval = RCNDefaultMinimumFetchInterval;
_deviceContext = [[NSMutableDictionary alloc] init];
_customVariables = [[NSMutableDictionary alloc] init];
_successFetchTimes = [[NSMutableArray alloc] init];
_failureFetchTimes = [[NSMutableArray alloc] init];
_DBManager = manager;
_internalMetadata = [[_DBManager loadInternalMetadataTable] mutableCopy];
if (!_internalMetadata) {
_internalMetadata = [[NSMutableDictionary alloc] init];
}
_userDefaultsManager = [[RCNUserDefaultsManager alloc] initWithAppName:appName
bundleID:_bundleIdentifier
namespace:_FIRNamespace];
// Check if the config database is new. If so, clear the configs saved in userDefaults.
if ([_DBManager isNewDatabase]) {
FIRLogNotice(kFIRLoggerRemoteConfig, @"I-RCN000072",
@"New config database created. Resetting user defaults.");
[_userDefaultsManager resetUserDefaults];
}
_isFetchInProgress = NO;
_lastFetchedTemplateVersion = [_userDefaultsManager lastFetchedTemplateVersion];
_lastActiveTemplateVersion = [_userDefaultsManager lastActiveTemplateVersion];
_realtimeExponentialBackoffRetryInterval =
[_userDefaultsManager currentRealtimeThrottlingRetryIntervalSeconds];
_realtimeExponentialBackoffThrottleEndTime = [_userDefaultsManager realtimeThrottleEndTime];
_realtimeRetryCount = [_userDefaultsManager realtimeRetryCount];
}
return self;
}
#pragma mark - read from / update userDefaults
- (NSString *)lastETag {
return [_userDefaultsManager lastETag];
}
- (void)setLastETag:(NSString *)lastETag {
[self setLastETagUpdateTime:[[NSDate date] timeIntervalSince1970]];
[_userDefaultsManager setLastETag:lastETag];
}
- (void)setLastETagUpdateTime:(NSTimeInterval)lastETagUpdateTime {
[_userDefaultsManager setLastETagUpdateTime:lastETagUpdateTime];
}
- (NSTimeInterval)lastFetchTimeInterval {
return _userDefaultsManager.lastFetchTime;
}
- (NSTimeInterval)lastETagUpdateTime {
return _userDefaultsManager.lastETagUpdateTime;
}
// TODO: Update logic for app extensions as required.
- (void)updateLastFetchTimeInterval:(NSTimeInterval)lastFetchTimeInterval {
_userDefaultsManager.lastFetchTime = lastFetchTimeInterval;
}
#pragma mark - load from DB
- (NSDictionary *)loadConfigFromMetadataTable {
NSDictionary *metadata = [[_DBManager loadMetadataWithBundleIdentifier:_bundleIdentifier
namespace:_FIRNamespace] copy];
if (metadata) {
// TODO: Remove (all metadata in general) once ready to
// migrate to user defaults completely.
if (metadata[RCNKeyDeviceContext]) {
self->_deviceContext = [metadata[RCNKeyDeviceContext] mutableCopy];
}
if (metadata[RCNKeyAppContext]) {
self->_customVariables = [metadata[RCNKeyAppContext] mutableCopy];
}
if (metadata[RCNKeySuccessFetchTime]) {
self->_successFetchTimes = [metadata[RCNKeySuccessFetchTime] mutableCopy];
}
if (metadata[RCNKeyFailureFetchTime]) {
self->_failureFetchTimes = [metadata[RCNKeyFailureFetchTime] mutableCopy];
}
if (metadata[RCNKeyLastFetchStatus]) {
self->_lastFetchStatus =
(FIRRemoteConfigFetchStatus)[metadata[RCNKeyLastFetchStatus] intValue];
}
if (metadata[RCNKeyLastFetchError]) {
self->_lastFetchError = (FIRRemoteConfigError)[metadata[RCNKeyLastFetchError] intValue];
}
if (metadata[RCNKeyLastApplyTime]) {
self->_lastApplyTimeInterval = [metadata[RCNKeyLastApplyTime] doubleValue];
}
if (metadata[RCNKeyLastFetchStatus]) {
self->_lastSetDefaultsTimeInterval = [metadata[RCNKeyLastSetDefaultsTime] doubleValue];
}
}
return metadata;
}
#pragma mark - update DB/cached
// Update internal metadata content to cache and DB.
- (void)updateInternalContentWithResponse:(NSDictionary *)response {
// Remove all the keys with current package name.
[_DBManager deleteRecordWithBundleIdentifier:_bundleIdentifier
namespace:_FIRNamespace
isInternalDB:YES];
for (NSString *key in _internalMetadata.allKeys) {
if ([key hasPrefix:_bundleIdentifier]) {
[_internalMetadata removeObjectForKey:key];
}
}
for (NSString *entry in response) {
NSData *val = [response[entry] dataUsingEncoding:NSUTF8StringEncoding];
NSArray *values = @[ entry, val ];
_internalMetadata[entry] = response[entry];
[self updateInternalMetadataTableWithValues:values];
}
}
- (void)updateInternalMetadataTableWithValues:(NSArray *)values {
[_DBManager insertInternalMetadataTableWithValues:values completionHandler:nil];
}
/// If the last fetch was not successful, update the (exponential backoff) period that we wait until
/// fetching again. Any subsequent fetch requests will be checked and allowed only if past this
/// throttle end time.
- (void)updateExponentialBackoffTime {
// If not in exponential backoff mode, reset the retry interval.
if (_lastFetchStatus == FIRRemoteConfigFetchStatusSuccess) {
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000057",
@"Throttling: Entering exponential backoff mode.");
_exponentialBackoffRetryInterval = kRCNExponentialBackoffMinimumInterval;
} else {
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000057",
@"Throttling: Updating throttling interval.");
// Double the retry interval until we hit the truncated exponential backoff. More info here:
// https://cloud.google.com/storage/docs/exponential-backoff
_exponentialBackoffRetryInterval =
((_exponentialBackoffRetryInterval * 2) < kRCNExponentialBackoffMaximumInterval)
? _exponentialBackoffRetryInterval * 2
: _exponentialBackoffRetryInterval;
}
// Randomize the next retry interval.
int randomPlusMinusInterval = ((arc4random() % 2) == 0) ? -1 : 1;
NSTimeInterval randomizedRetryInterval =
_exponentialBackoffRetryInterval +
(0.5 * _exponentialBackoffRetryInterval * randomPlusMinusInterval);
_exponentialBackoffThrottleEndTime =
[[NSDate date] timeIntervalSince1970] + randomizedRetryInterval;
}
/// If the last Realtime stream attempt was not successful, update the (exponential backoff) period
/// that we wait until trying again. Any subsequent Realtime requests will be checked and allowed
/// only if past this throttle end time.
- (void)updateRealtimeExponentialBackoffTime {
// If there was only one stream attempt before, reset the retry interval.
if (_realtimeRetryCount == 0) {
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000058",
@"Throttling: Entering exponential Realtime backoff mode.");
_realtimeExponentialBackoffRetryInterval = kRCNExponentialBackoffMinimumInterval;
} else {
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000058",
@"Throttling: Updating Realtime throttling interval.");
// Double the retry interval until we hit the truncated exponential backoff. More info here:
// https://cloud.google.com/storage/docs/exponential-backoff
_realtimeExponentialBackoffRetryInterval =
((_realtimeExponentialBackoffRetryInterval * 2) < kRCNExponentialBackoffMaximumInterval)
? _realtimeExponentialBackoffRetryInterval * 2
: _realtimeExponentialBackoffRetryInterval;
}
// Randomize the next retry interval.
int randomPlusMinusInterval = ((arc4random() % 2) == 0) ? -1 : 1;
NSTimeInterval randomizedRetryInterval =
_realtimeExponentialBackoffRetryInterval +
(0.5 * _realtimeExponentialBackoffRetryInterval * randomPlusMinusInterval);
_realtimeExponentialBackoffThrottleEndTime =
[[NSDate date] timeIntervalSince1970] + randomizedRetryInterval;
[_userDefaultsManager setRealtimeThrottleEndTime:_realtimeExponentialBackoffThrottleEndTime];
[_userDefaultsManager
setCurrentRealtimeThrottlingRetryIntervalSeconds:_realtimeExponentialBackoffRetryInterval];
}
- (void)setRealtimeRetryCount:(int)realtimeRetryCount {
_realtimeRetryCount = realtimeRetryCount;
[_userDefaultsManager setRealtimeRetryCount:_realtimeRetryCount];
}
- (NSTimeInterval)getRealtimeBackoffInterval {
NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
return _realtimeExponentialBackoffThrottleEndTime - now;
}
- (void)updateMetadataWithFetchSuccessStatus:(BOOL)fetchSuccess
templateVersion:(NSString *)templateVersion {
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000056", @"Updating metadata with fetch result.");
[self updateFetchTimeWithSuccessFetch:fetchSuccess];
_lastFetchStatus =
fetchSuccess ? FIRRemoteConfigFetchStatusSuccess : FIRRemoteConfigFetchStatusFailure;
_lastFetchError = fetchSuccess ? FIRRemoteConfigErrorUnknown : FIRRemoteConfigErrorInternalError;
if (fetchSuccess) {
[self updateLastFetchTimeInterval:[[NSDate date] timeIntervalSince1970]];
// Note: We expect the googleAppID to always be available.
_deviceContext = FIRRemoteConfigDeviceContextWithProjectIdentifier(_googleAppID);
_lastFetchedTemplateVersion = templateVersion;
[_userDefaultsManager setLastFetchedTemplateVersion:templateVersion];
}
[self updateMetadataTable];
}
- (void)updateFetchTimeWithSuccessFetch:(BOOL)isSuccessfulFetch {
NSTimeInterval epochTimeInterval = [[NSDate date] timeIntervalSince1970];
if (isSuccessfulFetch) {
[_successFetchTimes addObject:@(epochTimeInterval)];
} else {
[_failureFetchTimes addObject:@(epochTimeInterval)];
}
}
- (void)updateMetadataTable {
[_DBManager deleteRecordWithBundleIdentifier:_bundleIdentifier
namespace:_FIRNamespace
isInternalDB:NO];
NSError *error;
// Objects to be serialized cannot be invalid.
if (!_bundleIdentifier) {
return;
}
if (![NSJSONSerialization isValidJSONObject:_customVariables]) {
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000028",
@"Invalid custom variables to be serialized.");
return;
}
if (![NSJSONSerialization isValidJSONObject:_deviceContext]) {
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000029",
@"Invalid device context to be serialized.");
return;
}
if (![NSJSONSerialization isValidJSONObject:_successFetchTimes]) {
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000031",
@"Invalid success fetch times to be serialized.");
return;
}
if (![NSJSONSerialization isValidJSONObject:_failureFetchTimes]) {
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000032",
@"Invalid failure fetch times to be serialized.");
return;
}
NSData *serializedAppContext = [NSJSONSerialization dataWithJSONObject:_customVariables
options:NSJSONWritingPrettyPrinted
error:&error];
NSData *serializedDeviceContext =
[NSJSONSerialization dataWithJSONObject:_deviceContext
options:NSJSONWritingPrettyPrinted
error:&error];
// The digestPerNamespace is not used and only meant for backwards DB compatibility.
NSData *serializedDigestPerNamespace =
[NSJSONSerialization dataWithJSONObject:@{} options:NSJSONWritingPrettyPrinted error:&error];
NSData *serializedSuccessTime = [NSJSONSerialization dataWithJSONObject:_successFetchTimes
options:NSJSONWritingPrettyPrinted
error:&error];
NSData *serializedFailureTime = [NSJSONSerialization dataWithJSONObject:_failureFetchTimes
options:NSJSONWritingPrettyPrinted
error:&error];
if (!serializedDigestPerNamespace || !serializedDeviceContext || !serializedAppContext ||
!serializedSuccessTime || !serializedFailureTime) {
return;
}
NSDictionary *columnNameToValue = @{
RCNKeyBundleIdentifier : _bundleIdentifier,
RCNKeyNamespace : _FIRNamespace,
RCNKeyFetchTime : @(self.lastFetchTimeInterval),
RCNKeyDigestPerNamespace : serializedDigestPerNamespace,
RCNKeyDeviceContext : serializedDeviceContext,
RCNKeyAppContext : serializedAppContext,
RCNKeySuccessFetchTime : serializedSuccessTime,
RCNKeyFailureFetchTime : serializedFailureTime,
RCNKeyLastFetchStatus : [NSString stringWithFormat:@"%ld", (long)_lastFetchStatus],
RCNKeyLastFetchError : [NSString stringWithFormat:@"%ld", (long)_lastFetchError],
RCNKeyLastApplyTime : @(_lastApplyTimeInterval),
RCNKeyLastSetDefaultsTime : @(_lastSetDefaultsTimeInterval)
};
[_DBManager insertMetadataTableWithValues:columnNameToValue completionHandler:nil];
}
- (void)updateLastActiveTemplateVersion {
_lastActiveTemplateVersion = _lastFetchedTemplateVersion;
[_userDefaultsManager setLastActiveTemplateVersion:_lastActiveTemplateVersion];
}
#pragma mark - fetch request
/// Returns a fetch request with the latest device and config change.
/// Whenever user issues a fetch api call, collect the latest request.
- (NSString *)nextRequestWithUserProperties:(NSDictionary *)userProperties {
// Note: We only set user properties as mentioned in the new REST API Design doc
NSString *ret = [NSString stringWithFormat:@"{"];
ret = [ret stringByAppendingString:[NSString stringWithFormat:@"app_instance_id:'%@'",
_configInstallationsIdentifier]];
ret = [ret stringByAppendingString:[NSString stringWithFormat:@", app_instance_id_token:'%@'",
_configInstallationsToken]];
ret = [ret stringByAppendingString:[NSString stringWithFormat:@", app_id:'%@'", _googleAppID]];
ret = [ret stringByAppendingString:[NSString stringWithFormat:@", country_code:'%@'",
FIRRemoteConfigDeviceCountry()]];
ret = [ret stringByAppendingString:[NSString stringWithFormat:@", language_code:'%@'",
FIRRemoteConfigDeviceLocale()]];
ret = [ret
stringByAppendingString:[NSString stringWithFormat:@", platform_version:'%@'",
[GULAppEnvironmentUtil systemVersion]]];
ret = [ret stringByAppendingString:[NSString stringWithFormat:@", time_zone:'%@'",
FIRRemoteConfigTimezone()]];
ret = [ret stringByAppendingString:[NSString stringWithFormat:@", package_name:'%@'",
_bundleIdentifier]];
ret = [ret stringByAppendingString:[NSString stringWithFormat:@", app_version:'%@'",
FIRRemoteConfigAppVersion()]];
ret = [ret stringByAppendingString:[NSString stringWithFormat:@", app_build:'%@'",
FIRRemoteConfigAppBuildVersion()]];
ret = [ret stringByAppendingString:[NSString stringWithFormat:@", sdk_version:'%@'",
FIRRemoteConfigPodVersion()]];
if (userProperties && userProperties.count > 0) {
NSError *error;
// Extract first open time from user properties and send as a separate field
NSNumber *firstOpenTime = userProperties[kRCNAnalyticsFirstOpenTimePropertyName];
NSMutableDictionary *remainingUserProperties = [userProperties mutableCopy];
if (firstOpenTime != nil) {
NSDate *date = [NSDate dateWithTimeIntervalSince1970:([firstOpenTime longValue] / 1000)];
NSISO8601DateFormatter *formatter = [[NSISO8601DateFormatter alloc] init];
NSString *firstOpenTimeISOString = [formatter stringFromDate:date];
ret = [ret stringByAppendingString:[NSString stringWithFormat:@", first_open_time:'%@'",
firstOpenTimeISOString]];
[remainingUserProperties removeObjectForKey:kRCNAnalyticsFirstOpenTimePropertyName];
}
if (remainingUserProperties.count > 0) {
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:remainingUserProperties
options:0
error:&error];
if (!error) {
ret = [ret
stringByAppendingString:[NSString
stringWithFormat:@", analytics_user_properties:%@",
[[NSString alloc]
initWithData:jsonData
encoding:NSUTF8StringEncoding]]];
}
}
}
ret = [ret stringByAppendingString:@"}"];
return ret;
}
#pragma mark - getter/setter
- (void)setLastFetchError:(FIRRemoteConfigError)lastFetchError {
if (_lastFetchError != lastFetchError) {
_lastFetchError = lastFetchError;
[_DBManager updateMetadataWithOption:RCNUpdateOptionFetchStatus
namespace:_FIRNamespace
values:@[ @(_lastFetchStatus), @(_lastFetchError) ]
completionHandler:nil];
}
}
- (NSArray *)successFetchTimes {
return [_successFetchTimes copy];
}
- (NSArray *)failureFetchTimes {
return [_failureFetchTimes copy];
}
- (NSDictionary *)customVariables {
return [_customVariables copy];
}
- (NSDictionary *)internalMetadata {
return [_internalMetadata copy];
}
- (NSDictionary *)deviceContext {
return [_deviceContext copy];
}
- (void)setCustomVariables:(NSDictionary *)customVariables {
_customVariables = [[NSMutableDictionary alloc] initWithDictionary:customVariables];
[self updateMetadataTable];
}
- (void)setMinimumFetchInterval:(NSTimeInterval)minimumFetchInterval {
if (minimumFetchInterval < 0) {
_minimumFetchInterval = 0;
} else {
_minimumFetchInterval = minimumFetchInterval;
}
}
- (void)setFetchTimeout:(NSTimeInterval)fetchTimeout {
if (fetchTimeout <= 0) {
_fetchTimeout = RCNHTTPDefaultConnectionTimeout;
} else {
_fetchTimeout = fetchTimeout;
}
}
- (void)setLastApplyTimeInterval:(NSTimeInterval)lastApplyTimestamp {
_lastApplyTimeInterval = lastApplyTimestamp;
[_DBManager updateMetadataWithOption:RCNUpdateOptionApplyTime
namespace:_FIRNamespace
values:@[ @(lastApplyTimestamp) ]
completionHandler:nil];
}
- (void)setLastSetDefaultsTimeInterval:(NSTimeInterval)lastSetDefaultsTimestamp {
_lastSetDefaultsTimeInterval = lastSetDefaultsTimestamp;
[_DBManager updateMetadataWithOption:RCNUpdateOptionDefaultTime
namespace:_FIRNamespace
values:@[ @(lastSetDefaultsTimestamp) ]
completionHandler:nil];
}
#pragma mark Throttling
- (BOOL)hasMinimumFetchIntervalElapsed:(NSTimeInterval)minimumFetchInterval {
if (self.lastFetchTimeInterval == 0) return YES;
// Check if last config fetch is within minimum fetch interval in seconds.
NSTimeInterval diffInSeconds = [[NSDate date] timeIntervalSince1970] - self.lastFetchTimeInterval;
return diffInSeconds > minimumFetchInterval;
}
- (BOOL)shouldThrottle {
NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
return ((self.lastFetchTimeInterval > 0) &&
(_lastFetchStatus != FIRRemoteConfigFetchStatusSuccess) &&
(_exponentialBackoffThrottleEndTime - now > 0));
}
@end

View File

@ -0,0 +1,25 @@
/*
* 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 "FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h"
@interface FIRRemoteConfigValue ()
@property(nonatomic, readwrite, assign) FIRRemoteConfigSource source;
/// Designated initializer.
- (instancetype)initWithData:(NSData *)data
source:(FIRRemoteConfigSource)source NS_DESIGNATED_INITIALIZER;
@end

View File

@ -0,0 +1,21 @@
/*
* 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 "FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h"
/// Firebase Remote Config service default namespace.
/// TODO(doudounan): Change to use this namespace defined in RemoteConfigInterop.
NSString *const FIRNamespaceGoogleMobilePlatform = @"firebase";

View File

@ -0,0 +1,57 @@
/*
* 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>
typedef NS_ENUM(NSInteger, RCNDeviceModel) {
RCNDeviceModelOther,
RCNDeviceModelPhone,
RCNDeviceModelTablet,
RCNDeviceModelTV,
RCNDeviceModelGlass,
RCNDeviceModelCar,
RCNDeviceModelWearable,
};
/// CocoaPods SDK version
NSString *FIRRemoteConfigPodVersion(void);
/// App version.
NSString *FIRRemoteConfigAppVersion(void);
/// App build version
NSString *FIRRemoteConfigAppBuildVersion(void);
/// Device country, in lowercase.
NSString *FIRRemoteConfigDeviceCountry(void);
/// Device locale, in language_country format, e.g. en_US.
NSString *FIRRemoteConfigDeviceLocale(void);
/// Device subtype.
RCNDeviceModel FIRRemoteConfigDeviceSubtype(void);
/// Device timezone.
NSString *FIRRemoteConfigTimezone(void);
/// Update device context to the given dictionary.
NSMutableDictionary *FIRRemoteConfigDeviceContextWithProjectIdentifier(
NSString *GMPProjectIdentifier);
/// Check whether client has changed device context, including app version,
/// iOS version, device country etc. This is used to determine whether to throttle.
BOOL FIRRemoteConfigHasDeviceContextChanged(NSDictionary *deviceContext,
NSString *GMPProjectIdentifier);

View File

@ -0,0 +1,241 @@
/*
* 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 "FirebaseRemoteConfig/Sources/RCNDevice.h"
#import <sys/utsname.h>
#import <GoogleUtilities/GULAppEnvironmentUtil.h>
#import "FirebaseCore/Extension/FirebaseCoreInternal.h"
#import "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h"
#define STR(x) STR_EXPAND(x)
#define STR_EXPAND(x) #x
static NSString *const RCNDeviceContextKeyVersion = @"app_version";
static NSString *const RCNDeviceContextKeyBuild = @"app_build";
static NSString *const RCNDeviceContextKeyOSVersion = @"os_version";
static NSString *const RCNDeviceContextKeyDeviceLocale = @"device_locale";
static NSString *const RCNDeviceContextKeyLocaleLanguage = @"locale_language";
static NSString *const RCNDeviceContextKeyGMPProjectIdentifier = @"GMP_project_Identifier";
NSString *FIRRemoteConfigAppVersion(void) {
return [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"];
}
NSString *FIRRemoteConfigAppBuildVersion(void) {
return [[NSBundle mainBundle] infoDictionary][@"CFBundleVersion"];
}
NSString *FIRRemoteConfigPodVersion(void) {
return FIRFirebaseVersion();
}
RCNDeviceModel FIRRemoteConfigDeviceSubtype(void) {
NSString *model = [GULAppEnvironmentUtil deviceModel];
if ([model hasPrefix:@"iPhone"]) {
return RCNDeviceModelPhone;
}
if ([model isEqualToString:@"iPad"]) {
return RCNDeviceModelTablet;
}
return RCNDeviceModelOther;
}
NSString *FIRRemoteConfigDeviceCountry(void) {
return [[[NSLocale currentLocale] objectForKey:NSLocaleCountryCode] lowercaseString];
}
NSDictionary<NSString *, NSArray *> *FIRRemoteConfigFirebaseLocaleMap(void) {
return @{
// Albanian
@"sq" : @[ @"sq_AL" ],
// Belarusian
@"be" : @[ @"be_BY" ],
// Bulgarian
@"bg" : @[ @"bg_BG" ],
// Catalan
@"ca" : @[ @"ca", @"ca_ES" ],
// Croatian
@"hr" : @[ @"hr", @"hr_HR" ],
// Czech
@"cs" : @[ @"cs", @"cs_CZ" ],
// Danish
@"da" : @[ @"da", @"da_DK" ],
// Estonian
@"et" : @[ @"et_EE" ],
// Finnish
@"fi" : @[ @"fi", @"fi_FI" ],
// Hebrew
@"he" : @[ @"he", @"iw_IL" ],
// Hindi
@"hi" : @[ @"hi_IN" ],
// Hungarian
@"hu" : @[ @"hu", @"hu_HU" ],
// Icelandic
@"is" : @[ @"is_IS" ],
// Indonesian
@"id" : @[ @"id", @"in_ID", @"id_ID" ],
// Irish
@"ga" : @[ @"ga_IE" ],
// Korean
@"ko" : @[ @"ko", @"ko_KR", @"ko-KR" ],
// Latvian
@"lv" : @[ @"lv_LV" ],
// Lithuanian
@"lt" : @[ @"lt_LT" ],
// Macedonian
@"mk" : @[ @"mk_MK" ],
// Malay
@"ms" : @[ @"ms_MY" ],
// Maltese
@"mt" : @[ @"mt_MT" ],
// Polish
@"pl" : @[ @"pl", @"pl_PL", @"pl-PL" ],
// Romanian
@"ro" : @[ @"ro", @"ro_RO" ],
// Russian
@"ru" : @[ @"ru_RU", @"ru", @"ru_BY", @"ru_KZ", @"ru-RU" ],
// Slovak
@"sk" : @[ @"sk", @"sk_SK" ],
// Slovenian
@"sl" : @[ @"sl_SI" ],
// Swedish
@"sv" : @[ @"sv", @"sv_SE", @"sv-SE" ],
// Turkish
@"tr" : @[ @"tr", @"tr-TR", @"tr_TR" ],
// Ukrainian
@"uk" : @[ @"uk", @"uk_UA" ],
// Vietnamese
@"vi" : @[ @"vi", @"vi_VN" ],
// The following are groups of locales or locales that sub-divide a
// language).
// Arabic
@"ar" : @[
@"ar", @"ar_DZ", @"ar_BH", @"ar_EG", @"ar_IQ", @"ar_JO", @"ar_KW",
@"ar_LB", @"ar_LY", @"ar_MA", @"ar_OM", @"ar_QA", @"ar_SA", @"ar_SD",
@"ar_SY", @"ar_TN", @"ar_AE", @"ar_YE", @"ar_GB", @"ar-IQ", @"ar_US"
],
// Simplified Chinese
@"zh_Hans" : @[ @"zh_CN", @"zh_SG", @"zh-Hans" ],
// Traditional Chinese
// Remove zh_HK until console added to the list. Otherwise client sends
// zh_HK and server/console falls back to zh.
// @"zh_Hant" : @[ @"zh_HK", @"zh_TW", @"zh-Hant", @"zh-HK", @"zh-TW" ],
@"zh_Hant" : @[ @"zh_TW", @"zh-Hant", @"zh-TW" ],
// Dutch
@"nl" : @[ @"nl", @"nl_BE", @"nl_NL", @"nl-NL" ],
// English
@"en" : @[
@"en", @"en_AU", @"en_CA", @"en_IN", @"en_IE", @"en_MT", @"en_NZ", @"en_PH",
@"en_SG", @"en_ZA", @"en_GB", @"en_US", @"en_AE", @"en-AE", @"en_AS", @"en-AU",
@"en_BD", @"en-CA", @"en_EG", @"en_ES", @"en_GB", @"en-GB", @"en_HK", @"en_ID",
@"en-IN", @"en_NG", @"en-PH", @"en_PK", @"en-SG", @"en-US"
],
// French
@"fr" :
@[ @"fr", @"fr_BE", @"fr_CA", @"fr_FR", @"fr_LU", @"fr_CH", @"fr-CA", @"fr-FR", @"fr_MA" ],
// German
@"de" : @[ @"de", @"de_AT", @"de_DE", @"de_LU", @"de_CH", @"de-DE" ],
// Greek
@"el" : @[ @"el", @"el_CY", @"el_GR" ],
// Italian
@"it" : @[ @"it", @"it_IT", @"it_CH", @"it-IT" ],
// Japanese
@"ja" : @[ @"ja", @"ja_JP", @"ja_JP_JP", @"ja-JP" ],
// Norwegian
@"no" : @[ @"nb", @"no_NO", @"no_NO_NY", @"nb_NO" ],
// Brazilian Portuguese
@"pt_BR" : @[ @"pt_BR", @"pt-BR" ],
// European Portuguese
@"pt_PT" : @[ @"pt", @"pt_PT", @"pt-PT" ],
// Serbian
@"sr" : @[ @"sr_BA", @"sr_ME", @"sr_RS", @"sr_Latn_BA", @"sr_Latn_ME", @"sr_Latn_RS" ],
// European Spanish
@"es_ES" : @[ @"es", @"es_ES", @"es-ES" ],
// Mexican Spanish
@"es_MX" : @[ @"es-MX", @"es_MX", @"es_US", @"es-US" ],
// Latin American Spanish
@"es_419" : @[
@"es_AR", @"es_BO", @"es_CL", @"es_CO", @"es_CR", @"es_DO", @"es_EC",
@"es_SV", @"es_GT", @"es_HN", @"es_NI", @"es_PA", @"es_PY", @"es_PE",
@"es_PR", @"es_UY", @"es_VE", @"es-AR", @"es-CL", @"es-CO"
],
// Thai
@"th" : @[ @"th", @"th_TH", @"th_TH_TH" ],
};
}
NSArray<NSString *> *FIRRemoteConfigAppManagerLocales(void) {
NSMutableArray *locales = [NSMutableArray array];
NSDictionary<NSString *, NSArray *> *localesMap = FIRRemoteConfigFirebaseLocaleMap();
for (NSString *key in localesMap) {
[locales addObjectsFromArray:localesMap[key]];
}
return locales;
}
NSString *FIRRemoteConfigDeviceLocale(void) {
NSArray<NSString *> *locales = FIRRemoteConfigAppManagerLocales();
NSArray<NSString *> *preferredLocalizations =
[NSBundle preferredLocalizationsFromArray:locales
forPreferences:[NSLocale preferredLanguages]];
NSString *legalDocsLanguage = [preferredLocalizations firstObject];
// Use en as the default language
return legalDocsLanguage ? legalDocsLanguage : @"en";
}
NSString *FIRRemoteConfigTimezone(void) {
NSTimeZone *timezone = [NSTimeZone systemTimeZone];
return timezone.name;
}
NSMutableDictionary *FIRRemoteConfigDeviceContextWithProjectIdentifier(
NSString *GMPProjectIdentifier) {
NSMutableDictionary *deviceContext = [[NSMutableDictionary alloc] init];
deviceContext[RCNDeviceContextKeyVersion] = FIRRemoteConfigAppVersion();
deviceContext[RCNDeviceContextKeyBuild] = FIRRemoteConfigAppBuildVersion();
deviceContext[RCNDeviceContextKeyOSVersion] = [GULAppEnvironmentUtil systemVersion];
deviceContext[RCNDeviceContextKeyDeviceLocale] = FIRRemoteConfigDeviceLocale();
// NSDictionary setObjectForKey will fail if there's no GMP project ID, must check ahead.
if (GMPProjectIdentifier) {
deviceContext[RCNDeviceContextKeyGMPProjectIdentifier] = GMPProjectIdentifier;
}
return deviceContext;
}
BOOL FIRRemoteConfigHasDeviceContextChanged(NSDictionary *deviceContext,
NSString *GMPProjectIdentifier) {
if (![deviceContext[RCNDeviceContextKeyVersion] isEqual:FIRRemoteConfigAppVersion()]) {
return YES;
}
if (![deviceContext[RCNDeviceContextKeyBuild] isEqual:FIRRemoteConfigAppBuildVersion()]) {
return YES;
}
if (![deviceContext[RCNDeviceContextKeyOSVersion]
isEqual:[GULAppEnvironmentUtil systemVersion]]) {
return YES;
}
if (![deviceContext[RCNDeviceContextKeyDeviceLocale] isEqual:FIRRemoteConfigDeviceLocale()]) {
return YES;
}
// GMP project id is optional.
if (deviceContext[RCNDeviceContextKeyGMPProjectIdentifier] &&
![deviceContext[RCNDeviceContextKeyGMPProjectIdentifier] isEqual:GMPProjectIdentifier]) {
return YES;
}
return NO;
}

View File

@ -0,0 +1,56 @@
/*
* 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 "Interop/Analytics/Public/FIRAnalyticsInterop.h"
NS_ASSUME_NONNULL_BEGIN
static NSString *const kAnalyticsOriginPersonalization = @"fp";
static NSString *const kExternalEvent = @"personalization_assignment";
static NSString *const kExternalRcParameterParam = @"arm_key";
static NSString *const kExternalArmValueParam = @"arm_value";
static NSString *const kPersonalizationId = @"personalizationId";
static NSString *const kExternalPersonalizationIdParam = @"personalization_id";
static NSString *const kArmIndex = @"armIndex";
static NSString *const kExternalArmIndexParam = @"arm_index";
static NSString *const kGroup = @"group";
static NSString *const kExternalGroupParam = @"group";
static NSString *const kInternalEvent = @"_fpc";
static NSString *const kChoiceId = @"choiceId";
static NSString *const kInternalChoiceIdParam = @"_fpid";
@interface RCNPersonalization : NSObject
/// Analytics connector
@property(nonatomic, strong) id<FIRAnalyticsInterop> _Nullable analytics;
@property(atomic, strong) NSMutableDictionary *loggedChoiceIds;
- (instancetype)init NS_UNAVAILABLE;
/// Designated initializer.
- (instancetype)initWithAnalytics:(id<FIRAnalyticsInterop> _Nullable)analytics
NS_DESIGNATED_INITIALIZER;
/// Called when an arm is pulled from Remote Config. If the arm is personalized, log information to
/// Google in another thread.
- (void)logArmActive:(NSString *)rcParameter config:(NSDictionary *)config;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,72 @@
/*
* 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 "FirebaseRemoteConfig/Sources/RCNPersonalization.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h"
@implementation RCNPersonalization
- (instancetype)initWithAnalytics:(id<FIRAnalyticsInterop> _Nullable)analytics {
self = [super init];
if (self) {
self->_analytics = analytics;
self->_loggedChoiceIds = [[NSMutableDictionary alloc] init];
}
return self;
}
- (void)logArmActive:(NSString *)rcParameter config:(NSDictionary *)config {
NSDictionary *ids = config[RCNFetchResponseKeyPersonalizationMetadata];
NSDictionary<NSString *, FIRRemoteConfigValue *> *values = config[RCNFetchResponseKeyEntries];
if (ids.count < 1 || values.count < 1 || !values[rcParameter]) {
return;
}
NSDictionary *metadata = ids[rcParameter];
if (!metadata) {
return;
}
NSString *choiceId = metadata[kChoiceId];
if (choiceId == nil) {
return;
}
// Listeners like logArmActive() are dispatched to a serial queue, so loggedChoiceIds should
// contain any previously logged RC parameter / choice ID pairs.
if (self->_loggedChoiceIds[rcParameter] == choiceId) {
return;
}
self->_loggedChoiceIds[rcParameter] = choiceId;
[self->_analytics logEventWithOrigin:kAnalyticsOriginPersonalization
name:kExternalEvent
parameters:@{
kExternalRcParameterParam : rcParameter,
kExternalArmValueParam : values[rcParameter].stringValue,
kExternalPersonalizationIdParam : metadata[kPersonalizationId],
kExternalArmIndexParam : metadata[kArmIndex],
kExternalGroupParam : metadata[kGroup]
}];
[self->_analytics logEventWithOrigin:kAnalyticsOriginPersonalization
name:kInternalEvent
parameters:@{kInternalChoiceIdParam : choiceId}];
}
@end

View File

@ -0,0 +1,66 @@
/*
* 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 RCNUserDefaultsManager : NSObject
/// The last eTag received from the backend.
@property(nonatomic, assign) NSString *lastETag;
/// The time of the last eTag update.
@property(nonatomic, assign) NSTimeInterval lastETagUpdateTime;
/// The time of the last successful fetch.
@property(nonatomic, assign) NSTimeInterval lastFetchTime;
/// The time of the last successful fetch.
@property(nonatomic, assign) NSString *lastFetchStatus;
/// Boolean indicating if the last (one or more) fetch(es) was/were unsuccessful, in which case we
/// are in an exponential backoff mode.
@property(nonatomic, assign) BOOL isClientThrottledWithExponentialBackoff;
/// Time when the next request can be made while being throttled.
@property(nonatomic, assign) NSTimeInterval throttleEndTime;
/// The retry interval increases exponentially for cumulative fetch failures. Refer to
/// go/rc-client-throttling for details.
@property(nonatomic, assign) NSTimeInterval currentThrottlingRetryIntervalSeconds;
/// Time when the next request can be made while being throttled.
@property(nonatomic, assign) NSTimeInterval realtimeThrottleEndTime;
/// The retry interval increases exponentially for cumulative Realtime failures. Refer to
/// go/rc-client-throttling for details.
@property(nonatomic, assign) NSTimeInterval currentRealtimeThrottlingRetryIntervalSeconds;
/// Realtime retry count.
@property(nonatomic, assign) int realtimeRetryCount;
/// Last fetched template version.
@property(nonatomic, assign) NSString *lastFetchedTemplateVersion;
/// Last active template version.
@property(nonatomic, assign) NSString *lastActiveTemplateVersion;
/// Designated initializer.
- (instancetype)initWithAppName:(NSString *)appName
bundleID:(NSString *)bundleIdentifier
namespace:(NSString *)firebaseNamespace NS_DESIGNATED_INITIALIZER;
// NOLINTBEGIN
/// Use `initWithAppName:bundleID:namespace:` instead.
- (instancetype)init
__attribute__((unavailable("Use `initWithAppName:bundleID:namespace:` instead.")));
// NOLINTEND
/// Delete all saved userdefaults for this instance.
- (void)resetUserDefaults;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,313 @@
/*
* 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 "FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h"
#import "FirebaseCore/Extension/FirebaseCoreInternal.h"
#import "FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h"
#import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h"
static NSString *const kRCNGroupPrefix = @"group";
static NSString *const kRCNGroupSuffix = @"firebase";
static NSString *const kRCNUserDefaultsKeyNamelastETag = @"lastETag";
static NSString *const kRCNUserDefaultsKeyNamelastETagUpdateTime = @"lastETagUpdateTime";
static NSString *const kRCNUserDefaultsKeyNameLastSuccessfulFetchTime = @"lastSuccessfulFetchTime";
static NSString *const kRCNUserDefaultsKeyNamelastFetchStatus = @"lastFetchStatus";
static NSString *const kRCNUserDefaultsKeyNameIsClientThrottled =
@"isClientThrottledWithExponentialBackoff";
static NSString *const kRCNUserDefaultsKeyNameThrottleEndTime = @"throttleEndTime";
static NSString *const kRCNUserDefaultsKeyNamecurrentThrottlingRetryInterval =
@"currentThrottlingRetryInterval";
static NSString *const kRCNUserDefaultsKeyNameRealtimeThrottleEndTime = @"throttleRealtimeEndTime";
static NSString *const kRCNUserDefaultsKeyNameCurrentRealtimeThrottlingRetryInterval =
@"currentRealtimeThrottlingRetryInterval";
static NSString *const kRCNUserDefaultsKeyNameRealtimeRetryCount = @"realtimeRetryCount";
@interface RCNUserDefaultsManager () {
/// User Defaults instance for this bundleID. NSUserDefaults is guaranteed to be thread-safe.
NSUserDefaults *_userDefaults;
/// The suite name for this user defaults instance. It is a combination of a prefix and the
/// bundleID. This is because you cannot use just the bundleID of the current app as the suite
/// name when initializing user defaults.
NSString *_userDefaultsSuiteName;
/// The FIRApp that this instance is scoped within.
NSString *_firebaseAppName;
/// The Firebase Namespace that this instance is scoped within.
NSString *_firebaseNamespace;
/// The bundleID of the app. In case of an extension, this will be the bundleID of the parent app.
NSString *_bundleIdentifier;
}
@end
@implementation RCNUserDefaultsManager
#pragma mark Initializers.
/// Designated initializer.
- (instancetype)initWithAppName:(NSString *)appName
bundleID:(NSString *)bundleIdentifier
namespace:(NSString *)firebaseNamespace {
self = [super init];
if (self) {
_firebaseAppName = appName;
_bundleIdentifier = bundleIdentifier;
NSInteger location = [firebaseNamespace rangeOfString:@":"].location;
if (location == NSNotFound) {
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000064",
@"Error: Namespace %@ is not fully qualified app:namespace.", firebaseNamespace);
_firebaseNamespace = firebaseNamespace;
} else {
_firebaseNamespace = [firebaseNamespace substringToIndex:location];
}
// Initialize the user defaults with a prefix and the bundleID. For app extensions, this will be
// the bundleID of the app extension.
_userDefaults =
[RCNUserDefaultsManager sharedUserDefaultsForBundleIdentifier:_bundleIdentifier];
}
return self;
}
+ (NSUserDefaults *)sharedUserDefaultsForBundleIdentifier:(NSString *)bundleIdentifier {
static dispatch_once_t onceToken;
static NSUserDefaults *sharedInstance;
dispatch_once(&onceToken, ^{
NSString *userDefaultsSuiteName =
[RCNUserDefaultsManager userDefaultsSuiteNameForBundleIdentifier:bundleIdentifier];
sharedInstance = [[NSUserDefaults alloc] initWithSuiteName:userDefaultsSuiteName];
});
return sharedInstance;
}
+ (NSString *)userDefaultsSuiteNameForBundleIdentifier:(NSString *)bundleIdentifier {
NSString *suiteName =
[NSString stringWithFormat:@"%@.%@.%@", kRCNGroupPrefix, bundleIdentifier, kRCNGroupSuffix];
return suiteName;
}
#pragma mark Public properties.
- (NSString *)lastETag {
return [[self instanceUserDefaults] objectForKey:kRCNUserDefaultsKeyNamelastETag];
}
- (void)setLastETag:(NSString *)lastETag {
if (lastETag) {
[self setInstanceUserDefaultsValue:lastETag forKey:kRCNUserDefaultsKeyNamelastETag];
}
}
- (NSString *)lastFetchedTemplateVersion {
NSDictionary *userDefaults = [self instanceUserDefaults];
if ([userDefaults objectForKey:RCNFetchResponseKeyTemplateVersion]) {
return [userDefaults objectForKey:RCNFetchResponseKeyTemplateVersion];
}
return @"0";
}
- (void)setLastFetchedTemplateVersion:(NSString *)templateVersion {
if (templateVersion) {
[self setInstanceUserDefaultsValue:templateVersion forKey:RCNFetchResponseKeyTemplateVersion];
}
}
- (NSString *)lastActiveTemplateVersion {
NSDictionary *userDefaults = [self instanceUserDefaults];
if ([userDefaults objectForKey:RCNActiveKeyTemplateVersion]) {
return [userDefaults objectForKey:RCNActiveKeyTemplateVersion];
}
return @"0";
}
- (void)setLastActiveTemplateVersion:(NSString *)templateVersion {
if (templateVersion) {
[self setInstanceUserDefaultsValue:templateVersion forKey:RCNActiveKeyTemplateVersion];
}
}
- (NSTimeInterval)lastETagUpdateTime {
NSNumber *lastETagUpdateTime =
[[self instanceUserDefaults] objectForKey:kRCNUserDefaultsKeyNamelastETagUpdateTime];
return lastETagUpdateTime.doubleValue;
}
- (void)setLastETagUpdateTime:(NSTimeInterval)lastETagUpdateTime {
if (lastETagUpdateTime) {
[self setInstanceUserDefaultsValue:@(lastETagUpdateTime)
forKey:kRCNUserDefaultsKeyNamelastETagUpdateTime];
}
}
- (NSTimeInterval)lastFetchTime {
NSNumber *lastFetchTime =
[[self instanceUserDefaults] objectForKey:kRCNUserDefaultsKeyNameLastSuccessfulFetchTime];
return lastFetchTime.doubleValue;
}
- (void)setLastFetchTime:(NSTimeInterval)lastFetchTime {
[self setInstanceUserDefaultsValue:@(lastFetchTime)
forKey:kRCNUserDefaultsKeyNameLastSuccessfulFetchTime];
}
- (NSString *)lastFetchStatus {
return [[self instanceUserDefaults] objectForKey:kRCNUserDefaultsKeyNamelastFetchStatus];
}
- (void)setLastFetchStatus:(NSString *)lastFetchStatus {
if (lastFetchStatus) {
[self setInstanceUserDefaultsValue:lastFetchStatus
forKey:kRCNUserDefaultsKeyNamelastFetchStatus];
}
}
- (BOOL)isClientThrottledWithExponentialBackoff {
NSNumber *isClientThrottled =
[[self instanceUserDefaults] objectForKey:kRCNUserDefaultsKeyNameIsClientThrottled];
return isClientThrottled.boolValue;
}
- (void)setIsClientThrottledWithExponentialBackoff:(BOOL)isClientThrottled {
[self setInstanceUserDefaultsValue:@(isClientThrottled)
forKey:kRCNUserDefaultsKeyNameIsClientThrottled];
}
- (NSTimeInterval)throttleEndTime {
NSNumber *throttleEndTime =
[[self instanceUserDefaults] objectForKey:kRCNUserDefaultsKeyNameThrottleEndTime];
return throttleEndTime.doubleValue;
}
- (void)setThrottleEndTime:(NSTimeInterval)throttleEndTime {
[self setInstanceUserDefaultsValue:@(throttleEndTime)
forKey:kRCNUserDefaultsKeyNameThrottleEndTime];
}
- (NSTimeInterval)currentThrottlingRetryIntervalSeconds {
NSNumber *throttleEndTime = [[self instanceUserDefaults]
objectForKey:kRCNUserDefaultsKeyNamecurrentThrottlingRetryInterval];
return throttleEndTime.doubleValue;
}
- (void)setCurrentThrottlingRetryIntervalSeconds:(NSTimeInterval)throttlingRetryIntervalSeconds {
[self setInstanceUserDefaultsValue:@(throttlingRetryIntervalSeconds)
forKey:kRCNUserDefaultsKeyNamecurrentThrottlingRetryInterval];
}
- (int)realtimeRetryCount {
int realtimeRetryCount = 0;
if ([[self instanceUserDefaults] objectForKey:kRCNUserDefaultsKeyNameRealtimeRetryCount]) {
realtimeRetryCount = [[[self instanceUserDefaults]
objectForKey:kRCNUserDefaultsKeyNameRealtimeRetryCount] intValue];
}
return realtimeRetryCount;
}
- (void)setRealtimeRetryCount:(int)realtimeRetryCount {
[self setInstanceUserDefaultsValue:[NSNumber numberWithInt:realtimeRetryCount]
forKey:kRCNUserDefaultsKeyNameRealtimeRetryCount];
}
- (NSTimeInterval)realtimeThrottleEndTime {
NSNumber *realtimeThrottleEndTime = 0;
if ([[self instanceUserDefaults] objectForKey:kRCNUserDefaultsKeyNameRealtimeThrottleEndTime]) {
realtimeThrottleEndTime =
[[self instanceUserDefaults] objectForKey:kRCNUserDefaultsKeyNameRealtimeThrottleEndTime];
}
return realtimeThrottleEndTime.doubleValue;
}
- (void)setRealtimeThrottleEndTime:(NSTimeInterval)throttleEndTime {
[self setInstanceUserDefaultsValue:@(throttleEndTime)
forKey:kRCNUserDefaultsKeyNameRealtimeThrottleEndTime];
}
- (NSTimeInterval)currentRealtimeThrottlingRetryIntervalSeconds {
NSNumber *realtimeThrottleEndTime = 0;
if ([[self instanceUserDefaults]
objectForKey:kRCNUserDefaultsKeyNameCurrentRealtimeThrottlingRetryInterval]) {
realtimeThrottleEndTime = [[self instanceUserDefaults]
objectForKey:kRCNUserDefaultsKeyNameCurrentRealtimeThrottlingRetryInterval];
}
return realtimeThrottleEndTime.doubleValue;
}
- (void)setCurrentRealtimeThrottlingRetryIntervalSeconds:
(NSTimeInterval)throttlingRetryIntervalSeconds {
[self setInstanceUserDefaultsValue:@(throttlingRetryIntervalSeconds)
forKey:kRCNUserDefaultsKeyNameCurrentRealtimeThrottlingRetryInterval];
}
#pragma mark Public methods.
- (void)resetUserDefaults {
[self resetInstanceUserDefaults];
}
#pragma mark Private methods.
// There is a nested hierarchy for the userdefaults as follows:
// [FIRAppName][FIRNamespaceName][Key]
- (nonnull NSDictionary *)appUserDefaults {
NSString *appPath = _firebaseAppName;
NSDictionary *appDict = [_userDefaults valueForKeyPath:appPath];
if (!appDict) {
appDict = [[NSDictionary alloc] init];
}
return appDict;
}
// Search for the user defaults for this (app, namespace) instance using the valueForKeyPath method.
- (nonnull NSDictionary *)instanceUserDefaults {
NSString *appNamespacePath =
[NSString stringWithFormat:@"%@.%@", _firebaseAppName, _firebaseNamespace];
NSDictionary *appNamespaceDict = [_userDefaults valueForKeyPath:appNamespacePath];
if (!appNamespaceDict) {
appNamespaceDict = [[NSMutableDictionary alloc] init];
}
return appNamespaceDict;
}
// Update users defaults for just this (app, namespace) instance.
- (void)setInstanceUserDefaultsValue:(NSObject *)value forKey:(NSString *)key {
@synchronized(_userDefaults) {
NSMutableDictionary *appUserDefaults = [[self appUserDefaults] mutableCopy];
NSMutableDictionary *appNamespaceUserDefaults = [[self instanceUserDefaults] mutableCopy];
[appNamespaceUserDefaults setObject:value forKey:key];
[appUserDefaults setObject:appNamespaceUserDefaults forKey:_firebaseNamespace];
[_userDefaults setObject:appUserDefaults forKey:_firebaseAppName];
// We need to synchronize to have this value updated for the extension.
[_userDefaults synchronize];
}
}
// Delete any existing userdefaults for this instance.
- (void)resetInstanceUserDefaults {
@synchronized(_userDefaults) {
NSMutableDictionary *appUserDefaults = [[self appUserDefaults] mutableCopy];
NSMutableDictionary *appNamespaceUserDefaults = [[self instanceUserDefaults] mutableCopy];
[appNamespaceUserDefaults removeAllObjects];
[appUserDefaults setObject:appNamespaceUserDefaults forKey:_firebaseNamespace];
[_userDefaults setObject:appUserDefaults forKey:_firebaseAppName];
// We need to synchronize to have this value updated for the extension.
[_userDefaults synchronize];
}
}
@end

View File

@ -0,0 +1,69 @@
/*
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
#if SWIFT_PACKAGE
@_exported import FirebaseRemoteConfigInternal
#endif // SWIFT_PACKAGE
import FirebaseSharedSwift
public enum RemoteConfigValueCodableError: Error {
case unsupportedType(String)
}
public extension RemoteConfigValue {
/// Extracts a RemoteConfigValue JSON-encoded object and decodes it to the requested type.
///
/// - Parameter asType: The type to decode the JSON-object to
func decoded<Value: Decodable>(asType: Value.Type = Value.self) throws -> Value {
if asType == Date.self {
throw RemoteConfigValueCodableError
.unsupportedType("Date type is not currently supported for " +
" Remote Config Value decoding. Please file a feature request")
}
return try FirebaseDataDecoder()
.decode(Value.self, from: FirebaseRemoteConfigValueDecoderHelper(value: self))
}
}
public enum RemoteConfigCodableError: Error {
case invalidSetDefaultsInput(String)
}
public extension RemoteConfig {
/// Decodes a struct from the respective Remote Config values.
///
/// - Parameter asType: The type to decode to.
func decoded<Value: Decodable>(asType: Value.Type = Value.self) throws -> Value {
let keys = allKeys(from: RemoteConfigSource.default) + allKeys(from: RemoteConfigSource.remote)
let config = keys.reduce(into: [String: FirebaseRemoteConfigValueDecoderHelper]()) {
$0[$1] = FirebaseRemoteConfigValueDecoderHelper(value: configValue(forKey: $1))
}
return try FirebaseDataDecoder().decode(Value.self, from: config)
}
/// Sets config defaults from an encodable struct.
///
/// - Parameter value: The object to use to set the defaults.
func setDefaults<Value: Encodable>(from value: Value) throws {
guard let encoded = try FirebaseDataEncoder().encode(value) as? [String: NSObject] else {
throw RemoteConfigCodableError.invalidSetDefaultsInput(
"The setDefaults input: \(value), must be a Struct that encodes to a Dictionary"
)
}
setDefaults(encoded)
}
}

View File

@ -0,0 +1,58 @@
/*
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
#if SWIFT_PACKAGE
@_exported import FirebaseRemoteConfigInternal
#endif // SWIFT_PACKAGE
import FirebaseSharedSwift
/// Implement the FirebaseRemoteConfigValueDecoding protocol for the shared Firebase decoder to
/// decode Remote Config Values. It returns the four different kinds of values from
/// a RemoteConfigValue object.
struct FirebaseRemoteConfigValueDecoderHelper: FirebaseRemoteConfigValueDecoding {
let value: RemoteConfigValue
func numberValue() -> NSNumber {
return value.numberValue
}
func boolValue() -> Bool {
return value.boolValue
}
func stringValue() -> String {
return value.stringValue
}
func dataValue() -> Data {
return value.dataValue
}
func arrayValue() -> [AnyHashable]? {
guard let value = value.jsonValue as? [AnyHashable] else {
return nil
}
return value
}
func dictionaryValue() -> [String: AnyHashable]? {
guard let value = value.jsonValue as? [String: AnyHashable] else {
return nil
}
return value
}
}

View File

@ -0,0 +1,51 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#if SWIFT_PACKAGE
@_exported import FirebaseRemoteConfigInternal
#endif // SWIFT_PACKAGE
import SwiftUI
/// A property wrapper that listens to a Remote Config value.
@available(iOS 14.0, macOS 11.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, *)
@propertyWrapper
public struct RemoteConfigProperty<T: Decodable>: DynamicProperty {
@StateObject private var configValueObserver: RemoteConfigValueObservable<T>
/// Remote Config key name for this property
public let key: String
public var wrappedValue: T {
configValueObserver.configValue
}
/// Creates an instance by providing a config key.
///
/// - Parameter key: key name
/// - Parameter fallback: The value to fall back to if the key doesn't exist in remote or default
/// configs
public init(key: String, fallback: T) {
self.key = key
_configValueObserver = StateObject(
wrappedValue: RemoteConfigValueObservable<T>(
key: key,
fallbackValue: fallback
)
)
}
}

View File

@ -0,0 +1,75 @@
/*
* 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.
*/
#if SWIFT_PACKAGE
@_exported import FirebaseRemoteConfigInternal
#endif // SWIFT_PACKAGE
import FirebaseCore
import SwiftUI
extension Notification.Name {
// Listens to FirebaseRemoteConfig SDK if new configs are activated.
static let onRemoteConfigActivated = Notification.Name("FIRRemoteConfigActivateNotification")
}
// Make sure this key is consistent with kFIRGoogleAppIDKey in FirebaseCore SDK
let FirebaseRemoteConfigAppNameKey = "FIRAppNameKey"
@available(iOS 14.0, macOS 11.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, *)
class RemoteConfigValueObservable<T: Decodable>: ObservableObject {
@Published var configValue: T
private let key: String
private let remoteConfig: RemoteConfig
private let fallbackValue: T
init(key: String, fallbackValue: T) {
self.key = key
remoteConfig = RemoteConfig.remoteConfig()
self.fallbackValue = fallbackValue
// Initialize with fallback value
configValue = fallbackValue
// Check cached remote config value
do {
let configValue: RemoteConfigValue = remoteConfig[key]
if configValue.source == .remote || configValue.source == .default {
self.configValue = try remoteConfig[key].decoded()
} else {
self.configValue = fallbackValue
}
} catch {
configValue = fallbackValue
}
NotificationCenter.default.addObserver(
self, selector: #selector(configDidActivate), name: .onRemoteConfigActivated, object: nil
)
}
@objc func configDidActivate(notification: NSNotification) {
// This feature is only available in the default app.
let appName = notification.userInfo?[FirebaseRemoteConfigAppNameKey] as? String
if FirebaseApp.app()?.name != appName {
return
}
do {
let configValue: RemoteConfigValue = remoteConfig[key]
if configValue.source == .remote {
self.configValue = try remoteConfig[key].decoded()
}
} catch {
// Suppresses a hard failure if decoding failed.
}
}
}

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array>
</array>
<key>NSPrivacyCollectedDataTypes</key>
<array>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeOtherDiagnosticData</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<false/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
</array>
</dict>
</array>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>1C8F.1</string>
</array>
</dict>
</array>
</dict>
</plist>

View File

@ -0,0 +1,30 @@
// Copyright 2023 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.
#if SWIFT_PACKAGE
@_exported import FirebaseRemoteConfigInternal
// This is a trick to force generate a `FirebaseRemoteConfig-Swift.h` header
// that re-exports `FirebaseRemoteConfigInternal` for Objective-C clients. It
// is important for the below code to reference a Remote Config symbol defined
// in Objective-C as that will import the symbol's module
// (`FirebaseRemoteConfigInternal`) in the generated header. This allows
// Objective-C clients to import Remote Config's Objective-C API using
// `@import FirebaseRemoteConfig;`. This API is not needed for Swift clients
// and is therefore unavailable in a Swift context.
@available(*, unavailable)
@objc public extension RemoteConfig {
static var __no_op: () -> Void { {} }
}
#endif // SWIFT_PACKAGE

View File

@ -0,0 +1,42 @@
/*
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
#if SWIFT_PACKAGE
@_exported import FirebaseRemoteConfigInternal
#endif // SWIFT_PACKAGE
/// Implements subscript overloads to enable Remote Config values to be accessed
/// in a type-safe way directly from the current config.
public extension RemoteConfig {
/// Return a typed RemoteConfigValue for a key.
/// - Parameter key: A Remote Config key.
/// - Returns: A typed RemoteConfigValue.
subscript<T: Decodable>(decodedValue key: String) -> T? {
return try? configValue(forKey: key).decoded()
}
/// Return a Dictionary for a RemoteConfig JSON key.
/// - Parameter key: A Remote Config key.
/// - Returns: A Dictionary representing a RemoteConfig JSON value.
subscript(jsonValue key: String) -> [String: AnyHashable]? {
guard let value = configValue(forKey: key).jsonValue as? [String: AnyHashable] else {
// nil is the historical behavior for failing to extract JSON.
return nil
}
return value
}
}

View File

@ -0,0 +1,68 @@
/*
* Copyright 2018 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>
@protocol FIRAnalyticsInteropListener;
NS_ASSUME_NONNULL_BEGIN
/// Block typedef callback parameter to `getUserProperties(with:)`.
typedef void (^FIRAInteropUserPropertiesCallback)(NSDictionary<NSString *, id> *userProperties)
NS_SWIFT_UNAVAILABLE("Use Swift's closure syntax instead.");
/// Connector for bridging communication between Firebase SDKs and FirebaseAnalytics APIs.
@protocol FIRAnalyticsInterop
/// Sets user property when trigger event is logged. This API is only available in the SDK.
- (void)setConditionalUserProperty:(NSDictionary<NSString *, id> *)conditionalUserProperty;
/// Clears user property if set.
- (void)clearConditionalUserProperty:(NSString *)userPropertyName
forOrigin:(NSString *)origin
clearEventName:(NSString *)clearEventName
clearEventParameters:(NSDictionary<NSString *, NSString *> *)clearEventParameters;
/// Returns currently set user properties.
- (NSArray<NSDictionary<NSString *, NSString *> *> *)conditionalUserProperties:(NSString *)origin
propertyNamePrefix:
(NSString *)propertyNamePrefix;
/// Returns the maximum number of user properties.
- (NSInteger)maxUserProperties:(NSString *)origin;
/// Returns the user properties to a callback function.
- (void)getUserPropertiesWithCallback:
(void (^)(NSDictionary<NSString *, id> *userProperties))callback;
/// Logs events.
- (void)logEventWithOrigin:(NSString *)origin
name:(NSString *)name
parameters:(nullable NSDictionary<NSString *, id> *)parameters;
/// Sets user property.
- (void)setUserPropertyWithOrigin:(NSString *)origin name:(NSString *)name value:(id)value;
/// Registers an Analytics listener for the given origin.
- (void)registerAnalyticsListener:(id<FIRAnalyticsInteropListener>)listener
withOrigin:(NSString *)origin;
/// Unregisters an Analytics listener for the given origin.
- (void)unregisterAnalyticsListenerWithOrigin:(NSString *)origin;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,24 @@
/*
* 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.
*/
/// Handles events and messages from Analytics.
@protocol FIRAnalyticsInteropListener <NSObject>
/// Triggers when an Analytics event happens for the registered origin with
/// FirebaseAnalyticsInterop`s `registerAnalyticsListener(_:withOrigin:)`.
- (void)messageTriggered:(NSString *)name parameters:(NSDictionary *)parameters;
@end

View File

@ -0,0 +1,28 @@
/*
* Copyright 2018 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.
*/
/// @file FIRInteropEventNames.h
#import <Foundation/Foundation.h>
/// Notification open event name.
static NSString *const kFIRIEventNotificationOpen = @"_no";
/// Notification foreground event name.
static NSString *const kFIRIEventNotificationForeground = @"_nf";
/// Campaign event name.
static NSString *const kFIRIEventFirebaseCampaign = @"_cmp";

View File

@ -0,0 +1,73 @@
/*
* Copyright 2018 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>
/// @file FIRInteropParameterNames.h
///
/// Predefined event parameter names used by Firebase. This file is a subset of the
/// FirebaseAnalytics FIRParameterNames.h public header.
///
/// The origin of your traffic, such as an Ad network (for example, google) or partner (urban
/// airship). Identify the advertiser, site, publication, etc. that is sending traffic to your
/// property. Highly recommended (String).
/// <pre>
/// let params = [
/// kFIRParameterSource : "InMobi",
/// // ...
/// ]
/// </pre>
static NSString *const kFIRIParameterSource NS_SWIFT_NAME(AnalyticsParameterSource) = @"source";
/// The advertising or marketing medium, for example: cpc, banner, email, push. Highly recommended
/// (String).
/// <pre>
/// let params = [
/// kFIRParameterMedium : "email",
/// // ...
/// ]
/// </pre>
static NSString *const kFIRIParameterMedium NS_SWIFT_NAME(AnalyticsParameterMedium) = @"medium";
/// The individual campaign name, slogan, promo code, etc. Some networks have pre-defined macro to
/// capture campaign information, otherwise can be populated by developer. Highly Recommended
/// (String).
/// <pre>
/// let params = [
/// kFIRParameterCampaign : "winter_promotion",
/// // ...
/// ]
/// </pre>
static NSString *const kFIRIParameterCampaign NS_SWIFT_NAME(AnalyticsParameterCampaign) =
@"campaign";
/// Message identifier.
static NSString *const kFIRIParameterMessageIdentifier = @"_nmid";
/// Message name.
static NSString *const kFIRIParameterMessageName = @"_nmn";
/// Message send time.
static NSString *const kFIRIParameterMessageTime = @"_nmt";
/// Message device time.
static NSString *const kFIRIParameterMessageDeviceTime = @"_ndt";
/// Topic message.
static NSString *const kFIRIParameterTopic = @"_nt";
/// Stores the message_id of the last notification opened by the app.
static NSString *const kFIRIUserPropertyLastNotification = @"_ln";

202
Pods/FirebaseRemoteConfig/LICENSE generated Normal file
View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

302
Pods/FirebaseRemoteConfig/README.md generated Normal file
View File

@ -0,0 +1,302 @@
<p align="center">
<a href="https://cocoapods.org/pods/Firebase">
<img src="https://img.shields.io/github/v/release/Firebase/firebase-ios-sdk?style=flat&label=CocoaPods"/>
</a>
<a href="https://swiftpackageindex.com/firebase/firebase-ios-sdk">
<img src="https://img.shields.io/github/v/release/Firebase/firebase-ios-sdk?style=flat&label=Swift%20Package%20Index&color=red"/>
</a>
<a href="https://cocoapods.org/pods/Firebase">
<img src="https://img.shields.io/github/license/Firebase/firebase-ios-sdk?style=flat"/>
</a><br/>
<a href="https://swiftpackageindex.com/firebase/firebase-ios-sdk">
<img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Ffirebase%2Ffirebase-ios-sdk%2Fbadge%3Ftype%3Dplatforms"/>
</a>
<a href="https://swiftpackageindex.com/firebase/firebase-ios-sdk">
<img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Ffirebase%2Ffirebase-ios-sdk%2Fbadge%3Ftype%3Dswift-versions"/>
</a>
</p>
# Firebase Apple Open Source Development
This repository contains the source code for all Apple platform Firebase SDKs except FirebaseAnalytics.
Firebase is an app development platform with tools to help you build, grow, and
monetize your app. More information about Firebase can be found on the
[official Firebase website](https://firebase.google.com).
## Installation
See the subsections below for details about the different installation methods. Where
available, it's recommended to install any libraries with a `Swift` suffix to get the
best experience when writing your app in Swift.
1. [Standard pod install](#standard-pod-install)
2. [Swift Package Manager](#swift-package-manager)
3. [Installing from the GitHub repo](#installing-from-github)
4. [Experimental Carthage](#carthage-ios-only)
### Standard pod install
For instructions on the standard pod install, visit:
[https://firebase.google.com/docs/ios/setup](https://firebase.google.com/docs/ios/setup).
### Swift Package Manager
Instructions for [Swift Package Manager](https://swift.org/package-manager/) support can be
found in the [SwiftPackageManager.md](SwiftPackageManager.md) Markdown file.
### Installing from GitHub
These instructions can be used to access the Firebase repo at other branches,
tags, or commits.
#### Background
See [the Podfile Syntax Reference](https://guides.cocoapods.org/syntax/podfile.html#pod)
for instructions and options about overriding pod source locations.
#### Accessing Firebase Source Snapshots
All official releases are tagged in this repo and available via CocoaPods. To access a local
source snapshot or unreleased branch, use Podfile directives like the following:
To access FirebaseFirestore via a branch:
```ruby
pod 'FirebaseCore', :git => 'https://github.com/firebase/firebase-ios-sdk.git', :branch => 'main'
pod 'FirebaseFirestore', :git => 'https://github.com/firebase/firebase-ios-sdk.git', :branch => 'main'
```
To access FirebaseMessaging via a checked-out version of the firebase-ios-sdk repo:
```ruby
pod 'FirebaseCore', :path => '/path/to/firebase-ios-sdk'
pod 'FirebaseMessaging', :path => '/path/to/firebase-ios-sdk'
```
### Carthage (iOS only)
Instructions for the experimental Carthage distribution can be found at
[Carthage.md](Carthage.md).
### Using Firebase from a Framework or a library
For details on using Firebase from a Framework or a library, refer to [firebase_in_libraries.md](docs/firebase_in_libraries.md).
## Development
To develop Firebase software in this repository, ensure that you have at least
the following software:
* Xcode 15.2 (or later)
CocoaPods is still the canonical way to develop, but much of the repo now supports
development with Swift Package Manager.
### CocoaPods
Install the following:
* CocoaPods 1.12.0 (or later)
* [CocoaPods generate](https://github.com/square/cocoapods-generate)
For the pod that you want to develop:
```ruby
pod gen Firebase{name here}.podspec --local-sources=./ --auto-open --platforms=ios
```
Note: If the CocoaPods cache is out of date, you may need to run
`pod repo update` before the `pod gen` command.
Note: Set the `--platforms` option to `macos` or `tvos` to develop/test for
those platforms. Since 10.2, Xcode does not properly handle multi-platform
CocoaPods workspaces.
Firestore has a self-contained Xcode project. See
[Firestore/README](Firestore/README.md) Markdown file.
#### Development for Catalyst
* `pod gen {name here}.podspec --local-sources=./ --auto-open --platforms=ios`
* Check the Mac box in the App-iOS Build Settings
* Sign the App in the Settings Signing & Capabilities tab
* Click Pods in the Project Manager
* Add Signing to the iOS host app and unit test targets
* Select the Unit-unit scheme
* Run it to build and test
Alternatively, disable signing in each target:
* Go to Build Settings tab
* Click `+`
* Select `Add User-Defined Setting`
* Add `CODE_SIGNING_REQUIRED` setting with a value of `NO`
### Swift Package Manager
* To enable test schemes: `./scripts/setup_spm_tests.sh`
* `open Package.swift` or double click `Package.swift` in Finder.
* Xcode will open the project
* Choose a scheme for a library to build or test suite to run
* Choose a target platform by selecting the run destination along with the scheme
### Adding a New Firebase Pod
Refer to [AddNewPod](AddNewPod.md) Markdown file for details.
### Managing Headers and Imports
For information about managing headers and imports, see [HeadersImports](HeadersImports.md) Markdown file.
### Code Formatting
To ensure that the code is formatted consistently, run the script
[./scripts/check.sh](https://github.com/firebase/firebase-ios-sdk/blob/main/scripts/check.sh)
before creating a pull request (PR).
GitHub Actions will verify that any code changes are done in a style-compliant
way. Install `clang-format` and `mint`:
```console
brew install clang-format@18
brew install mint
```
### Running Unit Tests
Select a scheme and press Command-u to build a component and run its unit tests.
### Running Sample Apps
To run the sample apps and integration tests, you'll need a valid
`GoogleService-Info.plist
` file. The Firebase Xcode project contains dummy plist
files without real values, but they can be replaced with real plist files. To get your own
`GoogleService-Info.plist` files:
1. Go to the [Firebase Console](https://console.firebase.google.com/)
2. Create a new Firebase project, if you don't already have one
3. For each sample app you want to test, create a new Firebase app with the sample app's bundle
identifier (e.g., `com.google.Database-Example`)
4. Download the resulting `GoogleService-Info.plist` and add it to the Xcode project.
### Coverage Report Generation
For coverage report generation instructions, see [scripts/code_coverage_report/README](scripts/code_coverage_report/README.md) Markdown file.
## Specific Component Instructions
See the sections below for any special instructions for those components.
### Firebase Auth
For specific Firebase Auth development, refer to the [Auth Sample README](FirebaseAuth/Tests/Sample/README.md) for instructions about
building and running the FirebaseAuth pod along with various samples and tests.
### Firebase Database
The Firebase Database Integration tests can be run against a locally running Database Emulator
or against a production instance.
To run against a local emulator instance, invoke `./scripts/run_database_emulator.sh start` before
running the integration test.
To run against a production instance, provide a valid `GoogleServices-Info.plist` and copy it to
`FirebaseDatabase/Tests/Resources/GoogleService-Info.plist`. Your Security Rule must be set to
[public](https://firebase.google.com/docs/database/security/quickstart) while your tests are
running.
### Firebase Dynamic Links
Firebase Dynamic Links is **deprecated** and should not be used in new projects. The service will shut down on August 25, 2025.
Please see our [Dynamic Links Deprecation FAQ documentation](https://firebase.google.com/support/dynamic-links-faq) for more guidance.
### Firebase Performance Monitoring
For specific Firebase Performance Monitoring development, see
[the Performance README](FirebasePerformance/README.md) for instructions about building the SDK
and [the Performance TestApp README](FirebasePerformance/Tests/TestApp/README.md) for instructions about
integrating Performance with the dev test App.
### Firebase Storage
To run the Storage Integration tests, follow the instructions in
[StorageIntegration.swift](FirebaseStorage/Tests/Integration/StorageIntegration.swift).
#### Push Notifications
Push notifications can only be delivered to specially provisioned App IDs in the developer portal.
In order to test receiving push notifications, you will need to:
1. Change the bundle identifier of the sample app to something you own in your Apple Developer
account and enable that App ID for push notifications.
2. You'll also need to
[upload your APNs Provider Authentication Key or certificate to the
Firebase Console](https://firebase.google.com/docs/cloud-messaging/ios/certs)
at **Project Settings > Cloud Messaging > [Your Firebase App]**.
3. Ensure your iOS device is added to your Apple Developer portal as a test device.
#### iOS Simulator
The iOS Simulator cannot register for remote notifications and will not receive push notifications.
To receive push notifications, follow the steps above and run the app on a physical device.
### Vertex AI for Firebase
See the [Vertex AI for Firebase README](FirebaseVertexAI#development) for
instructions about building and testing the SDK.
## Building with Firebase on Apple platforms
Firebase provides official beta support for macOS, Catalyst, and tvOS. visionOS and watchOS
are community supported. Thanks to community contributions for many of the multi-platform PRs.
At this time, most of Firebase's products are available across Apple platforms. There are still
a few gaps, especially on visionOS and watchOS. For details about the current support matrix, see
[this chart](https://firebase.google.com/docs/ios/learn-more#firebase_library_support_by_platform)
in Firebase's documentation.
### visionOS
Where supported, visionOS works as expected with the exception of Firestore via Swift Package
Manager where it is required to use the source distribution.
To enable the Firestore source distribution, quit Xcode and open the desired
project from the command line with the `FIREBASE_SOURCE_FIRESTORE` environment
variable: `open --env FIREBASE_SOURCE_FIRESTORE /path/to/project.xcodeproj`.
To go back to using the binary distribution of Firestore, quit Xcode and open
Xcode like normal, without the environment variable.
### watchOS
Thanks to contributions from the community, many of Firebase SDKs now compile, run unit tests, and
work on watchOS. See the [Independent Watch App Sample](Example/watchOSSample).
Keep in mind that watchOS is not officially supported by Firebase. While we can catch basic unit
test issues with GitHub Actions, there may be some changes where the SDK no longer works as expected
on watchOS. If you encounter this, please
[file an issue](https://github.com/firebase/firebase-ios-sdk/issues).
During app setup in the console, you may get to a step that mentions something like "Checking if the
app has communicated with our servers". This relies on Analytics and will not work on watchOS.
**It's safe to ignore the message and continue**, the rest of the SDKs will work as expected.
#### Additional Crashlytics Notes
* watchOS has limited support. Due to watchOS restrictions, mach exceptions and signal crashes are
not recorded. (Crashes in SwiftUI are generated as mach exceptions, so will not be recorded)
## Combine
Thanks to contributions from the community, _FirebaseCombineSwift_ contains support for Apple's Combine
framework. This module is currently under development and not yet supported for use in production
environments. For more details, please refer to the [docs](FirebaseCombineSwift/README.md).
## Roadmap
See [Roadmap](ROADMAP.md) for more about the Firebase Apple SDK Open Source
plans and directions.
## Contributing
See [Contributing](CONTRIBUTING.md) for more information on contributing to the Firebase
Apple SDK.
## License
The contents of this repository are licensed under the
[Apache License, version 2.0](http://www.apache.org/licenses/LICENSE-2.0).
Your use of Firebase is governed by the
[Terms of Service for Firebase Services](https://firebase.google.com/terms/).