From 69bfe3fd3cd0b4e41cfd0a23f69e4fdd036a2dbf Mon Sep 17 00:00:00 2001 From: Di Wu <49409954+diwu-arete@users.noreply.github.com> Date: Thu, 1 Oct 2020 15:10:32 -0700 Subject: [PATCH] Merge FLoC SDK into master branch. (#6466) * Create Segmentation SDK structure for source and unit tests. (#3214) * Create Segmentation SDK structure for source and unit tests. * Review changes: Remove unnecessary files in test folder. Add md-floc-master to CI * Rename Segmentation directory to FirebaseSegmentation directory. Update podspec to include search header path. * Add core support with interop for Segmentation SDK. (#3430) * Add core support with interop for Segmentation SDK. Also update headers to be under sources folder. * Review fixes. * Minor changes. * Fix style. * Fix style. * Style changes. * Fix whitespace in travis.yml * Fix style. * Travis CI is stuck..try updating the travis.yml * Undo travis.yml change. * Working drop of Segmentation SDK along with test app and unit tests. (#4574) * Working drop of Segmentation SDK along with sample app and unit tests. * Update if_changed.sh to include FirebaseSegmentation. * Complete NS_ASSUME_NON_NULL_START with NS_ASSUME_NON_NULL_END in header file. * Fix unit tests. * Fix style. * Fixes after running XCode's static analyzer. * Fix style. * fix style. * 'pod lib lint' fixes. * Fix analyzer errors. * Address review comments. * Minor changes for review comments. * Address review comments. * Address review comments. * stop mocking in tear down method for tests. * Minor update to sample app project. * Fix trailing whitespace in Podfile. * Add set -x to check.sh * update segmetation dependency version * migrate FloC SDK to depend on FIS SDK directly * format floc * format * using customized FIRapp * remove test plist file * refactor to capture weakself * format * replace partial mock with class mock * minor refactoring * use subscript to manipulate dictionary instance * fix import error * minor refoctoring, addressing comments * address comments * fix configurations * format Co-authored-by: dmandar Co-authored-by: ChaoqunCHEN --- .gitignore | 3 + Dangerfile | 4 + FirebaseSegmentation.podspec | 45 ++ FirebaseSegmentation/Sources/CHANGELOG.md | 0 .../Sources/FIRSegmentation.m | 78 ++++ .../Sources/FIRSegmentationComponent.m | 107 +++++ .../Private/FIRSegmentationComponent.h | 58 +++ .../Sources/Private/FIRSegmentationInternal.h | 30 ++ .../Sources/Public/FIRSegmentation.h | 77 ++++ .../Sources/Public/FirebaseSegmentation.h | 17 + .../Sources/SEGContentManager.h | 34 ++ .../Sources/SEGContentManager.m | 172 +++++++ .../Sources/SEGDatabaseManager.h | 54 +++ .../Sources/SEGDatabaseManager.m | 417 +++++++++++++++++ .../Sources/SEGNetworkManager.h | 34 ++ .../Sources/SEGNetworkManager.m | 223 +++++++++ .../Sources/SEGSegmentationConstants.h | 52 +++ FirebaseSegmentation/Tests/Sample/Podfile | 15 + .../project.pbxproj | 426 ++++++++++++++++++ .../SegmentationSampleApp/AppDelegate.h | 21 + .../SegmentationSampleApp/AppDelegate.m | 39 ++ .../AppIcon.appiconset/Contents.json | 98 ++++ .../Assets.xcassets/Contents.json | 6 + .../Base.lproj/LaunchScreen.storyboard | 25 + .../Base.lproj/Main.storyboard | 24 + .../Sample/SegmentationSampleApp/Info.plist | 49 ++ .../SegmentationSampleApp/ViewController.h | 19 + .../SegmentationSampleApp/ViewController.m | 28 ++ .../Tests/Sample/SegmentationSampleApp/main.m | 22 + .../Tests/Unit/SEGContentManagerTests.m | 122 +++++ .../Tests/Unit/SEGDatabaseManagerTests.m | 118 +++++ .../Tests/Unit/SEGInitializationTests.m | 53 +++ scripts/check.sh | 1 - scripts/if_changed.sh | 9 +- 34 files changed, 2477 insertions(+), 3 deletions(-) create mode 100644 FirebaseSegmentation.podspec create mode 100644 FirebaseSegmentation/Sources/CHANGELOG.md create mode 100644 FirebaseSegmentation/Sources/FIRSegmentation.m create mode 100644 FirebaseSegmentation/Sources/FIRSegmentationComponent.m create mode 100644 FirebaseSegmentation/Sources/Private/FIRSegmentationComponent.h create mode 100644 FirebaseSegmentation/Sources/Private/FIRSegmentationInternal.h create mode 100644 FirebaseSegmentation/Sources/Public/FIRSegmentation.h create mode 100644 FirebaseSegmentation/Sources/Public/FirebaseSegmentation.h create mode 100644 FirebaseSegmentation/Sources/SEGContentManager.h create mode 100644 FirebaseSegmentation/Sources/SEGContentManager.m create mode 100644 FirebaseSegmentation/Sources/SEGDatabaseManager.h create mode 100644 FirebaseSegmentation/Sources/SEGDatabaseManager.m create mode 100644 FirebaseSegmentation/Sources/SEGNetworkManager.h create mode 100644 FirebaseSegmentation/Sources/SEGNetworkManager.m create mode 100644 FirebaseSegmentation/Sources/SEGSegmentationConstants.h create mode 100644 FirebaseSegmentation/Tests/Sample/Podfile create mode 100644 FirebaseSegmentation/Tests/Sample/SegmentationSampleApp.xcodeproj/project.pbxproj create mode 100644 FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/AppDelegate.h create mode 100644 FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/AppDelegate.m create mode 100644 FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/Assets.xcassets/Contents.json create mode 100644 FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/Base.lproj/LaunchScreen.storyboard create mode 100644 FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/Base.lproj/Main.storyboard create mode 100644 FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/Info.plist create mode 100644 FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/ViewController.h create mode 100644 FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/ViewController.m create mode 100644 FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/main.m create mode 100644 FirebaseSegmentation/Tests/Unit/SEGContentManagerTests.m create mode 100644 FirebaseSegmentation/Tests/Unit/SEGDatabaseManagerTests.m create mode 100644 FirebaseSegmentation/Tests/Unit/SEGInitializationTests.m diff --git a/.gitignore b/.gitignore index 9604ed7104..bdec8d64ad 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,9 @@ FirebaseStorage/Tests/Integration/Credentials.h FirebaseStorage/Tests/SwiftIntegration/Credentials.swift FirebaseStorageSwift/Tests/Integration/Credentials.swift +# FirebaseSegmentation integration tests GoogleService-Info.plist +FirebaseSegmentation/Tests/Sample/GoogleService-Info.plist + Secrets.tar # OS X diff --git a/Dangerfile b/Dangerfile index 8fcb1b113a..dbe5a1a68a 100644 --- a/Dangerfile +++ b/Dangerfile @@ -49,6 +49,7 @@ def labelsForModifiedFiles() labels.push("api: instanceid") if @has_instanceid_changes labels.push("api: messaging") if @has_messaging_changes labels.push("api: remoteconfig") if @has_remoteconfig_changes + labels.push("api: segmentation") if @has_segmentation_changes labels.push("api: storage") if @has_storage_changes labels.push("GoogleDataTransport") if @has_gdt_changes labels.push("GoogleUtilities") if @has_googleutilities_changes @@ -102,6 +103,8 @@ has_license_changes = didModify(["LICENSE"]) @has_messaging_api_changes = hasChangesIn("FirebaseMessaging/Sources/Public/") @has_remoteconfig_changes = hasChangesIn("FirebaseRemoteConfig/") @has_remoteconfig_api_changes = hasChangesIn("FirebaseRemoteConfig/Sources/Public/") +@has_segmentation_changes = hasChangesIn("FirebaseSegmentation/") +@has_segmentation_api_changes = hasChangesIn("FirebaseSegmentation/Source/Public/ @has_storage_changes = hasChangesIn("FirebaseStorage/") @has_storage_api_changes = hasChangesIn("FirebaseStorage/Sources/Public/") @@ -125,6 +128,7 @@ has_license_changes = didModify(["LICENSE"]) @has_instanceid_api_changes || @has_messaging_api_changes || @has_remoteconfig_api_changes || + @has_segmentation_api_changes || @has_storage_api_changes || @has_gdt_api_changes diff --git a/FirebaseSegmentation.podspec b/FirebaseSegmentation.podspec new file mode 100644 index 0000000000..88fdf84c39 --- /dev/null +++ b/FirebaseSegmentation.podspec @@ -0,0 +1,45 @@ +Pod::Spec.new do |s| + s.name = 'FirebaseSegmentation' + s.version = '0.1.0' + s.summary = 'Firebase Segmentation SDK' + s.description = <<-DESC +Firebase Segmentation enables you to associate your custom application instance ID with Firebase for user segmentation. + DESC + + s.homepage = 'https://firebase.google.com' + s.license = { :type => 'Apache', :file => 'LICENSE' } + s.authors = 'Google, Inc.' + s.source = { + :git => 'https://github.com/firebase/firebase-ios-sdk.git', + :tag => 'Segmentation-' + s.version.to_s + } + + s.ios.deployment_target = '8.0' + s.cocoapods_version = '>= 1.4.0' + s.static_framework = true + s.prefix_header_file = false + + s.source_files = [ + 'FirebaseSegmentation/Sources/**/*.[mh]', + 'FirebaseCore/Sources/Private/*.h', + ] + s.public_header_files = 'FirebaseSegmentation/Sources/Public/*.h' + + s.dependency 'FirebaseCore', '~> 6.7' + s.dependency 'FirebaseInstallations', '~> 1.7' + + header_search_paths = { + 'HEADER_SEARCH_PATHS' => '"${PODS_TARGET_SRCROOT}"' + } + + s.pod_target_xcconfig = { + 'GCC_C_LANGUAGE_STANDARD' => 'c99', + 'GCC_PREPROCESSOR_DEFINITIONS' => 'FIRSegmentation_VERSION=' + s.version.to_s + }.merge(header_search_paths) + + s.test_spec 'unit' do |unit_tests| + unit_tests.source_files = 'FirebaseSegmentation/Tests/Unit/*.[mh]' + unit_tests.dependency 'OCMock' + unit_tests.requires_app_host = true + end +end diff --git a/FirebaseSegmentation/Sources/CHANGELOG.md b/FirebaseSegmentation/Sources/CHANGELOG.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/FirebaseSegmentation/Sources/FIRSegmentation.m b/FirebaseSegmentation/Sources/FIRSegmentation.m new file mode 100644 index 0000000000..ea64e4db4b --- /dev/null +++ b/FirebaseSegmentation/Sources/FIRSegmentation.m @@ -0,0 +1,78 @@ +// 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 "FirebaseSegmentation/Sources/Public/FIRSegmentation.h" + +#import "FirebaseCore/Sources/Private/FIRComponentContainer.h" +#import "FirebaseCore/Sources/Private/FIRLogger.h" +#import "FirebaseCore/Sources/Private/FIROptionsInternal.h" +#import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h" +#import "FirebaseSegmentation/Sources/Private/FIRSegmentationComponent.h" +#import "FirebaseSegmentation/Sources/SEGContentManager.h" + +@implementation FIRSegmentation { + NSString *_firebaseAppName; + SEGContentManager *_contentManager; +} + ++ (nonnull FIRSegmentation *)segmentation { + if (![FIRApp isDefaultAppConfigured]) { + [NSException + raise:kFirebaseSegmentationErrorDomain + format:@"FIRApp not configured. Please make sure you have called [FIRApp configure]"]; + } + + return [FIRSegmentation segmentationWithApp:[FIRApp defaultApp]]; +} + ++ (nonnull FIRSegmentation *)segmentationWithApp:(nonnull FIRApp *)firebaseApp { + // Use the provider to generate and return instances of FIRSegmentation for this specific app and + // namespace. This will ensure the app is configured before Remote Config can return an instance. + id provider = + FIR_COMPONENT(FIRSegmentationProvider, firebaseApp.container); + return [provider segmentation]; +} + +- (void)setCustomInstallationID:(NSString *)customInstallationID + completion:(void (^)(NSError *))completionHandler { + [_contentManager + associateCustomInstallationIdentiferNamed:customInstallationID + firebaseApp:_firebaseAppName + completion:^(BOOL success, NSDictionary *result) { + if (!success) { + // TODO(dmandar) log; pass along internal error code. + NSError *error = [NSError + errorWithDomain:kFirebaseSegmentationErrorDomain + code:FIRSegmentationErrorCodeInternal + userInfo:result]; + completionHandler(error); + } else { + completionHandler(nil); + } + }]; +} + +/// Designated initializer +- (instancetype)initWithAppName:(NSString *)appName FIROptions:(FIROptions *)options { + self = [super init]; + if (self) { + _firebaseAppName = appName; + + // Initialize the content manager. + _contentManager = [SEGContentManager sharedInstanceWithOptions:options]; + } + return self; +} + +@end diff --git a/FirebaseSegmentation/Sources/FIRSegmentationComponent.m b/FirebaseSegmentation/Sources/FIRSegmentationComponent.m new file mode 100644 index 0000000000..a2784b2bed --- /dev/null +++ b/FirebaseSegmentation/Sources/FIRSegmentationComponent.m @@ -0,0 +1,107 @@ +/* + * 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 "FirebaseSegmentation/Sources/Private/FIRSegmentationComponent.h" + +#import "FirebaseCore/Sources/Private/FIRAppInternal.h" +#import "FirebaseCore/Sources/Private/FIRComponentContainer.h" +#import "FirebaseCore/Sources/Private/FIROptionsInternal.h" +#import "FirebaseSegmentation/Sources/Private/FIRSegmentationInternal.h" +#import "FirebaseSegmentation/Sources/SEGSegmentationConstants.h" + +#ifndef FIRSegmentation_VERSION +#error "FIRSegmentation_VERSION is not defined: \ +add -DFIRSegmentation_VERSION=... to the build invocation" +#endif + +#define STR(x) STR_EXPAND(x) +#define STR_EXPAND(x) #x + +@implementation FIRSegmentationComponent + +/// Default method for retrieving a Segmentation instance, or creating one if it doesn't exist. +- (FIRSegmentation *)segmentation { + // 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"; + } + + if (errorPropertyName) { + [NSException + raise:kFirebaseSegmentationErrorDomain + format:@"%@", + [NSString + stringWithFormat: + @"Firebase Segmentation 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]]; + } + + FIRSegmentation *instance = self.segmentationInstance; + if (!instance) { + instance = [[FIRSegmentation alloc] initWithAppName:self.app.name FIROptions:self.app.options]; + self.segmentationInstance = instance; + } + + return instance; +} + +/// Default initializer. +- (instancetype)initWithApp:(FIRApp *)app { + self = [super init]; + if (self) { + _app = app; + if (!_segmentationInstance) { + _segmentationInstance = [[FIRSegmentation alloc] initWithAppName:app.name + FIROptions:app.options]; + } + } + 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-seg" + withVersion:[NSString stringWithUTF8String:STR(FIRSegmentation_VERSION)]]; +} + +#pragma mark - Interoperability + ++ (NSArray *)componentsToRegister { + FIRComponent *segProvider = [FIRComponent + componentWithProtocol:@protocol(FIRSegmentationProvider) + instantiationTiming:FIRInstantiationTimingAlwaysEager + dependencies:@[] + creationBlock:^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) { + // Cache the component so instances of Segmentation are cached. + *isCacheable = YES; + return [[FIRSegmentationComponent alloc] initWithApp:container.app]; + }]; + return @[ segProvider ]; +} + +@synthesize instances; + +@end diff --git a/FirebaseSegmentation/Sources/Private/FIRSegmentationComponent.h b/FirebaseSegmentation/Sources/Private/FIRSegmentationComponent.h new file mode 100644 index 0000000000..2824848260 --- /dev/null +++ b/FirebaseSegmentation/Sources/Private/FIRSegmentationComponent.h @@ -0,0 +1,58 @@ +/* + * 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 + +#import "FIRLibrary.h" + +@class FIRApp; +@class FIRSegmentation; + +NS_ASSUME_NONNULL_BEGIN + +/// Provides and creates instances of Segmentation. Used in the +/// interop registration process to keep track of Segmentation instances for each `FIRApp` instance. +@protocol FIRSegmentationProvider + +/// Cached instances of Segmentation objects. +@property(nonatomic, strong) NSMutableDictionary *instances; + +/// Default method for retrieving a Segmentation instance, or creating one if it doesn't exist. +- (FIRSegmentation *)segmentation; + +@end + +/// A concrete implementation for FIRSegmentationInterop to create Segmentation instances and +/// register with Core's component system. +@interface FIRSegmentationComponent : NSObject + +/// The FIRApp that instances will be set up with. +@property(nonatomic, weak, readonly) FIRApp *app; + +/// Cached instances of Segmentation objects. +@property(nonatomic, strong) FIRSegmentation *segmentationInstance; + +/// Default method for retrieving a Segmentation instance, or creating one if it doesn't exist. +- (FIRSegmentation *)segmentation; + +/// Default initializer. +- (instancetype)initWithApp:(FIRApp *)app NS_DESIGNATED_INITIALIZER; + +- (instancetype)init __attribute__((unavailable("Use `initWithApp:`."))); + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseSegmentation/Sources/Private/FIRSegmentationInternal.h b/FirebaseSegmentation/Sources/Private/FIRSegmentationInternal.h new file mode 100644 index 0000000000..5e126ef7bf --- /dev/null +++ b/FirebaseSegmentation/Sources/Private/FIRSegmentationInternal.h @@ -0,0 +1,30 @@ +/* + * 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 + +NS_ASSUME_NONNULL_BEGIN + +@class FIRApp; + +NS_SWIFT_NAME(Segmentation) +@interface FIRSegmentation : NSObject + +/// Initialize a Segmentation instance with all the required parameters directly. +- (instancetype)initWithAppName:(NSString *)appName FIROptions:(FIROptions *)options; +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseSegmentation/Sources/Public/FIRSegmentation.h b/FirebaseSegmentation/Sources/Public/FIRSegmentation.h new file mode 100644 index 0000000000..8b0dd6d7ba --- /dev/null +++ b/FirebaseSegmentation/Sources/Public/FIRSegmentation.h @@ -0,0 +1,77 @@ +/* + * 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 + +NS_ASSUME_NONNULL_BEGIN + +@class FIRApp; +/** + * The Firebase Segmentation SDK is used to associate a custom, non-Firebase custom installation + * identifier to Firebase. Once this custom installation identifier is set, developers can use the + * current app installation for segmentation purposes. If the custom installation identifier is + * explicitely set to nil, any existing custom installation identifier data will be removed. + */ +NS_SWIFT_NAME(Segmentation) +@interface FIRSegmentation : NSObject + +/// Firebase Segmentation service fetch error. +typedef NS_ENUM(NSInteger, FIRSegmentationErrorCode) { + /// No error. The operation was successful. + FIRSegmentationErrorCodeNone = 8001, + /// An internal error occurred. + FIRSegmentationErrorCodeInternal = 8002, + /// Error indicating that backend reports an existing association for this custom installation + /// identifier. + FIRSegmentationErrorCodeConflict = 8003, + /// Error indicating that a network error occurred during association. + FIRSegmentationErrorCodeNetwork = 8004, +} NS_SWIFT_NAME(SegmentationErrorCode); + +/** + * Singleton instance (scoped to the default FIRApp) + * Returns the FIRSegmentation instance for the default Firebase application. Please make sure you + * call [FIRApp configure] beforehand for a default Firebase app to already be initialized and + * available. This singleton class instance lets you set your own custom identifier to be used for + * user segmentation purposes within Firebase. + * + * @return A shared instance of FIRSegmentation. + */ ++ (instancetype)segmentation; + +/// Singleton instance (scoped to FIRApp) +/// Returns the FIRSegmentation instance for your Firebase application. This singleton class +/// instance lets you set your own custom identifier to be used for targeting purposes within +/// Firebase. ++ (instancetype)segmentationWithApp:(nonnull FIRApp *)app; + +/** + * :nodoc: + * Unavailable. Use +segmentation instead. + */ +- (instancetype)init __attribute__((unavailable("Use +segmentation instead."))); + +/// Set your own custom installation ID to be used for segmentation purposes. +/// This method needs to be called every time (and immediately) upon any changes to the custom +/// installation ID. +/// @param completionHandler Set custom installation ID completion. Returns nil if initialization +/// succeeded or an NSError object if initialization failed. +- (void)setCustomInstallationID:(nullable NSString *)customInstallationID + completion:(nullable void (^)(NSError *))completionHandler; + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseSegmentation/Sources/Public/FirebaseSegmentation.h b/FirebaseSegmentation/Sources/Public/FirebaseSegmentation.h new file mode 100644 index 0000000000..31465cbb87 --- /dev/null +++ b/FirebaseSegmentation/Sources/Public/FirebaseSegmentation.h @@ -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 "FIRSegmentation.h" diff --git a/FirebaseSegmentation/Sources/SEGContentManager.h b/FirebaseSegmentation/Sources/SEGContentManager.h new file mode 100644 index 0000000000..6087fbfa09 --- /dev/null +++ b/FirebaseSegmentation/Sources/SEGContentManager.h @@ -0,0 +1,34 @@ +// 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 + +#import "FirebaseSegmentation/Sources/SEGSegmentationConstants.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FIROptions; + +@interface SEGContentManager : NSObject + +/// Shared Singleton Instance ++ (instancetype)sharedInstanceWithOptions:(FIROptions*)options; + +- (void)associateCustomInstallationIdentiferNamed:(NSString*)customInstallationID + firebaseApp:(NSString*)appName + completion:(SEGRequestCompletion)completionHandler; + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseSegmentation/Sources/SEGContentManager.m b/FirebaseSegmentation/Sources/SEGContentManager.m new file mode 100644 index 0000000000..b11408295d --- /dev/null +++ b/FirebaseSegmentation/Sources/SEGContentManager.m @@ -0,0 +1,172 @@ +// 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 "FirebaseSegmentation/Sources/SEGContentManager.h" + +#import "FirebaseCore/Sources/Private/FIRAppInternal.h" +#import "FirebaseInstallations/Source/Library/Public/FirebaseInstallations/FirebaseInstallations.h" +#import "FirebaseSegmentation/Sources/Public/FIRSegmentation.h" +#import "FirebaseSegmentation/Sources/SEGDatabaseManager.h" +#import "FirebaseSegmentation/Sources/SEGNetworkManager.h" +#import "FirebaseSegmentation/Sources/SEGSegmentationConstants.h" + +NSString *const kErrorDescription = @"ErrorDescription"; + +@interface SEGContentManager () { + NSMutableDictionary *_associationData; + NSString *_installationIdentifier; + NSString *_installationIdentifierToken; + SEGDatabaseManager *_databaseManager; + SEGNetworkManager *_networkManager; +} +@end + +@implementation SEGContentManager + ++ (instancetype)sharedInstanceWithOptions:(FIROptions *)options { + static dispatch_once_t onceToken; + static SEGContentManager *sharedInstance; + dispatch_once(&onceToken, ^{ + sharedInstance = [[SEGContentManager alloc] + initWithDatabaseManager:[SEGDatabaseManager sharedInstance] + networkManager:[[SEGNetworkManager alloc] initWithOptions:options]]; + }); + return sharedInstance; +} + +- (instancetype)initWithDatabaseManager:databaseManager networkManager:networkManager { + self = [super init]; + if (self) { + // Initialize the database manager. + _databaseManager = databaseManager; + + // Initialize the network manager. + _networkManager = networkManager; + + // Load all data from the database. + [_databaseManager createOrOpenDatabaseWithCompletion:^(BOOL success, NSDictionary *result) { + self->_associationData = [result mutableCopy]; + }]; + // TODO(dmandar) subscribe to FIS notifications once integrated. + } + return self; +} + +- (FIRInstallations *)installationForApp:(NSString *)firebaseApp { + return [FIRInstallations installationsWithApp:[FIRApp appNamed:firebaseApp]]; +} + +- (void)associateCustomInstallationIdentiferNamed:(NSString *)customInstallationID + firebaseApp:(NSString *)firebaseApp + completion:(SEGRequestCompletion)completionHandler { + // Get the latest installation identifier + FIRInstallations *installation = [self installationForApp:firebaseApp]; + if (installation == nil) { + completionHandler(NO, @{kErrorDescription : @"Firebase Installations SDK not available"}); + } + __weak SEGContentManager *weakSelf = self; + [installation + installationIDWithCompletion:^(NSString *_Nullable identifier, NSError *_Nullable error) { + SEGContentManager *strongSelf = weakSelf; + if (!strongSelf) { + completionHandler(NO, @{kErrorDescription : @"Internal Error getting installation ID."}); + return; + } + + [strongSelf associateInstallationWithLatestIdentifier:identifier + installation:installation + customizedIdentifier:customInstallationID + firebaseApp:firebaseApp + error:error + completion:completionHandler]; + }]; +} + +- (void)associateInstallationWithLatestIdentifier:(NSString *_Nullable)identifier + installation:(FIRInstallations *)installation + customizedIdentifier:(NSString *)customInstallationID + firebaseApp:(NSString *)firebaseApp + error:(NSError *_Nullable)error + completion:(SEGRequestCompletion)completionHandler { + if (!identifier || error) { + NSString *errorMessage = @"Error getting installation ID."; + if (error) { + errorMessage = [errorMessage stringByAppendingString:error.description]; + } + NSDictionary *errorDictionary = @{kErrorDescription : errorMessage}; + completionHandler(NO, errorDictionary); + return; + } + + _installationIdentifier = identifier; + + __weak SEGContentManager *weakSelf = self; + [installation authTokenWithCompletion:^(FIRInstallationsAuthTokenResult *_Nullable tokenResult, + NSError *_Nullable error) { + SEGContentManager *strongSelf = weakSelf; + if (!strongSelf) { + completionHandler(NO, @{kErrorDescription : @"Internal Error getting installation token."}); + return; + } + [strongSelf associateInstallationWithToken:tokenResult + customizedIdentifier:customInstallationID + firebaseApp:firebaseApp + error:error + completion:completionHandler]; + }]; +} + +- (void)associateInstallationWithToken:(FIRInstallationsAuthTokenResult *_Nullable)tokenResult + customizedIdentifier:(NSString *)customInstallationID + firebaseApp:(NSString *)firebaseApp + error:(NSError *_Nullable)error + completion:(SEGRequestCompletion)completionHandler { + if (!tokenResult || error) { + NSString *errorMessage = @"Error getting AuthToken."; + if (error) { + errorMessage = [errorMessage stringByAppendingString:error.description]; + } + NSDictionary *errorDictionary = @{kErrorDescription : errorMessage}; + completionHandler(NO, errorDictionary); + return; + } + _installationIdentifierToken = tokenResult.authToken; + + NSMutableDictionary *appAssociationData = + [[NSMutableDictionary alloc] init]; + appAssociationData[kSEGCustomInstallationIdentifierKey] = customInstallationID; + appAssociationData[kSEGFirebaseInstallationIdentifierKey] = _installationIdentifier; + appAssociationData[kSEGAssociationStatusKey] = kSEGAssociationStatusPending; + _associationData[firebaseApp] = appAssociationData; + + // Update the database async. + // TODO(mandard) The database write and corresponding completion handler needs to be wired up + // once we support listening to FID changes. + [_databaseManager insertMainTableApplicationNamed:firebaseApp + customInstanceIdentifier:customInstallationID + firebaseInstanceIdentifier:_installationIdentifier + associationStatus:kSEGAssociationStatusPending + completionHandler:nil]; + + // Send the change up to the backend. Also add the token. + [_networkManager + makeAssociationRequestToBackendWithData:appAssociationData + token:_installationIdentifierToken + completion:^(BOOL status, NSDictionary *result) { + // TODO: log, update database. + completionHandler(status, result); + }]; +} + +@end diff --git a/FirebaseSegmentation/Sources/SEGDatabaseManager.h b/FirebaseSegmentation/Sources/SEGDatabaseManager.h new file mode 100644 index 0000000000..a73428128f --- /dev/null +++ b/FirebaseSegmentation/Sources/SEGDatabaseManager.h @@ -0,0 +1,54 @@ +// 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 + +#import "FirebaseSegmentation/Sources/SEGSegmentationConstants.h" + +NS_ASSUME_NONNULL_BEGIN + +/// Persist config data in sqlite database on device. Managing data read/write from/to database. +@interface SEGDatabaseManager : NSObject +/// Shared Singleton Instance ++ (instancetype)sharedInstance; + +/// Open the database. +- (void)createOrOpenDatabaseWithCompletion:(SEGRequestCompletion)completionHandler; + +/// Read all contents of main table. +- (void)loadMainTableWithCompletion:(SEGRequestCompletion)completionHandler; + +/// Insert a record in main table. +/// @param firebaseApplication The name of the Firebase App that this segmentation instance is +/// associated with. +/// @param customInstanceIdentifier The custom instance identifier provided by the developer. +/// @param firebaseInstanceIdentifier The firebase instance identifier provided by the IID/FIS SDK. +/// @param associationStatus The current status of the association - Pending until reported to the +/// backend. +- (void)insertMainTableApplicationNamed:(NSString *)firebaseApplication + customInstanceIdentifier:(NSString *)customInstanceIdentifier + firebaseInstanceIdentifier:(NSString *)firebaseInstanceIdentifier + associationStatus:(NSString *)associationStatus + completionHandler:(nullable SEGRequestCompletion)handler; + +/// Clear the record of given namespace and package name +/// before updating the table.//TODO: Add delete. +- (void)deleteRecordFromMainTableWithCustomInstanceIdentifier:(NSString *)customInstanceIdentifier; + +/// Remove all the records from a config content table. +- (void)deleteAllRecordsFromTable; + +NS_ASSUME_NONNULL_END + +@end diff --git a/FirebaseSegmentation/Sources/SEGDatabaseManager.m b/FirebaseSegmentation/Sources/SEGDatabaseManager.m new file mode 100644 index 0000000000..73726124a6 --- /dev/null +++ b/FirebaseSegmentation/Sources/SEGDatabaseManager.m @@ -0,0 +1,417 @@ +// 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 "FirebaseSegmentation/Sources/SEGDatabaseManager.h" + +#import +#import "FirebaseCore/Sources/Private/FIRLogger.h" + +/// SQLite file name. +static NSString *const kDatabaseName = @"FirebaseSegmentation.sqlite3"; +/// The application support sub-directory that the Segmentation database resides in. +static NSString *const kApplicationSupportSubDirectory = @"Google/FirebaseSegmentation"; +/// Column names +static NSString *const kMainTableName = @"main"; +static NSString *const kMainTableColumnApplicationIdentifier = @"firebase_app_identifier"; +static NSString *const kMainTableColumnCustomInstallationIdentifier = + @"custom_installation_identifier"; +static NSString *const kMainTableColumnFirebaseInstallationIdentifier = + @"firebase_installation_identifier"; +static NSString *const kMainTableColumnAssociationStatus = @"association_status"; + +// Exclude the database from iCloud backup. +static BOOL SEGAddSkipBackupAttributeToItemAtPath(NSString *filePathString) { + NSURL *URL = [NSURL fileURLWithPath:filePathString]; + assert([[NSFileManager defaultManager] fileExistsAtPath:URL.path]); + + NSError *error = nil; + BOOL success = [URL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:&error]; + if (!success) { + // TODO(dmandar): log error. + NSLog(@"Error excluding %@ from backup %@.", [URL lastPathComponent], error); + } + return success; +} + +static BOOL SEGCreateFilePathIfNotExist(NSString *filePath) { + if (!filePath || !filePath.length) { + // TODO(dmandar) log error. + NSLog(@"Failed to create subdirectory for an empty file path."); + return NO; + } + NSFileManager *fileManager = [NSFileManager defaultManager]; + if (![fileManager fileExistsAtPath:filePath]) { + NSError *error; + [fileManager createDirectoryAtPath:[filePath stringByDeletingLastPathComponent] + withIntermediateDirectories:YES + attributes:nil + error:&error]; + if (error) { + // TODO(dmandar) log error. + NSLog(@"Failed to create subdirectory for database file: %@.", error); + return NO; + } + } + return YES; +} + +@interface SEGDatabaseManager () { + /// Database storing all the config information. + sqlite3 *_database; + /// Serial queue for database read/write operations. + dispatch_queue_t _databaseOperationQueue; +} +@end + +@implementation SEGDatabaseManager + ++ (instancetype)sharedInstance { + static dispatch_once_t onceToken; + static SEGDatabaseManager *sharedInstance; + dispatch_once(&onceToken, ^{ + sharedInstance = [[SEGDatabaseManager alloc] init]; + }); + return sharedInstance; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _databaseOperationQueue = + dispatch_queue_create("com.google.firebasesegmentation.database", DISPATCH_QUEUE_SERIAL); + } + return self; +} + +#pragma mark - Public Methods + +- (void)loadMainTableWithCompletion:(SEGRequestCompletion)completionHandler { + __weak SEGDatabaseManager *weakSelf = self; + dispatch_async(_databaseOperationQueue, ^{ + SEGDatabaseManager *strongSelf = weakSelf; + if (!strongSelf) { + completionHandler(NO, @{@"Database Error" : @"Internal database error"}); + } + + // Read the database into memory. + NSDictionary *> *associations = + [self loadMainTable]; + completionHandler(YES, associations); + }); + return; +} + +- (void)createOrOpenDatabaseWithCompletion:(SEGRequestCompletion)completionHandler { + __weak SEGDatabaseManager *weakSelf = self; + dispatch_async(_databaseOperationQueue, ^{ + SEGDatabaseManager *strongSelf = weakSelf; + if (!strongSelf) { + completionHandler(NO, @{@"ErrorDescription" : @"Internal database error"}); + } + NSString *dbPath = [SEGDatabaseManager pathForSegmentationDatabase]; + // TODO(dmandar) log. + NSLog(@"Loading segmentation database at path %@", dbPath); + const char *databasePath = dbPath.UTF8String; + // Create or open database path. + if (!SEGCreateFilePathIfNotExist(dbPath)) { + completionHandler(NO, @{@"ErrorDescription" : @"Could not create database file at path"}); + } + int flags = SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_FILEPROTECTION_COMPLETE | + SQLITE_OPEN_FULLMUTEX; + if (sqlite3_open_v2(databasePath, &strongSelf->_database, flags, NULL) == SQLITE_OK) { + // Create table if does not exist already. + if ([strongSelf createTableSchema]) { + // DB file created or already exists. + // Exclude the app data used from iCloud backup. + SEGAddSkipBackupAttributeToItemAtPath(dbPath); + + // Read the database into memory. + NSDictionary *associations = [self loadMainTable]; + completionHandler(YES, associations); + + } else { + // Remove database before fail. + [strongSelf removeDatabase:dbPath]; + FIRLogError(kFIRLoggerSegmentation, @"I-SEG000010", @"Failed to create table."); + // Create a new database if existing database file is corrupted. + if (!SEGCreateFilePathIfNotExist(dbPath)) { + completionHandler(NO, + @{@"ErrorDescription" : @"Could not recreate database file at path"}); + } + if (sqlite3_open_v2(databasePath, &strongSelf->_database, flags, NULL) == SQLITE_OK) { + if (![strongSelf createTableSchema]) { + // Remove database before fail. + [strongSelf removeDatabase:dbPath]; + // If it failed again, there's nothing we can do here. + FIRLogError(kFIRLoggerSegmentation, @"I-SEG000010", @"Failed to create table."); + } else { + // Exclude the app data used from iCloud backup. + SEGAddSkipBackupAttributeToItemAtPath(dbPath); + } + } else { + [strongSelf logDatabaseError]; + completionHandler(NO, @{@"ErrorDescription" : @"Could not create database."}); + } + } + } else { + [strongSelf logDatabaseError]; + completionHandler(NO, @{@"ErrorDescription" : @"Error creating database."}); + } + }); +} + +- (void)removeDatabase:(NSString *)path completion:(SEGRequestCompletion)completionHandler { + dispatch_async(_databaseOperationQueue, ^{ + SEGDatabaseManager *strongSelf = self; + if (!strongSelf) { + return; + } + [strongSelf removeDatabase:path]; + completionHandler(YES, nil); + }); +} + +#pragma mark - Private Methods + +- (NSDictionary *)loadMainTable { + NSString *SQLQuery = [NSString + stringWithFormat:@"SELECT %@, %@, %@, %@ FROM %@", kMainTableColumnApplicationIdentifier, + kMainTableColumnCustomInstallationIdentifier, + kMainTableColumnFirebaseInstallationIdentifier, + kMainTableColumnAssociationStatus, kMainTableName]; + + sqlite3_stmt *statement = [self prepareSQL:[SQLQuery cStringUsingEncoding:NSUTF8StringEncoding]]; + if (!statement) { + return nil; + } + + NSMutableDictionary *> *associations = + [[NSMutableDictionary alloc] init]; + while (sqlite3_step(statement) == SQLITE_ROW) { + NSString *firebaseApplicationName = + [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 0)]; + NSString *customInstallationIdentifier = + [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 1)]; + NSString *firebaseInstallationIdentifier = + [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 2)]; + NSString *associationStatus = + [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 3)]; + NSDictionary *associationData = @{ + kSEGCustomInstallationIdentifierKey : customInstallationIdentifier, + kSEGFirebaseInstallationIdentifierKey : firebaseInstallationIdentifier, + kSEGAssociationStatusKey : associationStatus + }; + [associations setObject:associationData forKey:firebaseApplicationName]; + } + sqlite3_finalize(statement); + return associations; +} + +/// Returns the current version of the Remote Config database. ++ (NSString *)pathForSegmentationDatabase { + NSArray *dirPaths = + NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); + NSString *appSupportPath = dirPaths.firstObject; + NSArray *components = + @[ appSupportPath, kApplicationSupportSubDirectory, kDatabaseName ]; + return [NSString pathWithComponents:components]; +} + +- (BOOL)createTableSchema { + SEG_MUST_NOT_BE_MAIN_THREAD(); + NSString *mainTableSchema = + [NSString stringWithFormat:@"create TABLE IF NOT EXISTS %@ (_id INTEGER PRIMARY KEY, %@ " + @"TEXT, %@ TEXT, %@ TEXT, %@ TEXT)", + kMainTableName, kMainTableColumnApplicationIdentifier, + kMainTableColumnCustomInstallationIdentifier, + kMainTableColumnFirebaseInstallationIdentifier, + kMainTableColumnAssociationStatus]; + + return [self executeQuery:[mainTableSchema cStringUsingEncoding:NSUTF8StringEncoding]]; +} + +- (void)removeDatabase:(NSString *)path { + SEG_MUST_NOT_BE_MAIN_THREAD(); + if (sqlite3_close(self->_database) != SQLITE_OK) { + [self logDatabaseError]; + } + self->_database = nil; + + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSError *error; + if (![fileManager removeItemAtPath:path error:&error]) { + FIRLogError(kFIRLoggerSegmentation, @"I-SEG000011", + @"Failed to remove database at path %@ for error %@.", path, error); + } +} + +#pragma mark - execute +- (BOOL)executeQuery:(const char *)SQL { + SEG_MUST_NOT_BE_MAIN_THREAD(); + char *error; + if (sqlite3_exec(_database, SQL, nil, nil, &error) != SQLITE_OK) { + FIRLogError(kFIRLoggerSegmentation, @"I-SEG000012", @"Failed to execute query with error %s.", + error); + return NO; + } + return YES; +} + +#pragma mark - insert +- (void)insertMainTableApplicationNamed:(NSString *)firebaseApplication + customInstanceIdentifier:(NSString *)customInstanceIdentifier + firebaseInstanceIdentifier:(NSString *)firebaseInstanceIdentifier + associationStatus:(NSString *)associationStatus + completionHandler:(SEGRequestCompletion)handler { + // TODO: delete the row first. + __weak SEGDatabaseManager *weakSelf = self; + dispatch_async(_databaseOperationQueue, ^{ + NSArray *values = + [[NSArray alloc] initWithObjects:firebaseApplication, customInstanceIdentifier, + firebaseInstanceIdentifier, associationStatus, nil]; + BOOL success = [weakSelf insertMainTableWithValues:values]; + if (handler) { + dispatch_async(dispatch_get_main_queue(), ^{ + handler(success, nil); + }); + } + }); +} + +- (BOOL)insertMainTableWithValues:(NSArray *)values { + SEG_MUST_NOT_BE_MAIN_THREAD(); + if (values.count != 4) { + FIRLogError(kFIRLoggerSegmentation, @"I-SEG000013", + @"Failed to insert config record. Wrong number of give parameters, current " + @"number is %ld, correct number is 4.", + (long)values.count); + return NO; + } + NSString *SQL = [NSString stringWithFormat:@"INSERT INTO %@ (%@, %@, %@, %@) values (?, ?, ?, ?)", + kMainTableName, kMainTableColumnApplicationIdentifier, + kMainTableColumnCustomInstallationIdentifier, + kMainTableColumnFirebaseInstallationIdentifier, + kMainTableColumnAssociationStatus]; + + sqlite3_stmt *statement = [self prepareSQL:[SQL UTF8String]]; + if (!statement) { + return NO; + } + + NSString *aString = values[0]; + if (![self bindStringToStatement:statement index:1 string:aString]) { + return [self logErrorWithSQL:[SQL UTF8String] finalizeStatement:statement returnValue:NO]; + } + aString = values[1]; + if (![self bindStringToStatement:statement index:2 string:aString]) { + return [self logErrorWithSQL:[SQL UTF8String] finalizeStatement:statement returnValue:NO]; + } + aString = values[2]; + if (![self bindStringToStatement:statement index:3 string:aString]) { + return [self logErrorWithSQL:[SQL UTF8String] finalizeStatement:statement returnValue:NO]; + } + aString = values[3]; + if (![self bindStringToStatement:statement index:4 string:aString]) { + return [self logErrorWithSQL:[SQL UTF8String] finalizeStatement:statement returnValue:NO]; + } + if (sqlite3_step(statement) != SQLITE_DONE) { + return [self logErrorWithSQL:[SQL UTF8String] finalizeStatement:statement returnValue:NO]; + } + sqlite3_finalize(statement); + return YES; +} + +/// TODO: (Check if required). Clear the record of given namespace and package name +/// before updating the table. +- (void)deleteRecordFromMainTableWithCustomInstanceIdentifier: + (nonnull NSString *)customInstanceIdentifier { +} + +/// TODO: (Check if required). Remove all the records from a config content table. +- (void)deleteAllRecordsFromTable { +} + +#pragma mark - helper +- (BOOL)executeQuery:(const char *)SQL withParams:(NSArray *)params { + SEG_MUST_NOT_BE_MAIN_THREAD(); + sqlite3_stmt *statement = [self prepareSQL:SQL]; + if (!statement) { + return NO; + } + + [self bindStringsToStatement:statement stringArray:params]; + if (sqlite3_step(statement) != SQLITE_DONE) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + sqlite3_finalize(statement); + return YES; +} + +/// Params only accept TEXT format string. +- (BOOL)bindStringsToStatement:(sqlite3_stmt *)statement stringArray:(NSArray *)array { + int index = 1; + for (NSString *param in array) { + if (![self bindStringToStatement:statement index:index string:param]) { + return [self logErrorWithSQL:nil finalizeStatement:statement returnValue:NO]; + } + index++; + } + return YES; +} + +- (BOOL)bindStringToStatement:(sqlite3_stmt *)statement index:(int)index string:(NSString *)value { + if (sqlite3_bind_text(statement, index, [value UTF8String], -1, SQLITE_TRANSIENT) != SQLITE_OK) { + return [self logErrorWithSQL:nil finalizeStatement:statement returnValue:NO]; + } + return YES; +} + +- (sqlite3_stmt *)prepareSQL:(const char *)SQL { + sqlite3_stmt *statement = nil; + if (sqlite3_prepare_v2(_database, SQL, -1, &statement, NULL) != SQLITE_OK) { + [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + return nil; + } + return statement; +} + +- (NSString *)errorMessage { + return [NSString stringWithFormat:@"%s", sqlite3_errmsg(_database)]; +} + +- (int)errorCode { + return sqlite3_errcode(_database); +} + +- (void)logDatabaseError { + FIRLogError(kFIRLoggerSegmentation, @"I-SEG000015", @"Error message: %@. Error code: %d.", + [self errorMessage], [self errorCode]); +} + +- (BOOL)logErrorWithSQL:(const char *)SQL + finalizeStatement:(sqlite3_stmt *)statement + returnValue:(BOOL)returnValue { + if (SQL) { + FIRLogError(kFIRLoggerSegmentation, @"I-SEG000016", @"Failed with SQL: %s.", SQL); + } + [self logDatabaseError]; + + if (statement) { + sqlite3_finalize(statement); + } + + return returnValue; +} + +@end diff --git a/FirebaseSegmentation/Sources/SEGNetworkManager.h b/FirebaseSegmentation/Sources/SEGNetworkManager.h new file mode 100644 index 0000000000..f6fc7588d8 --- /dev/null +++ b/FirebaseSegmentation/Sources/SEGNetworkManager.h @@ -0,0 +1,34 @@ +// 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 + +#import "FirebaseCore/Sources/Private/FIROptionsInternal.h" + +#import "FirebaseSegmentation/Sources/SEGSegmentationConstants.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface SEGNetworkManager : NSObject + +- (instancetype)initWithOptions:(FIROptions *)options; + +- (void)makeAssociationRequestToBackendWithData: + (nonnull NSDictionary *)associationData + token:(NSString *)token + completion:(SEGRequestCompletion)completionHandler; + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseSegmentation/Sources/SEGNetworkManager.m b/FirebaseSegmentation/Sources/SEGNetworkManager.m new file mode 100644 index 0000000000..22261ec292 --- /dev/null +++ b/FirebaseSegmentation/Sources/SEGNetworkManager.m @@ -0,0 +1,223 @@ +// 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 "FirebaseSegmentation/Sources/SEGNetworkManager.h" + +#import "FirebaseCore/Sources/Private/FIRAppInternal.h" +#import "FirebaseCore/Sources/Private/FIRLogger.h" +#import "FirebaseCore/Sources/Private/FIROptionsInternal.h" + +// TODO(dmandar): define in build file. +#define SEG_ALPHA_SERVER + +static NSString *const kServerURLDomain = @"https://firebasesegmentation.googleapis.com"; + +#ifdef SEG_ALPHA_SERVER +static NSString *const kServerURLVersion = @"/v1alpha"; +#else +static NSString *const kServerURLVersion = @"/v1"; +#endif + +static NSString *const kServerURLStringProjects = @"/projects/"; +static NSString *const kServerURLStringInstallations = @"/installations/"; +static NSString *const kServerURLStringCustomSegmentationData = @"/customSegmentationData"; + +static NSString *const kHTTPMethodPatch = @"PATCH"; +static NSString *const kRequestHeaderAuthorizationValueString = @"FIREBASE_INSTALLATIONS_AUTH"; +static NSString *const kRequestDataCustomInstallationIdString = @"custom_installation_id"; + +// HTTP header names. +static NSString *const kHeaderNameAPIKey = @"x-goog-api-key"; +static NSString *const kHeaderNameFirebaseAuthorizationToken = @"Authorization"; +static NSString *const kHeaderNameContentType = @"Content-Type"; +static NSString *const kHeaderNameContentEncoding = @"Content-Encoding"; +static NSString *const kHeaderNameAcceptEncoding = @"Accept-Encoding"; + +// Sends the bundle ID. Refer to b/130301479 for details. +static NSString *const kiOSBundleIdentifierHeaderName = + @"X-Ios-Bundle-Identifier"; ///< HTTP Header Field Name + +/// Config HTTP request content type JSON +static NSString *const kContentTypeValueJSON = @"application/json"; + +// TODO: Handle error codes. +/// HTTP status codes. Ref: https://cloud.google.com/apis/design/errors#error_retries +static NSInteger const kSEGResponseHTTPStatusCodeOK = 200; +// static NSInteger const kSEGResponseHTTPStatusCodeConflict = 409; +// static NSInteger const kSEGResponseHTTPStatusTooManyRequests = 429; +// static NSInteger const kSEGResponseHTTPStatusCodeInternalError = 500; +// static NSInteger const kSEGResponseHTTPStatusCodeServiceUnavailable = 503; +// static NSInteger const kSEGResponseHTTPStatusCodeGatewayTimeout = 504; + +// HTTP default timeout. +static NSTimeInterval const kSEGHTTPRequestTimeout = 60; + +/// Completion handler invoked by URLSession completion handler. +typedef void (^URLSessionCompletion)(NSData *data, NSURLResponse *response, NSError *error); + +@implementation SEGNetworkManager { + FIROptions *_firebaseAppOptions; + NSURLSession *_URLSession; +} + +- (instancetype)initWithOptions:(FIROptions *)options { + self = [super init]; + if (self) { + _firebaseAppOptions = options; + _URLSession = [self newURLSession]; + } + return self; +} + +- (void)dealloc { + [_URLSession invalidateAndCancel]; +} + +- (void)makeAssociationRequestToBackendWithData: + (NSDictionary *)associationData + token:(NSString *)token + completion:(SEGRequestCompletion)completionHandler { + // Construct the server URL. + NSString *URL = [self constructServerURLWithAssociationData:associationData]; + if (!URL) { + FIRLogError(kFIRLoggerSegmentation, @"I-SEG000020", @"Could not construct backend URL."); + completionHandler(NO, @{@"errorDescription" : @"Could not construct backend URL"}); + } + + FIRLogDebug(kFIRLoggerSegmentation, @"I-SEG000019", @"%@", + [NSString stringWithFormat:@"Making config request: %@", URL]); + + // Construct the request data. + NSString *customInstallationIdentifier = + [associationData objectForKey:kSEGCustomInstallationIdentifierKey]; + // TODO: Add tests for nil. + NSDictionary *requestDataDictionary = + @{kRequestDataCustomInstallationIdString : customInstallationIdentifier}; + NSError *error = nil; + NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestDataDictionary + options:0 + error:nil]; + if (!requestData || error) { + FIRLogError(kFIRLoggerSegmentation, @"I-SEG000021", @"Could not create request data. %@", + error.localizedDescription); + completionHandler(NO, + @{@"errorDescription" : @"Could not serialize JSON data for network call."}); + } + + // Handle NSURLSession completion. + __weak SEGNetworkManager *weakSelf = self; + [self URLSessionDataTaskWithURL:URL + content:requestData + token:token + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + SEGNetworkManager *strongSelf = weakSelf; + if (!strongSelf) { + FIRLogError(kFIRLoggerSegmentation, @"I-SEG000022", + @"Internal error making network request."); + completionHandler( + NO, @{@"errorDescription" : @"Internal error making network request."}); + return; + } + + NSInteger statusCode = [((NSHTTPURLResponse *)response) statusCode]; + if (!error && (statusCode == kSEGResponseHTTPStatusCodeOK)) { + FIRLogDebug(kFIRLoggerSegmentation, @"I-SEG000017", + @"SEGNetworkManager: Network request successful."); + completionHandler(YES, nil); + } else { + FIRLogError(kFIRLoggerSegmentation, @"I-SEG000018", + @"SEGNetworkManager: Network request failed with status code:%lu", + (long)statusCode); + completionHandler(NO, @{ + @"ErrorDescription" : + [NSString stringWithFormat:@"Network Error: %lu", (long)statusCode] + }); + }; + }]; +} + +#pragma mark Private + +- (NSURLSession *)newURLSession { + NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration]; + config.timeoutIntervalForRequest = kSEGHTTPRequestTimeout; + config.timeoutIntervalForResource = kSEGHTTPRequestTimeout; + NSURLSession *session = [NSURLSession sessionWithConfiguration:config]; + return session; +} + +- (NSString *)constructServerURLWithAssociationData: + (NSDictionary *)associationData { + NSString *serverURLStr = [[NSString alloc] initWithString:kServerURLDomain]; + serverURLStr = [serverURLStr stringByAppendingString:kServerURLVersion]; + serverURLStr = [serverURLStr stringByAppendingString:kServerURLStringProjects]; + + if (_firebaseAppOptions.projectID) { + serverURLStr = [serverURLStr stringByAppendingString:_firebaseAppOptions.projectID]; + } else { + FIRLogError(kFIRLoggerSegmentation, @"I-SEG000070", + @"Missing `projectID` from `FirebaseOptions`, please ensure the configured " + @"`FirebaseApp` is configured with `FirebaseOptions` that contains a `projectID`."); + return nil; + } + + serverURLStr = [serverURLStr stringByAppendingString:kServerURLStringInstallations]; + + // Get the FID. + NSString *firebaseInstallationIdentifier = + [associationData objectForKey:kSEGFirebaseInstallationIdentifierKey]; + if (!firebaseInstallationIdentifier) { + FIRLogError(kFIRLoggerSegmentation, @"I-SEG000071", + @"Missing Firebase installation identifier"); + return nil; + } + serverURLStr = [serverURLStr stringByAppendingString:firebaseInstallationIdentifier]; + serverURLStr = [serverURLStr stringByAppendingString:kServerURLStringCustomSegmentationData]; + + return serverURLStr; +} + +- (void)URLSessionDataTaskWithURL:(NSString *)stringURL + content:(NSData *)content + token:(NSString *)token + completionHandler:(URLSessionCompletion)completionHandler { + NSTimeInterval timeoutInterval = kSEGHTTPRequestTimeout; + NSURL *URL = [NSURL URLWithString:stringURL]; + NSMutableURLRequest *URLRequest = + [[NSMutableURLRequest alloc] initWithURL:URL + cachePolicy:NSURLRequestReloadIgnoringLocalCacheData + timeoutInterval:timeoutInterval]; + URLRequest.HTTPMethod = kHTTPMethodPatch; + + // Setup headers. + [URLRequest setValue:_firebaseAppOptions.APIKey forHTTPHeaderField:kHeaderNameAPIKey]; + NSString *authorizationTokenHeaderValue = + [NSString stringWithFormat:@"%@ %@", kRequestHeaderAuthorizationValueString, token]; + [URLRequest setValue:authorizationTokenHeaderValue + forHTTPHeaderField:kHeaderNameFirebaseAuthorizationToken]; + // TODO: Check if we accept gzip. + // [URLRequest setValue:@"gzip" forHTTPHeaderField:kHeaderNameContentEncoding]; + // [URLRequest setValue:@"gzip" forHTTPHeaderField:kHeaderNameAcceptEncoding]; + + // Send the bundleID for API Key restrictions. + [URLRequest setValue:[[NSBundle mainBundle] bundleIdentifier] + forHTTPHeaderField:kiOSBundleIdentifierHeaderName]; + [URLRequest setHTTPBody:content]; + + NSURLSessionDataTask *task = [_URLSession dataTaskWithRequest:URLRequest + completionHandler:completionHandler]; + [task resume]; +} + +@end diff --git a/FirebaseSegmentation/Sources/SEGSegmentationConstants.h b/FirebaseSegmentation/Sources/SEGSegmentationConstants.h new file mode 100644 index 0000000000..033f152cd9 --- /dev/null +++ b/FirebaseSegmentation/Sources/SEGSegmentationConstants.h @@ -0,0 +1,52 @@ +/* + * 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 + +#ifndef SEGSegmentationConstants_h +#define SEGSegmentationConstants_h + +#if defined(DEBUG) +#define SEG_MUST_NOT_BE_MAIN_THREAD() \ + do { \ + NSAssert(![NSThread isMainThread], @"Must not be executing on the main thread."); \ + } while (0); +#else +#define SEG_MUST_NOT_BE_MAIN_THREAD() \ + do { \ + } while (0); +#endif + +static NSString* const kFIRLoggerSegmentation = @"[Firebase/Segmentation]"; + +/// Keys for values stored in the Segmentation SDK. +static NSString* const kSEGFirebaseApplicationIdentifierKey = @"firebase_app_identifier"; +static NSString* const kSEGCustomInstallationIdentifierKey = @"custom_installation_identifier"; +static NSString* const kSEGFirebaseInstallationIdentifierKey = @"firebase_installation_identifier"; +static NSString* const kSEGAssociationStatusKey = @"association_status"; +/// Association Status +static NSString* const kSEGAssociationStatusPending = @"PENDING"; +static NSString* const kSEGAssociationStatusAssociated = @"ASSOCIATED"; + +/// Segmentation error domain when logging errors. +static NSString* const kFirebaseSegmentationErrorDomain = @"com.firebase.segmentation"; + +/// Segmentation Request Completion callback. +/// @param success Decide whether the network operation succeeds. +/// @param result Return operation result data. +typedef void (^SEGRequestCompletion)(BOOL success, NSDictionary* result); + +#endif /* SEGSegmentationConstants_h */ diff --git a/FirebaseSegmentation/Tests/Sample/Podfile b/FirebaseSegmentation/Tests/Sample/Podfile new file mode 100644 index 0000000000..9f52a7c02b --- /dev/null +++ b/FirebaseSegmentation/Tests/Sample/Podfile @@ -0,0 +1,15 @@ +# Uncomment the next two lines for pre-release testing on internal repo +#source 'sso://cpdc-internal/firebase' +#source 'https://cdn.cocoapods.org/' + +target 'SegmentationSampleApp' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + inherit! :search_paths + platform :ios, '8.0' + + # Pods for SegmentationSampleApp + pod 'FirebaseCore', :path => '../../../' + pod 'FirebaseSegmentation', :path => '../../../' + +end diff --git a/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp.xcodeproj/project.pbxproj b/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..902dffc5bd --- /dev/null +++ b/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp.xcodeproj/project.pbxproj @@ -0,0 +1,426 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXBuildFile section */ + 5B4DE0A622F8D7B100B55A7B /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 5B4DE0A522F8D7B100B55A7B /* AppDelegate.m */; }; + 5B4DE0A922F8D7B100B55A7B /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 5B4DE0A822F8D7B100B55A7B /* ViewController.m */; }; + 5B4DE0AC22F8D7B100B55A7B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5B4DE0AA22F8D7B100B55A7B /* Main.storyboard */; }; + 5B4DE0AE22F8D7B300B55A7B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5B4DE0AD22F8D7B300B55A7B /* Assets.xcassets */; }; + 5B4DE0B122F8D7B300B55A7B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5B4DE0AF22F8D7B300B55A7B /* LaunchScreen.storyboard */; }; + 5B4DE0B422F8D7B300B55A7B /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 5B4DE0B322F8D7B300B55A7B /* main.m */; }; + 5B4DE0BD22F8DA1300B55A7B /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5B4DE0BB22F8DA1300B55A7B /* GoogleService-Info.plist */; }; + AC252CE6B594C95D235E64F9 /* Pods_SegmentationSampleApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03FB7E52AF4C6B5EAA580F43 /* Pods_SegmentationSampleApp.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 03FB7E52AF4C6B5EAA580F43 /* Pods_SegmentationSampleApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SegmentationSampleApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 23212B47B363359777FCF914 /* Pods-SegmentationSampleApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SegmentationSampleApp.debug.xcconfig"; path = "Target Support Files/Pods-SegmentationSampleApp/Pods-SegmentationSampleApp.debug.xcconfig"; sourceTree = ""; }; + 2B8D0D05EDF54422ACEF8C82 /* Pods-SegmentationSampleApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SegmentationSampleApp.release.xcconfig"; path = "Target Support Files/Pods-SegmentationSampleApp/Pods-SegmentationSampleApp.release.xcconfig"; sourceTree = ""; }; + 5B4DE0A122F8D7B100B55A7B /* SegmentationSampleApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SegmentationSampleApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 5B4DE0A422F8D7B100B55A7B /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 5B4DE0A522F8D7B100B55A7B /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 5B4DE0A722F8D7B100B55A7B /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 5B4DE0A822F8D7B100B55A7B /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + 5B4DE0AB22F8D7B100B55A7B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 5B4DE0AD22F8D7B300B55A7B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 5B4DE0B022F8D7B300B55A7B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 5B4DE0B222F8D7B300B55A7B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 5B4DE0B322F8D7B300B55A7B /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 5B4DE0BA22F8DA1200B55A7B /* SecondApp-GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "SecondApp-GoogleService-Info.plist"; sourceTree = ""; }; + 5B4DE0BB22F8DA1300B55A7B /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + 5B4DE0BF22F8E07200B55A7B /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "../../../../../../configdaily googservice/GoogleService-Info.plist"; sourceTree = ""; }; + 5B4DE0C222F8E09900B55A7B /* SecondApp-GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "SecondApp-GoogleService-Info.plist"; sourceTree = ""; }; + 5B4DE0C322F8E09900B55A7B /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 5B4DE09E22F8D7B100B55A7B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + AC252CE6B594C95D235E64F9 /* Pods_SegmentationSampleApp.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 1DAAA9CDE17A7AD3D4D58BFA /* Pods */ = { + isa = PBXGroup; + children = ( + 23212B47B363359777FCF914 /* Pods-SegmentationSampleApp.debug.xcconfig */, + 2B8D0D05EDF54422ACEF8C82 /* Pods-SegmentationSampleApp.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 2074D9A873FD02A71321C678 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 5B4DE0BF22F8E07200B55A7B /* GoogleService-Info.plist */, + 03FB7E52AF4C6B5EAA580F43 /* Pods_SegmentationSampleApp.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 5B4DE09822F8D7B100B55A7B = { + isa = PBXGroup; + children = ( + 5B4DE0BB22F8DA1300B55A7B /* GoogleService-Info.plist */, + 5B4DE0C322F8E09900B55A7B /* GoogleService-Info.plist */, + 5B4DE0BA22F8DA1200B55A7B /* SecondApp-GoogleService-Info.plist */, + 5B4DE0C222F8E09900B55A7B /* SecondApp-GoogleService-Info.plist */, + 5B4DE0A322F8D7B100B55A7B /* SegmentationSampleApp */, + 5B4DE0A222F8D7B100B55A7B /* Products */, + 1DAAA9CDE17A7AD3D4D58BFA /* Pods */, + 2074D9A873FD02A71321C678 /* Frameworks */, + ); + sourceTree = ""; + }; + 5B4DE0A222F8D7B100B55A7B /* Products */ = { + isa = PBXGroup; + children = ( + 5B4DE0A122F8D7B100B55A7B /* SegmentationSampleApp.app */, + ); + name = Products; + sourceTree = ""; + }; + 5B4DE0A322F8D7B100B55A7B /* SegmentationSampleApp */ = { + isa = PBXGroup; + children = ( + 5B4DE0A422F8D7B100B55A7B /* AppDelegate.h */, + 5B4DE0A522F8D7B100B55A7B /* AppDelegate.m */, + 5B4DE0A722F8D7B100B55A7B /* ViewController.h */, + 5B4DE0A822F8D7B100B55A7B /* ViewController.m */, + 5B4DE0AA22F8D7B100B55A7B /* Main.storyboard */, + 5B4DE0AD22F8D7B300B55A7B /* Assets.xcassets */, + 5B4DE0AF22F8D7B300B55A7B /* LaunchScreen.storyboard */, + 5B4DE0B222F8D7B300B55A7B /* Info.plist */, + 5B4DE0B322F8D7B300B55A7B /* main.m */, + ); + path = SegmentationSampleApp; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 5B4DE0A022F8D7B100B55A7B /* SegmentationSampleApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5B4DE0B722F8D7B300B55A7B /* Build configuration list for PBXNativeTarget "SegmentationSampleApp" */; + buildPhases = ( + B73B50EA390CE7F1ED248C8C /* [CP] Check Pods Manifest.lock */, + 5B4DE09D22F8D7B100B55A7B /* Sources */, + 5B4DE09E22F8D7B100B55A7B /* Frameworks */, + 5B4DE09F22F8D7B100B55A7B /* Resources */, + F28D7493DD28A2A2A72AF1FC /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SegmentationSampleApp; + productName = SegmentationSampleApp; + productReference = 5B4DE0A122F8D7B100B55A7B /* SegmentationSampleApp.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 5B4DE09922F8D7B100B55A7B /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1010; + ORGANIZATIONNAME = "Mandar Deolalikar"; + TargetAttributes = { + 5B4DE0A022F8D7B100B55A7B = { + CreatedOnToolsVersion = 10.1; + }; + }; + }; + buildConfigurationList = 5B4DE09C22F8D7B100B55A7B /* Build configuration list for PBXProject "SegmentationSampleApp" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 5B4DE09822F8D7B100B55A7B; + productRefGroup = 5B4DE0A222F8D7B100B55A7B /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 5B4DE0A022F8D7B100B55A7B /* SegmentationSampleApp */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 5B4DE09F22F8D7B100B55A7B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5B4DE0BD22F8DA1300B55A7B /* GoogleService-Info.plist in Resources */, + 5B4DE0B122F8D7B300B55A7B /* LaunchScreen.storyboard in Resources */, + 5B4DE0AE22F8D7B300B55A7B /* Assets.xcassets in Resources */, + 5B4DE0AC22F8D7B100B55A7B /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + B73B50EA390CE7F1ED248C8C /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-SegmentationSampleApp-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + F28D7493DD28A2A2A72AF1FC /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-SegmentationSampleApp/Pods-SegmentationSampleApp-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-SegmentationSampleApp/Pods-SegmentationSampleApp-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SegmentationSampleApp/Pods-SegmentationSampleApp-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 5B4DE09D22F8D7B100B55A7B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5B4DE0A922F8D7B100B55A7B /* ViewController.m in Sources */, + 5B4DE0B422F8D7B300B55A7B /* main.m in Sources */, + 5B4DE0A622F8D7B100B55A7B /* AppDelegate.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 5B4DE0AA22F8D7B100B55A7B /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 5B4DE0AB22F8D7B100B55A7B /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 5B4DE0AF22F8D7B300B55A7B /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 5B4DE0B022F8D7B300B55A7B /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 5B4DE0B522F8D7B300B55A7B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 5B4DE0B622F8D7B300B55A7B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 5B4DE0B822F8D7B300B55A7B /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 23212B47B363359777FCF914 /* Pods-SegmentationSampleApp.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = EQHXZ8M8AV; + INFOPLIST_FILE = SegmentationSampleApp/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.google.firebase.config.testapp.dev; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "Firebase Remote Config TestApp Dev"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 5B4DE0B922F8D7B300B55A7B /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 2B8D0D05EDF54422ACEF8C82 /* Pods-SegmentationSampleApp.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = EQHXZ8M8AV; + INFOPLIST_FILE = SegmentationSampleApp/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.google.firebase.config.testapp.dev; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "Firebase Remote Config TestApp Dev"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 5B4DE09C22F8D7B100B55A7B /* Build configuration list for PBXProject "SegmentationSampleApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5B4DE0B522F8D7B300B55A7B /* Debug */, + 5B4DE0B622F8D7B300B55A7B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 5B4DE0B722F8D7B300B55A7B /* Build configuration list for PBXNativeTarget "SegmentationSampleApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5B4DE0B822F8D7B300B55A7B /* Debug */, + 5B4DE0B922F8D7B300B55A7B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 5B4DE09922F8D7B100B55A7B /* Project object */; +} diff --git a/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/AppDelegate.h b/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/AppDelegate.h new file mode 100644 index 0000000000..3e55b05f9a --- /dev/null +++ b/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/AppDelegate.h @@ -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 + +@interface AppDelegate : UIResponder + +@property(strong, nonatomic) UIWindow *window; + +@end diff --git a/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/AppDelegate.m b/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/AppDelegate.m new file mode 100644 index 0000000000..f18fd63f19 --- /dev/null +++ b/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/AppDelegate.m @@ -0,0 +1,39 @@ +// Copyright 2019 Google +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "AppDelegate.h" +#import +#import "FirebaseSegmentation.h" + +@interface AppDelegate () + +@end + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + // Override point for customization after application launch. + [FIRApp configure]; + FIRSegmentation *segmentation = [FIRSegmentation segmentation]; + [segmentation setCustomInstallationID:@"mandard-test-custom-installation-id3" + completion:^(NSError *error) { + if (error) { + NSLog(@"Error! Could not set custom id"); + } + }]; + return YES; +} + +@end diff --git a/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..d8db8d65fd --- /dev/null +++ b/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/Assets.xcassets/Contents.json b/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/Base.lproj/LaunchScreen.storyboard b/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..bfa3612941 --- /dev/null +++ b/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/Base.lproj/Main.storyboard b/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/Base.lproj/Main.storyboard new file mode 100644 index 0000000000..942f0bc452 --- /dev/null +++ b/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/Base.lproj/Main.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/Info.plist b/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/Info.plist new file mode 100644 index 0000000000..11970f5aaf --- /dev/null +++ b/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + diff --git a/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/ViewController.h b/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/ViewController.h new file mode 100644 index 0000000000..9e02fefb3b --- /dev/null +++ b/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/ViewController.h @@ -0,0 +1,19 @@ +// 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 + +@interface ViewController : UIViewController + +@end diff --git a/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/ViewController.m b/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/ViewController.m new file mode 100644 index 0000000000..022e80dbfa --- /dev/null +++ b/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/ViewController.m @@ -0,0 +1,28 @@ +// 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 "ViewController.h" + +@interface ViewController () + +@end + +@implementation ViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + // Do any additional setup after loading the view, typically from a nib. +} + +@end diff --git a/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/main.m b/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/main.m new file mode 100644 index 0000000000..1100a936f0 --- /dev/null +++ b/FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/main.m @@ -0,0 +1,22 @@ +// 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 +#import "AppDelegate.h" + +int main(int argc, char* argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/FirebaseSegmentation/Tests/Unit/SEGContentManagerTests.m b/FirebaseSegmentation/Tests/Unit/SEGContentManagerTests.m new file mode 100644 index 0000000000..794ed39ac6 --- /dev/null +++ b/FirebaseSegmentation/Tests/Unit/SEGContentManagerTests.m @@ -0,0 +1,122 @@ +// 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 + +#import "FirebaseSegmentation/Sources/SEGContentManager.h" +#import "FirebaseSegmentation/Sources/SEGDatabaseManager.h" +#import "FirebaseSegmentation/Sources/SEGNetworkManager.h" + +#import +#import "FirebaseCore/Sources/Public/FirebaseCore/FirebaseCore.h" +#import "FirebaseInstallations/Source/Library/InstallationsIDController/FIRInstallationsIDController.h" +#import "FirebaseInstallations/Source/Library/Public/FirebaseInstallations/FirebaseInstallations.h" + +@interface SEGContentManager (ForTest) +- (instancetype)initWithDatabaseManager:databaseManager networkManager:networkManager; +@end + +@interface FIRInstallations (Tests) +@property(nonatomic, readwrite, strong) FIROptions *appOptions; +@property(nonatomic, readwrite, strong) NSString *appName; + +- (instancetype)initWithAppOptions:(FIROptions *)appOptions + appName:(NSString *)appName + installationsIDController:(FIRInstallationsIDController *)installationsIDController + prefetchAuthToken:(BOOL)prefetchAuthToken; +@end + +@interface FIRInstallationsAuthTokenResult (ForTest) +- (instancetype)initWithToken:(NSString *)token expirationDate:(NSDate *)expirationTime; +@end + +@interface SEGContentManagerTests : XCTestCase +@property(nonatomic) SEGContentManager *contentManager; +@property(nonatomic) id networkManagerMock; +@property(nonatomic) id mockIDController; +@property(nonatomic) FIROptions *appOptions; +@property(readonly) NSString *firebaseAppName; +@property(strong, readonly, nonatomic) id mockInstallations; + +@end + +@implementation SEGContentManagerTests + +- (void)setUp { + // Setup FIRApp. + _firebaseAppName = @"my-firebase-app-id"; + XCTAssertNoThrow([FIRApp configureWithName:self.firebaseAppName options:[self FIRAppOptions]]); + + // Installations Mock + NSString *FID = @"fid-is-better-than-iid"; + _mockInstallations = OCMClassMock([FIRInstallations class]); + OCMStub([_mockInstallations installationsWithApp:[FIRApp appNamed:self.firebaseAppName]]) + .andReturn(_mockInstallations); + FIRInstallationsAuthTokenResult *FISToken = + [[FIRInstallationsAuthTokenResult alloc] initWithToken:@"fake-fis-token" expirationDate:nil]; + OCMStub([_mockInstallations + installationIDWithCompletion:([OCMArg invokeBlockWithArgs:FID, [NSNull null], nil])]); + OCMStub([_mockInstallations + authTokenWithCompletion:([OCMArg invokeBlockWithArgs:FISToken, [NSNull null], nil])]); + + // Mock the network manager. + FIROptions *options = [[FIROptions alloc] init]; + options.projectID = @"test-project-id"; + options.APIKey = @"test-api-key"; + self.networkManagerMock = OCMClassMock([SEGNetworkManager class]); + OCMStub([self.networkManagerMock + makeAssociationRequestToBackendWithData:[OCMArg any] + token:[OCMArg any] + completion:([OCMArg + invokeBlockWithArgs:@YES, [NSNull null], nil])]); + + // Initialize the content manager. + self.contentManager = + [[SEGContentManager alloc] initWithDatabaseManager:[SEGDatabaseManager sharedInstance] + networkManager:self.networkManagerMock]; +} + +- (void)tearDown { + [self.networkManagerMock stopMocking]; + self.networkManagerMock = nil; + self.contentManager = nil; + self.mockIDController = nil; +} + +// Associate a fake custom installation id and fake firebase installation id. +// TODO(mandard): check for result and add more tests. +- (void)testAssociateCustomInstallationIdentifierSuccessfully { + XCTestExpectation *expectation = + [self expectationWithDescription:@"associateCustomInstallation for contentmanager"]; + [_contentManager + associateCustomInstallationIdentiferNamed:@"my-custom-id" + firebaseApp:self.firebaseAppName + completion:^(BOOL success, NSDictionary *result) { + XCTAssertTrue(success, + @"Could not associate custom installation ID"); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:10 handler:nil]; +} + +#pragma mark private + +- (FIROptions *)FIRAppOptions { + FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:@"1:123:ios:123abc" + GCMSenderID:@"correct_gcm_sender_id"]; + options.APIKey = @"AIzaSaaaaaaaaaaaaaaaaaaaaaaaaaaa1111111"; + options.projectID = @"abc-xyz-123"; + return options; +} +@end diff --git a/FirebaseSegmentation/Tests/Unit/SEGDatabaseManagerTests.m b/FirebaseSegmentation/Tests/Unit/SEGDatabaseManagerTests.m new file mode 100644 index 0000000000..a1087540b0 --- /dev/null +++ b/FirebaseSegmentation/Tests/Unit/SEGDatabaseManagerTests.m @@ -0,0 +1,118 @@ +// 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 + +#import "FirebaseSegmentation/Sources/SEGDatabaseManager.h" + +#import + +@interface SEGDatabaseManager (Test) +- (NSString *)pathForSegmentationDatabase; +@end + +@interface SEGDatabaseManagerTests : XCTestCase { + long _expectationTimeout; +} +@property(nonatomic) id databaseManagerMock; + +@end + +@implementation SEGDatabaseManagerTests + +- (void)setUp { + // Override the database path to create a test database. + self.databaseManagerMock = OCMClassMock([SEGDatabaseManager class]); + OCMStub([self.databaseManagerMock pathForSegmentationDatabase]) + .andReturn([self pathForSegmentationTestDatabase]); + + // Expectation timeout for each test. + _expectationTimeout = 2; +} + +- (void)tearDown { + [self.databaseManagerMock stopMocking]; + self.databaseManagerMock = nil; +} + +- (void)testDatabaseCreateOrOpen { + XCTestExpectation *expectation = [self expectationWithDescription:@"testDatabaseLoad"]; + // Initialize the database manager. + SEGDatabaseManager *databaseManager = [[SEGDatabaseManager alloc] init]; + XCTAssertNotNil(databaseManager); + // Load all data from the database. + [databaseManager createOrOpenDatabaseWithCompletion:^(BOOL success, NSDictionary *result) { + XCTAssertTrue(success); + XCTAssertTrue(result.count == 0); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:_expectationTimeout handler:nil]; +} + +- (void)testDatabaseInsertAndRead { + XCTestExpectation *expectation = [self expectationWithDescription:@"testDatabaseLoad"]; + // Initialize the database manager. + SEGDatabaseManager *databaseManager = [[SEGDatabaseManager alloc] init]; + XCTAssertNotNil(databaseManager); + // Load all data from the database. + [databaseManager createOrOpenDatabaseWithCompletion:^(BOOL success, NSDictionary *result) { + XCTAssertTrue(success); + XCTAssertTrue(result.count == 0, "Result was %@", result); + + // Insert data. + [databaseManager + insertMainTableApplicationNamed:@"firebase_test_app" + customInstanceIdentifier:@"custom-123" + firebaseInstanceIdentifier:@"firebase-123" + associationStatus:kSEGAssociationStatusPending + completionHandler:^(BOOL success, NSDictionary *result) { + XCTAssertTrue(success); + XCTAssertNil(result); + + // Read data. + [databaseManager loadMainTableWithCompletion:^(BOOL success, + NSDictionary *result) { + XCTAssertTrue(success); + XCTAssertEqual(result.count, 1); + NSDictionary *associations = [result objectForKey:@"firebase_test_app"]; + XCTAssertNotNil(associations); + XCTAssertEqualObjects( + [associations objectForKey:kSEGCustomInstallationIdentifierKey], + @"custom-123"); + XCTAssertEqualObjects( + [associations objectForKey:kSEGFirebaseInstallationIdentifierKey], + @"firebase-123"); + XCTAssertEqualObjects( + [associations objectForKey:kSEGAssociationStatusKey], + kSEGAssociationStatusPending); + [expectation fulfill]; + }]; + }]; + }]; + [self waitForExpectationsWithTimeout:_expectationTimeout handler:nil]; +} + +#pragma mark Helpers +- (NSString *)pathForSegmentationTestDatabase { + NSArray *dirPaths = + NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); + NSString *appSupportPath = dirPaths.firstObject; + NSString *databaseName = + [NSString stringWithFormat:@"FirebaseSegmentation-test-%d.sqlite3", (arc4random() % 100)]; + NSArray *components = @[ appSupportPath, @"Google/FirebaseSegmentation", databaseName ]; + NSString *dbPath = [NSString pathWithComponents:components]; + NSLog(@"Created test database at: %@", dbPath); + return dbPath; +} +@end diff --git a/FirebaseSegmentation/Tests/Unit/SEGInitializationTests.m b/FirebaseSegmentation/Tests/Unit/SEGInitializationTests.m new file mode 100644 index 0000000000..030b325408 --- /dev/null +++ b/FirebaseSegmentation/Tests/Unit/SEGInitializationTests.m @@ -0,0 +1,53 @@ +// 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 + +#import "FirebaseCore/Sources/Public/FirebaseCore/FIRApp.h" +#import "FirebaseCore/Sources/Public/FirebaseCore/FIROptions.h" +#import "FirebaseSegmentation/Sources/Public/FIRSegmentation.h" + +@interface FIRSegmentation (ForTest) +- (instancetype)initWithAppName:(NSString *)appName FIROptions:(FIROptions *)options; +@end + +@interface SEGInitializationTests : XCTestCase { + FIRSegmentation *_segmentation; +} + +@end + +@implementation SEGInitializationTests + +- (void)setUp { + FIROptions *options = [[FIROptions alloc] init]; + options.APIKey = @"test-api-key"; + options.projectID = @"test-firebase-project-id"; + _segmentation = [[FIRSegmentation alloc] initWithAppName:@"test-firebase-app-name" + FIROptions:options]; +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the + // class. +} + +- (void)testExample { + [_segmentation setCustomInstallationID:@"test-custom-id" + completion:^(NSError *error){ + + }]; +} + +@end diff --git a/scripts/check.sh b/scripts/check.sh index b21e8be4a4..a308ba70e1 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -74,7 +74,6 @@ EXAMPLES: EOF } - set -euo pipefail unset CDPATH diff --git a/scripts/if_changed.sh b/scripts/if_changed.sh index babebb5dfa..a4bb566f55 100755 --- a/scripts/if_changed.sh +++ b/scripts/if_changed.sh @@ -55,8 +55,9 @@ else 'InAppMessaging|Firebase/InAppMessaging|'\ 'FirebaseInAppMessaging.podspec|'\ 'Firebase/InstanceID|FirebaseInstanceID.podspec|'\ -'FirebaseInstallations'\ -'FirebaseCrashlytics.podspec)'\ +'FirebaseInstallations|'\ +'FirebaseCrashlytics.podspec|'\ +'FirebaseSegmentation.podspec)'\ ;; FirebasePod-*) @@ -146,6 +147,10 @@ else check_changes '^(FirebaseCore|GoogleUtilities|FirebaseInstallations)' ;; + Segmentation-*) + check_changes '^(Firebase/Core|FirebaseSegmentation|FirebaseSegmentation.podspec)' + ;; + *) echo "Unknown project-method combo" 1>&2 echo " PROJECT=$PROJECT" 1>&2