Просмотр исходного кода

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 <dmandar@users.noreply.github.com>
Co-authored-by: ChaoqunCHEN <cqchen93@gmail.com>
Di Wu 5 лет назад
Родитель
Сommit
69bfe3fd3c
34 измененных файлов с 2477 добавлено и 3 удалено
  1. 3 0
      .gitignore
  2. 4 0
      Dangerfile
  3. 45 0
      FirebaseSegmentation.podspec
  4. 0 0
      FirebaseSegmentation/Sources/CHANGELOG.md
  5. 78 0
      FirebaseSegmentation/Sources/FIRSegmentation.m
  6. 107 0
      FirebaseSegmentation/Sources/FIRSegmentationComponent.m
  7. 58 0
      FirebaseSegmentation/Sources/Private/FIRSegmentationComponent.h
  8. 30 0
      FirebaseSegmentation/Sources/Private/FIRSegmentationInternal.h
  9. 77 0
      FirebaseSegmentation/Sources/Public/FIRSegmentation.h
  10. 17 0
      FirebaseSegmentation/Sources/Public/FirebaseSegmentation.h
  11. 34 0
      FirebaseSegmentation/Sources/SEGContentManager.h
  12. 172 0
      FirebaseSegmentation/Sources/SEGContentManager.m
  13. 54 0
      FirebaseSegmentation/Sources/SEGDatabaseManager.h
  14. 417 0
      FirebaseSegmentation/Sources/SEGDatabaseManager.m
  15. 34 0
      FirebaseSegmentation/Sources/SEGNetworkManager.h
  16. 223 0
      FirebaseSegmentation/Sources/SEGNetworkManager.m
  17. 52 0
      FirebaseSegmentation/Sources/SEGSegmentationConstants.h
  18. 15 0
      FirebaseSegmentation/Tests/Sample/Podfile
  19. 426 0
      FirebaseSegmentation/Tests/Sample/SegmentationSampleApp.xcodeproj/project.pbxproj
  20. 21 0
      FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/AppDelegate.h
  21. 39 0
      FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/AppDelegate.m
  22. 98 0
      FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/Assets.xcassets/AppIcon.appiconset/Contents.json
  23. 6 0
      FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/Assets.xcassets/Contents.json
  24. 25 0
      FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/Base.lproj/LaunchScreen.storyboard
  25. 24 0
      FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/Base.lproj/Main.storyboard
  26. 49 0
      FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/Info.plist
  27. 19 0
      FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/ViewController.h
  28. 28 0
      FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/ViewController.m
  29. 22 0
      FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/main.m
  30. 122 0
      FirebaseSegmentation/Tests/Unit/SEGContentManagerTests.m
  31. 118 0
      FirebaseSegmentation/Tests/Unit/SEGDatabaseManagerTests.m
  32. 53 0
      FirebaseSegmentation/Tests/Unit/SEGInitializationTests.m
  33. 0 1
      scripts/check.sh
  34. 7 2
      scripts/if_changed.sh

+ 3 - 0
.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

+ 4 - 0
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
 

+ 45 - 0
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

+ 0 - 0
FirebaseSegmentation/Sources/CHANGELOG.md


+ 78 - 0
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<FIRSegmentationProvider> 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

+ 107 - 0
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<FIRComponent *> *)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

+ 58 - 0
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 <Foundation/Foundation.h>
+
+#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<NSString *, FIRSegmentation *> *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 <FIRSegmentationProvider, FIRLibrary>
+
+/// 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

+ 30 - 0
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 <Foundation/Foundation.h>
+
+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

+ 77 - 0
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 <Foundation/Foundation.h>
+
+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

+ 17 - 0
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"

+ 34 - 0
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 <Foundation/Foundation.h>
+
+#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

+ 172 - 0
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<NSString *, id> *_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<NSString *, NSString *> *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<NSString *, id> *result) {
+                                     // TODO: log, update database.
+                                     completionHandler(status, result);
+                                   }];
+}
+
+@end

+ 54 - 0
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 <Foundation/Foundation.h>
+
+#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

+ 417 - 0
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 <sqlite3.h>
+#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<NSString *, NSDictionary<NSString *, NSString *> *> *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<NSString *, NSString *> *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<NSString *, NSDictionary<NSString *, NSString *> *> *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<NSString *, NSString *> *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<NSString *> *dirPaths =
+      NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
+  NSString *appSupportPath = dirPaths.firstObject;
+  NSArray<NSString *> *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<NSString *> *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<NSString *> *)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

+ 34 - 0
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 <Foundation/Foundation.h>
+
+#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<NSString *, id> *)associationData
+                                          token:(NSString *)token
+                                     completion:(SEGRequestCompletion)completionHandler;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 223 - 0
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<NSString *, NSString *> *)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<NSString *, NSString *> *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<NSString *, NSString *> *)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

+ 52 - 0
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 <Foundation/Foundation.h>
+
+#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<NSString*, id>* result);
+
+#endif /* SEGSegmentationConstants_h */

+ 15 - 0
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

+ 426 - 0
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 = "<group>"; };
+		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 = "<group>"; };
+		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 = "<group>"; };
+		5B4DE0A522F8D7B100B55A7B /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
+		5B4DE0A722F8D7B100B55A7B /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = "<group>"; };
+		5B4DE0A822F8D7B100B55A7B /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = "<group>"; };
+		5B4DE0AB22F8D7B100B55A7B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
+		5B4DE0AD22F8D7B300B55A7B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+		5B4DE0B022F8D7B300B55A7B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
+		5B4DE0B222F8D7B300B55A7B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		5B4DE0B322F8D7B300B55A7B /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
+		5B4DE0BA22F8DA1200B55A7B /* SecondApp-GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "SecondApp-GoogleService-Info.plist"; sourceTree = "<group>"; };
+		5B4DE0BB22F8DA1300B55A7B /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
+		5B4DE0BF22F8E07200B55A7B /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "../../../../../../configdaily googservice/GoogleService-Info.plist"; sourceTree = "<group>"; };
+		5B4DE0C222F8E09900B55A7B /* SecondApp-GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "SecondApp-GoogleService-Info.plist"; sourceTree = "<group>"; };
+		5B4DE0C322F8E09900B55A7B /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
+/* 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 = "<group>";
+		};
+		2074D9A873FD02A71321C678 /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+				5B4DE0BF22F8E07200B55A7B /* GoogleService-Info.plist */,
+				03FB7E52AF4C6B5EAA580F43 /* Pods_SegmentationSampleApp.framework */,
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
+		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 = "<group>";
+		};
+		5B4DE0A222F8D7B100B55A7B /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				5B4DE0A122F8D7B100B55A7B /* SegmentationSampleApp.app */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		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 = "<group>";
+		};
+/* 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 = "<group>";
+		};
+		5B4DE0AF22F8D7B300B55A7B /* LaunchScreen.storyboard */ = {
+			isa = PBXVariantGroup;
+			children = (
+				5B4DE0B022F8D7B300B55A7B /* Base */,
+			);
+			name = LaunchScreen.storyboard;
+			sourceTree = "<group>";
+		};
+/* 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 */;
+}

+ 21 - 0
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 <UIKit/UIKit.h>
+
+@interface AppDelegate : UIResponder <UIApplicationDelegate>
+
+@property(strong, nonatomic) UIWindow *window;
+
+@end

+ 39 - 0
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 <FirebaseCore/FirebaseCore.h>
+#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

+ 98 - 0
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"
+  }
+}

+ 6 - 0
FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/Assets.xcassets/Contents.json

@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}

+ 25 - 0
FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/Base.lproj/LaunchScreen.storyboard

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
+    <dependencies>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
+        <capability name="Safe area layout guides" minToolsVersion="9.0"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <scenes>
+        <!--View Controller-->
+        <scene sceneID="EHf-IW-A2E">
+            <objects>
+                <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+                    <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+                        <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                        <viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
+                    </view>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="53" y="375"/>
+        </scene>
+    </scenes>
+</document>

+ 24 - 0
FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/Base.lproj/Main.storyboard

@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
+    <dependencies>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
+        <capability name="Safe area layout guides" minToolsVersion="9.0"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <scenes>
+        <!--View Controller-->
+        <scene sceneID="tne-QT-ifu">
+            <objects>
+                <viewController id="BYZ-38-t0r" customClass="ViewController" customModuleProvider="" sceneMemberID="viewController">
+                    <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
+                        <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                        <viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
+                    </view>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
+            </objects>
+        </scene>
+    </scenes>
+</document>

+ 49 - 0
FirebaseSegmentation/Tests/Sample/SegmentationSampleApp/Info.plist

@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>$(DEVELOPMENT_LANGUAGE)</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>APPL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+	<key>LSRequiresIPhoneOS</key>
+	<true/>
+	<key>UILaunchStoryboardName</key>
+	<string>LaunchScreen</string>
+	<key>UIMainStoryboardFile</key>
+	<string>Main</string>
+	<key>UIRequiredDeviceCapabilities</key>
+	<array>
+		<string>armv7</string>
+	</array>
+	<key>UISupportedInterfaceOrientations</key>
+	<array>
+		<string>UIInterfaceOrientationPortrait</string>
+		<string>UIInterfaceOrientationLandscapeLeft</string>
+		<string>UIInterfaceOrientationLandscapeRight</string>
+	</array>
+	<key>UISupportedInterfaceOrientations~ipad</key>
+	<array>
+		<string>UIInterfaceOrientationPortrait</string>
+		<string>UIInterfaceOrientationPortraitUpsideDown</string>
+		<string>UIInterfaceOrientationLandscapeLeft</string>
+		<string>UIInterfaceOrientationLandscapeRight</string>
+	</array>
+  <key>NSAppTransportSecurity</key>
+  <dict>
+    <key>NSAllowsArbitraryLoads</key><true/>
+  </dict>
+</dict>
+</plist>

+ 19 - 0
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 <UIKit/UIKit.h>
+
+@interface ViewController : UIViewController
+
+@end

+ 28 - 0
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

+ 22 - 0
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 <UIKit/UIKit.h>
+#import "AppDelegate.h"
+
+int main(int argc, char* argv[]) {
+  @autoreleasepool {
+    return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
+  }
+}

+ 122 - 0
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 <XCTest/XCTest.h>
+
+#import "FirebaseSegmentation/Sources/SEGContentManager.h"
+#import "FirebaseSegmentation/Sources/SEGDatabaseManager.h"
+#import "FirebaseSegmentation/Sources/SEGNetworkManager.h"
+
+#import <OCMock/OCMock.h>
+#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

+ 118 - 0
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 <XCTest/XCTest.h>
+
+#import "FirebaseSegmentation/Sources/SEGDatabaseManager.h"
+
+#import <OCMock/OCMock.h>
+
+@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

+ 53 - 0
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 <XCTest/XCTest.h>
+
+#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

+ 0 - 1
scripts/check.sh

@@ -74,7 +74,6 @@ EXAMPLES:
 
 EOF
 }
-
 set -euo pipefail
 unset CDPATH
 

+ 7 - 2
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