Переглянути джерело

User defaults (#6907)

* Restores ML Pods after M77.

* Initial structure for Swift library.

* Custom model (#6594)

* Initial commit for CustomModel class.

* Initial commit for CustomModel class.

* API scaffolding for CustomModel and ModelDownloader classes

* API scaffolding for CustomModel and ModelDownloader classes

* Delete ModelDownloadConditions.swift

* Changed Swift structs to NSObject subclasses for objc visibility

* Design for a Swift-first SDK

* Design for a Swift-first SDK

* Design for a Swift-first SDK

* Updated design w/ errors and download progress handler

* Style compliance

* Refactor Test folder structure

* Refactor Test folder structure

* Fix pod lint errors

* Fix pod lint errors

* Add xcscheme for SwiftPM builds.

* Disable catalyst temporarily, re-enable issue #6790

* Better classification of download errors

* Documentation comments for custom model

* Refactor errors for model downloading

Co-authored-by: Ryan Wilson <wilsonryan@google.com>

* Update to Firebase 7.0 and add Installations dependency

* ML master update (#6891)

* Restores ML Pods after M77.

* Remove deprecated sendLogsWithServiceName (#6437)

* Remove deprecated pods from Firebase.h and Firebase.podspec (#6438)

* Remove the FCM Direct Channel API from Messaging (#6430)

* Remove unnecessary Core private headers (#6439)

* Update SPM docs to current version (#6524)

* Update podspecs to min iOS 10 (#6517)

* Merging the 6.33.0 release into master (#6523)

* Update versions for Release 6.33.0

* SPM M80(6.33.0) Analytics update (#6490)

Co-authored-by: Paul Beusterien <paulbeusterien@google.com>

* Merge 6.33 SPM fixes back to master (#6530)

* SPM M80(6.33.0) Analytics update (#6490)
* Remove unnecessary analytics public header copy (#6498)
* Fix SPM analytics warning introduced yesterday (#6504)
* Fix SPM version (#6527)

* Import Spec tests (#6525)

* Import Spec tests

* Init

* Add recovery specs

* Update FSTSyncEngineTestDriver.mm

* Add release candidate prerelease workflow. (#6487)

This also update cocoapods source to cocoapods repo since cdn is failed to connect.

* Move internal Crashlytics changes to Github (#6535)

Move *most* internal Crashlytics changes to Github

* GDTCORFlatFileStorageTest size limit tests: use timeout proportional to the number of events (#6537)

* IID: repo-relative headers (#6539)

* Use Xcode 12 in CI instead of the beta (#6542)

* SwiftPM for FCM (#6541)

* Fix a rare RC crash (#6556)

* Refactoring archive testing to a single yml file. (#6510)

* Refactoring archive testing to a single yml file.

This reduces the duplication in each of the workflow files. This also
will make archive testing with SwiftPM more straightforward.

* Prefixed with 'pod' name

* Commenting out to run archive testing on PR for now.

* Put array on single line

The yml won't be parsed unless the `pod` array is on a single line.

* Fix to run on PRs to the yml itself.

* Fix naming of AppDistro

* Remove schemes.

* Update versions automation (#6550)

* Remove iOS 10.0 check since minimum support in Messaging is already 10.0 (#6559)

* Add Performance podspec (#6526)

* Integrate Firestore with Firebase platform logging (#6507)

The sample user agent string I'm getting in XCode is `apple-platform/ios apple-sdk/17E8258 fire-fst/1.17.1 fire-ios/6.10.2 swift/true xcode/11E503a`.

* Change lastFetchTime to be readonly (#6567)

* Change lastFetchTime to be readonly

* Add changelog

* GDTCORDirectorySizeTracker: fix NSURL constructor (#6580)

* GDTCORDirectorySizeTracker: replace optional NSURL constructor with non-optional.

* GDTCORDirectorySizeTracker tests for the issue

* ./scripts/style.sh

* changelog

* Upload Symbols 3.4 with performance improvements (#6583)

* Stop requiring pods to be static frameworks (#6557)

* Always send the user agent regardless of the heartbeat value (#6592)

Any non-zero heartbeat value is immediately reset to zero upon retrieval. Thus, should the network request containing the heartbeat value and the user agent string fail, the previous logic would prevent us from sending the user agent string until the next day (when the heartbeat once again returns a non-zero value). Until this is resolved, always send the user agent string, regardless of the heartbeat value. The associated increase in bandwidth usage is minor (~1.2% in _total_ traffic from running the full suite of Firestore integration tests, which represent the worst-case scenario because they recreate the streams much more often than a typical application).

* Make all GoogleUtilities APIs public (#6588)

* Make FirestoreException a type defined by Firestore/core (#6589)

Move FirestoreInternalError to firebase::firestore to match
FirestoreException in Android.

When importing this into google3, we'll have to delete the
Android-specific version of this type.

* Update CHANGELOG for Firestore v1.19.0 (#6601)

* Remove deprecated method in ABT public header (#6602)

* Remove deprecated method, implementation

* Replace call to deprecated method in RC, fix tests

* Update changelogs

* Add InstanceID deprecation warning (#6585)

* Firebase 7 Firebase.podspec versions (#6604)

* Add a changelog entry for Firestore platform logging (#6603)

* 6.34.0 CHANGELOG update (#6611)

* Migrate FIRLoggerServices strings from Core to clients (#6608)

* Add static-framework testing to CI (#6599)

* FIS: Additional FIRInstallationsItem validation (#6570)

* FIS API tests for no FID in response

* FIRInstallationsIDControllerTests: test names

* FIRInstallationsIDControllerTests: corrupted storage tests

* FIRInstallationsItem validation

* Fix FIRInstallationsItem.IIDDefaultToken copy

* Improve error description.

* FIRInstallationsItem validation error

* FIRInstallationsItem validation tests

* ./scripts/style.sh

* FIRInstallationsIDController: validate stored installation

* FIRInstallationsAPIService installation validation

* Changelog

* Update versions for Release 6.34.0

* 6.34.0 updates for SwiftPM (#6614)

* M81 FIS cherry pick of #6570 (#6616)

* FIS: Additional FIRInstallationsItem validation (#6570)

* FIS API tests for no FID in response

* FIRInstallationsIDControllerTests: test names

* FIRInstallationsIDControllerTests: corrupted storage tests

* FIRInstallationsItem validation

* Fix FIRInstallationsItem.IIDDefaultToken copy

* Improve error description.

* FIRInstallationsItem validation error

* FIRInstallationsItem validation tests

* ./scripts/style.sh

* FIRInstallationsIDController: validate stored installation

* FIRInstallationsAPIService installation validation

* Changelog

* Update versions

* patch version

* release manifest

* Add link to App Distribution docs page (#6620)

* Add link to App Distribution docs page

* Remove trailing whitespaace

* FIS IID tests shouldn't use repo-relative imports

* Clean up usage of deprecated methods in unit tests (#6618)

* Clean up usage of deprecated methods in unit tests

* Add back second namespace tests

* Remove test for setDefaults:namespace:

* Remove deprecated auth APIs (#6607)

* Remove deprecated APIs from public header.

* Remove deprecated API implementations.

* Also remove deprecated APIs from FIRUser.

* Update unit tests.

* Update sample app.

* Update changelog.

* Use MIMEType for image download extension if not present in resource URL (#6591)

* ZipBuilder Firebase 7 updates (#6629)

* Change numberValue to be nonnull (#6623)

* Change numberValue to be nonnull

* Add changelog

* Add auth emulator support to public API. (#6624)

* Add auth emulator support to public API.

* Update changelog.

* FIS: don't log anything on success validation (#6635) (#6636)

* Remove the `FirebaseOptions()` bare initializer. (#6633)

* Remove the `FirebaseOptions()` bare initializer.

This was unintentionally surfaced from the ObjC header and should be
removed.

Note: although the existing initializer doesn't work, this is still a
breaking change.

* Style

* Changelog update.

* Remove deprecated elements of FIAM API (#6617)

* Remove deprecated FIAM API

* Remove call to deprecated method

* Fix imports

* Modify view controllers to use dummy test subclasses for message objects

* Add bridging header, include it in test app

* Add CHANGELOG entry

* Revert "Add CHANGELOG entry"

This reverts commit 55c0bbcff5439848150b431e145ba5d2ae53ed78.

* Add CHANGELOG entry

* Swift formatting

* Add LLC to copyright notice

* Revert "Fix imports"

This reverts commit 55fdbbd3e0c7eac9ff0b07ae9a2b3d062ffd414e.

* FIS: disable IID migration tests by default (#6619)

* FIS: disable IID migration tests by default

* remove redundant env variable

* Fix merge errors

* 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>

* Remove deprecated RC APIs (#6637)

* Remove deprecated method declarations and implementations

* Fix broken dangerfile (#6642)

* GDT: Move GDTCORAssert.h to internal headers (#6638)

* Move GDTCORAssert.h to internal headers

* Fix import

* Update check_imports check (#6640)

* Stop using CocoaPods private headers (#6572)

* Add check for unauthenticated error and sign out in fetchNewLatestRelease (#6648)

* Add check for unauthenticated error and sign out in fetchNewRelase method

* Update CHANGELOG

* Add check for Tester sign in before calling fetch

* One Firebase version (#6634)

* Update Symbol Collision Test for Firebase 7 (#6656)

* Fix ZipBuilder crash and warning (#6653)

* Public Version API (#6651)

* Bump nanopb to fix Xcode 12 warning (#6659)

* Fix breakage from #6651 (#6660)

* GDT: move some headers from public to internal (#6643)

* Move extra public headers to Internal and fix imports

* Cleanup

* Use relative paths in internal headers

* Fix imports in internal headers

* ./scripts/style.sh

* Fix GDTTestApp

* Fix test app

* Use real uploader for watchOS test app

* comma

* Update the sample project link. (#6661)

* Simplify FirebaseCore imports (#6664)

* Call fetchAndActivate completion handler on main thread (#6665)

* Call fetchAndActivate completion handler on main thread

* Add changelog

* Update changelog to use issue number

* Missing Foundation import (#6670)

* Remove duplicate section for instructions. (#6668)

* FirebaseInstallations import added to Firebase.h (#4908)

* FirebaseInstallations import added to Firebase.h

* Add Installations to Firebase.podspec

* UI_USER_INTERFACE_IDIOM() was deprecated in iOS 14 (#6576)

* Update FirebaseAnalytics to the latest version from the release branch. (#6675)

* Remove deprecated -[FIRInstanceID appInstanceID:] (#6677)

* Remove deprecated -[FIRInstanceID appInstanceID:]

* changelog

* typo fix

* Messaging token refresh delegate should be able to return null token (#6647)

* Cherry-pick two 6.34.0 commits to master (#6688)

* Update changelogs (#6649)
* Speedup SwiftPM (#6687)

Co-authored-by: Morgan Chen <morganchen12@gmail.com>

* Fix a google3 build error (#6689)

* Fix an issue notification is not received during app first install (#6669)

* FIS: require projectID and valid API Key format (#6678)

* FIRInstallations validation: remove GCMSenderID fallback for missing projectID

* FIRInstallationsIDController: remove GCMSenderID fallback for  missing projectID

* Add API Key validation

* Changelog

* changelog

* Constant for API key lenght

* array] -> alloc] init]

* typo

* message

* ./scripts/style.sh

* Fix dummy API key in integration tests

* Update dummy API Keys to match the expected format

* debug test quickstart with no xcpretty

* fix debug

* Use debug QS repo branch

* print debug

* Add missing allowed characters `-`, `_`

* Nicer dummy API Keys

* Comment

* Revert "Use debug QS repo branch"

This reverts commit 69739dd6aee98d7b5c5b31d432c62a666a531a76.

* Revert "fix debug"

This reverts commit 84fcbc75436df89308064a5593fecaf2f8c984a3.

* Revert "debug test quickstart with no xcpretty"

This reverts commit 1fa21ed62a5c2e3366a1ba1d30fd2a4addea8eb8.

* C API for Firebase Version (#6690)

* Fix retain cycle build issue (#6693)

* Provide version for FDL 1P builds (#6679)

* Fix tvOS storage issues. (#6658)

* Fix tvOS storage issues.

On tvOS devices, the ApplicationSupport directory cannot be used.
Unfortunately this doesn't come up in simulator testing, only on device.
This hasn't been fully tested on device but given that any writes to the
ApplicationSupport directory fail on tvOS, this change can't break much.

Fixes #6612

@defagos can you please give this a test?

* Fixed changed dir name for RC

* Fixed test naming, too.

* Fix the documentation comment for GoogleUtilities.

* Updated changelog.

* Review feedback

* Fixed CHANGELOG location. (#6698)

* Fix CI/regression - update for change FCM API (#6699)

* Support paths with + in List API (#6700)

* Update API documentation referencing removed methods (#6667)

* Update API documentation to remove references to removed methods

* Add changelog

* Address comments

* Merge release-6.34.0 branch after patch (#6701)

* Update versions for Release 6.34.0

* 6.34.0 updates for SwiftPM (#6614)

* M81 FIS cherry pick of #6570 (#6616)

* FIS: Additional FIRInstallationsItem validation (#6570)

* FIS API tests for no FID in response

* FIRInstallationsIDControllerTests: test names

* FIRInstallationsIDControllerTests: corrupted storage tests

* FIRInstallationsItem validation

* Fix FIRInstallationsItem.IIDDefaultToken copy

* Improve error description.

* FIRInstallationsItem validation error

* FIRInstallationsItem validation tests

* ./scripts/style.sh

* FIRInstallationsIDController: validate stored installation

* FIRInstallationsAPIService installation validation

* Changelog

* Update versions

* patch version

* release manifest

* FIS: don't log anything on success validation (#6635)

* Update changelogs (#6649)

* GoogleDataTransport: deprecated API conditions (#6697)

* GoogleDataTransport: deprecated API conditions

* Version bump

* use `TARGET_OS_IOS`

* changelog

* changelog

* style

* Fix merge

Co-authored-by: Ryan Wilson <wilsonryan@google.com>
Co-authored-by: Paul Beusterien <paulbeusterien@google.com>
Co-authored-by: Morgan Chen <morganchen12@gmail.com>

* Refactor GULSwizzledObject to ARC to unblock SwiftPM support (#5862)

* Refactor GULSwizzledObject to ARC to unblock SwiftPM transition.

* run ./scripts/style.sh

* Assert a single swizzler per swizzled object.

* testMultiSwizzling: fix iOS 10 tests

* Changelog

* Cleanup

* Address floc review comments (#6691)

* Address floc review comments

* Fix style

* fix bad synthesize

* Remove segmentation constants from core

* fix style

* Allow Generic OAuth for Facebook and Apple when using the auth emulator. (#6702)

* Update NOTICES now that several FirebaseML pods are deleted (#6709)

* Log warning instead of failing SDK when discovering dangling target. (#6685)

* Log warning instead of failing SDK when discovering dangling target.

* Add unit test

* Delete old test.

* Initial SPM building for watchOS (#6705)

* Update testing account since the previous one is disabled for GHA. (#6652)

* Update the version of analytics to the latest released one. (#6562)

* Disable deprecation warning (#6512)

* Remove the `FIR` prefix from notification constants. (#6645)

* Remove the `FIR` prefix from notification constants.

* Enable Firebase 7 path in quickstart test

Co-authored-by: Paul Beusterien <paulbeusterien@google.com>

* Change instances of 'dimiss' to 'dismiss' (#6708)

* Use completion handler when updating experiments after activation (#6711)

* Use completion handler when updating experiments after activation

* Fix Travis

* Add changelog

* Fix whitespace

* Nonnull initializers

* Firebase user agent: additional fields for platform logging (#6429)

* Move firebase user agent logic to a separate class.

* FIRAppTest: prefix some tests with the tested method names

* FIRAppTest: user agent new fields tests

* New user agent field added.

* Headers

* API docs

* ./scripts/style.sh

* nullability fix

* Move environment info methods to GoogleUtilities

* GULAppEnvironmentUtilTest

* GULAppEnvironmentUtil swift tests

* FirebaseApp user agent swift flag test

* ./scripts/style.sh

* Cleanup CoreDiagnostics

* Nullable device model

* FIRCoreDiagnosticsTest fix

* Merge fixes

* Fix import

* rename

* Cleanup GUL imports (since all headers are public now)

* Don't allow logger version deallocation (#6719)

* s/withMaxResults/maxResults (#6714)

* Remove support for setting timestampsInSnapshotsEnabled (#6622)

Snapshots will now always include `FIRTimestamp` values. Use
`[FIRTimestamp dateValue]` to convert to `NSDate` if required.

`timestampsInSnapshotsEnabled` was deprecated in Firebase 5.16.0
(January 22, 2019) and has defaulted to true since then.

Projects that have not enabled `timestampsInSnapshotsEnabled` have
logged a warning since Firebase 4.12.0 (April 10, 2018).

* FIS API docs: use term "installation auth token" consistently. (#6014)

* FIS API docs: use term "Installation auth token" consistently.

* Changelog

* Capitalize

* Use lowercase

* SHA1 usage explanation

* missed upper case

* a -> an

* Minor RC cleanup (#6722)

* Cleanup developer mode variables

* Fix README link

* Use fully qualified namespace in test to avoid errors

* Fix whitespace

* Re-enable quickstart testing for FirebaseUI apps (#6725)

* Update GDT version for M82 (#6729)

* Update versions for Release 7.0.0

* ios version fixes

* Delete Test dependency

* Add manual trigger and update catching branch version. (#6732)

* One version release process (#6724)

* Missing iid handler checking cause crash (#6737)

* Fix spelling in header name (#6738)

* Travis GoogleUtilities :  skip Swift tests when --use-libraries (#6735)

* Travis GoogleUtilities :  skip Swift tests when --use-libraries

* Temporary enable cron tests for PRs to check

* Revert temp change

* Add update-tags release process (#6739)

* Firebase zip contents from FirebaseManifest instead of textproto (#6747)

* Add Carthage JSON manifests to git repo (#6742)

* Address #6747 review comments (#6749)

* CocoaPods CI update (#6751)

* CocoaPods CI update

* CocoaPods CI update

* Restore cron after test success

* FIS docs: whitelist -> allowlist (#6757)

* FIS docs: whitelist -> allowlist

* Changelog

* Changelog fix

* Revert accidently added GUL change

* GoogleUtilities: NSURLSession promise extension (#6753)

* GoogleUtilities: NSURLSession promise extension

* Imports fix

* Changelog

* API and API docs

* style

* Changelog fix

* Finish port of update-versions.py (#6758)

* Finish port of update-versions.py

* comments

* more comment

* Prevent test build errors under Xcode 12.1 (#6760)

Xcode 12 changed the XCTest assertions to be macros that no longer implicitly depended on `self`. It seems as if Xcode 12.1 has made -Wunused-lambda-capture part of our default warnings and this breaks Objective-C++ code that lambda captures `self` only for its implicit use with the XCTest macros.

This isn't common, so just suppress the warning by forcing `self` to appear used regardless of whether XCTest assertions reference `self` or not.

* Exclude FirebaseSegmentation in testing (#6736)

* Some Release tooling cleanup (#6761)

* Only run schedule actions on the main repo (#6770)

* Rename ReleaseTooling (#6772)

* Address two comments from last week's PRs (#6774)

* Source updates for ReleaseTooling rename (#6773)

* Fix a memory leak introduced in #6418 (#6778)

The root of the issue is that when serializing a singular filter, it is being treated as a unary filter before it is definitively established whether it is a unary filter or a field filter. The leak is caused by always serializing the unary filter's field path field for equality and non-equality filters -- if the filter's value turns out not to be NaN or null, the serialization code switches the filter's type to a field filter without clearing the partially-initialized unary filter. `pb_release` would not free the `unary_filter.field.field_path` member variable because it would consider the object not to be a unary filter.

Also a small refactoring to make the function easier to digest.

* Fix typo to fix zip cron (#6784)

* Fix cron logic for forks (#6785)

* Crashlytics update run script to quote all arguments passed (#6789)

* Adjust Storage Timeouts to GTM's retry interval (#6791)

* Add custom domain support to callable functions (#6787)

* Update to CocoaPods 1.10.0 (#6795)

* Restore zip cron test (#6798)

* Fixed broken callback to AppDelegate (#6800)

Fixed broken callback to AppDelegate after retrieving a dynamic link during fresh app start.
The AppDelegate call was broken in PR : #6517

Reverting to the old implementation of dynamic link passing to App delegate with changes to remove iOS 9 checks.
Using new "openURL" method instead of the deprecated one.
Clean up in the sample app pod file.

* Merge release-7.0 branch to master (#6797)

* Update cron tests for new Swift product names (#6804)

* Stop failing on warnings (#6807)

* Merge SPM updates back to master to fix CI (#6809)

* Add support for other Firebase products to integrate with Remote Config. (#6692)

* Update zip builder to handle xcframeworks from binary pods (#6815)

* Readme and Roadmap updates (#6816)

* Fix typo on publish command (#6831)

* Update SPM instructions for 7.0 (#6832)

* Fix publish order - RemoteConfig is a dependency of Performance (#6834)

* Post-release 7.0 merge to master  (#6836)

* Firebase user agent: add iOS on mac platform (#6799)

* Firebase user agent: add iOS on mac platform

* Older Xcode compatibility

* Fix check iOS version

* changelog

* Carthage updates for 7.0.0 (#6833)

* Adding test to make sure pending dynamic link is getting passed to App delegate method. (#6819)

Adding unit test to make sure the pending dynamic link is getting delivered to specific method in App delegate.

* Remove Component version from issue template (#6838)

* Run integration tests against the emulator by default. (#6852)

Remove support for running against Hexa, which we haven't done in years. Integration tests will now assume a local emulator by default.

Once imported into google3, this will make it significantly easier to run integration tests against cls from third-parties because this will be able to use the emulator we're already running rather than requiring us to build hexa again.

* Fix cache for check workflow (#6853)

* clarify error when logging settings failures (#6847)

Added additional logging when settings requests fail with a 404 status to help customers debug onboarding issues.

* Update Firestore macOS build for Xcode 12 (#6859)

* Remove usages of "whitelist" in Firestore. (#6846)

Fixes #6755.

* Allocate global objects so that their destructors don't run. (#6849)

Fixes #6844.

* Fix minor style issues uncovered during auditing (#6851)

* Remove explicit MobileCoreServices from podspecs (#6858)

* Change the Crashlytics version constant to read from FIRCore instead of from a build flag. (#6712)

* Get rid of an additional import of FIRVersion that I missed deleting (#6867)

* Merge #6860 to master (#6868)

* Fix warning introduced with Xcode 12 (#6865)

* GDTCCTUploader: lazy initialize url session, use ephemeral session (#6870)

* GDTCCTUploader: lazy initialize url session, use ephemeral session

* Changelog

* typo

* Update to Firebase 7.0 and add Installations dependency

* Fix lint errors

Co-authored-by: Paul Beusterien <paulbeusterien@google.com>
Co-authored-by: Chen Liang <chliang@google.com>
Co-authored-by: Maksym Malyhin <mmaksym@google.com>
Co-authored-by: Sebastian Schmidt <mrschmidt@google.com>
Co-authored-by: Gran <ollkorrect999@gmail.com>
Co-authored-by: Tejas Deshpande <tdeshpande@google.com>
Co-authored-by: Ryan Wilson <wilsonryan@google.com>
Co-authored-by: Konstantin Varlamov <var-const@users.noreply.github.com>
Co-authored-by: karenyz <58443706+karenyz@users.noreply.github.com>
Co-authored-by: Sam Edson <samedson@google.com>
Co-authored-by: Gil <mcg@google.com>
Co-authored-by: christibbs <43829046+christibbs@users.noreply.github.com>
Co-authored-by: Alex Singer <alexsinger@users.noreply.github.com>
Co-authored-by: Rosalyn Tan <rosalyntan@google.com>
Co-authored-by: akiva <akiva.bamberger@gmail.com>
Co-authored-by: Di Wu <49409954+diwu-arete@users.noreply.github.com>
Co-authored-by: dmandar <dmandar@users.noreply.github.com>
Co-authored-by: ChaoqunCHEN <cqchen93@gmail.com>
Co-authored-by: Morgan Chen <morganchen12@gmail.com>
Co-authored-by: Cleo Schneider <cleoschneider@google.com>
Co-authored-by: Yuchen Shi <yuchenshi@google.com>
Co-authored-by: Daisuke TONOSAKI <daisuke.t.jp@gmail.com>
Co-authored-by: wu-hui <53845758+wu-hui@users.noreply.github.com>
Co-authored-by: Jim Hays <54556994+jim-hays-root@users.noreply.github.com>
Co-authored-by: Sam Stern <samstern@google.com>
Co-authored-by: Eldhose M Babu <eldhosembabu@google.com>
Co-authored-by: vic-flair <vicflair4life@gmail.com>
Co-authored-by: Adam Duke <94930+adamvduke@users.noreply.github.com>

* Fix lint errors

* user defaults backed properties for model info + unit test

* fis token WIP

* get local model info WIP

* get local model info WIP

* Improvements to user defaults and FIS token + network tests

* Improvements to user defaults and FIS token + network tests

* Improvements to user defaults and FIS token + network tests

* Improvements to user defaults and FIS token + network tests

* TODOs for force-unwrapping + changing ModelInfo to class

Co-authored-by: Ryan Wilson <wilsonryan@google.com>
Co-authored-by: Paul Beusterien <paulbeusterien@google.com>
Co-authored-by: Chen Liang <chliang@google.com>
Co-authored-by: Maksym Malyhin <mmaksym@google.com>
Co-authored-by: Sebastian Schmidt <mrschmidt@google.com>
Co-authored-by: Gran <ollkorrect999@gmail.com>
Co-authored-by: Tejas Deshpande <tdeshpande@google.com>
Co-authored-by: Konstantin Varlamov <var-const@users.noreply.github.com>
Co-authored-by: karenyz <58443706+karenyz@users.noreply.github.com>
Co-authored-by: Sam Edson <samedson@google.com>
Co-authored-by: Gil <mcg@google.com>
Co-authored-by: christibbs <43829046+christibbs@users.noreply.github.com>
Co-authored-by: Alex Singer <alexsinger@users.noreply.github.com>
Co-authored-by: Rosalyn Tan <rosalyntan@google.com>
Co-authored-by: akiva <akiva.bamberger@gmail.com>
Co-authored-by: Di Wu <49409954+diwu-arete@users.noreply.github.com>
Co-authored-by: dmandar <dmandar@users.noreply.github.com>
Co-authored-by: ChaoqunCHEN <cqchen93@gmail.com>
Co-authored-by: Morgan Chen <morganchen12@gmail.com>
Co-authored-by: Cleo Schneider <cleoschneider@google.com>
Co-authored-by: Yuchen Shi <yuchenshi@google.com>
Co-authored-by: Daisuke TONOSAKI <daisuke.t.jp@gmail.com>
Co-authored-by: wu-hui <53845758+wu-hui@users.noreply.github.com>
Co-authored-by: Jim Hays <54556994+jim-hays-root@users.noreply.github.com>
Co-authored-by: Sam Stern <samstern@google.com>
Co-authored-by: Eldhose M Babu <eldhosembabu@google.com>
Co-authored-by: vic-flair <vicflair4life@gmail.com>
Co-authored-by: Adam Duke <94930+adamvduke@users.noreply.github.com>
manjanac 5 роки тому
батько
коміт
324f8cf6e0

+ 92 - 0
.github/workflows/mlmodeldownloader.yml

@@ -0,0 +1,92 @@
+name: mlmodeldownloader
+
+on:
+  pull_request:
+    paths:
+    - 'FirebaseMLModelDownloader**'
+    - '.github/workflows/mlmodeldownloader.yml'
+    - 'Gemfile'
+  schedule:
+    # Run every day at 11pm (PST) - cron uses UTC times
+    - cron:  '0 7 * * *'
+
+jobs:
+  pod-lib-lint:
+    # Don't run on private repo unless it is a PR.
+    if: github.repository != 'FirebasePrivate/firebase-ios-sdk' || github.event_name == 'pull_request'
+
+    runs-on: macOS-latest
+
+    strategy:
+      matrix:
+        target: [ios, tvos, macos]
+    steps:
+    - uses: actions/checkout@v2
+    - name: Setup Bundler
+      run: scripts/setup_bundler.sh
+    - name: Build and test
+      run: scripts/third_party/travis/retry.sh scripts/pod_lib_lint.rb FirebaseMLModelDownloader.podspec --platforms=${{ matrix.target }}
+
+  spm:
+    # Don't run on private repo unless it is a PR.
+    if: github.repository != 'FirebasePrivate/firebase-ios-sdk' || github.event_name == 'pull_request'
+    runs-on: macOS-latest
+    steps:
+    - uses: actions/checkout@v2
+    - name: Xcode 12
+      run: sudo xcode-select -s /Applications/Xcode_12.app/Contents/Developer
+    - name: Initialize xcodebuild
+      run: xcodebuild -list
+    - name: iOS Unit Tests
+      run: scripts/third_party/travis/retry.sh ./scripts/build.sh FirebaseMLModelDownloaderUnit iOS spm
+
+  spm-cron:
+    # Don't run on private repo.
+    if: github.event_name == 'schedule' && github.repository != 'FirebasePrivate/firebase-ios-sdk'
+    runs-on: macOS-latest
+    strategy:
+      matrix:
+        # TODO: manjanac@ Add catalyst back here.
+        target: [tvOS, macOS]
+    steps:
+    - uses: actions/checkout@v2
+    - name: Xcode 12
+      run: sudo xcode-select -s /Applications/Xcode_12.app/Contents/Developer
+    - name: Initialize xcodebuild
+      run: xcodebuild -list
+    - name: Unit Tests
+      run: scripts/third_party/travis/retry.sh ./scripts/build.sh FirebaseMLModelDownloaderUnit ${{ matrix.target }} spm
+
+  catalyst:
+    # Don't run on private repo unless it is a PR.
+    # TODO: manjanac@ Uncomment line below to re-enable catalyst.
+    # if: github.repository != 'FirebasePrivate/firebase-ios-sdk' || github.event_name == 'pull_request'
+    if: false
+    runs-on: macOS-latest
+    steps:
+    - uses: actions/checkout@v2
+    - name: Setup Bundler
+      run: scripts/setup_bundler.sh
+    - name: Setup project and Build Catalyst
+      run: scripts/test_catalyst.sh FirebaseMLModelDownloader build
+
+  mlmodeldownloader-cron-only:
+    # Don't run on private repo.
+    if: github.event_name == 'schedule' && github.repository != 'FirebasePrivate/firebase-ios-sdk'
+
+    runs-on: macos-latest
+    strategy:
+      matrix:
+        target: [ios, tvos, macos]
+        flags: [
+          '--use-modular-headers',
+          # Tests are skipped since the Swift tests need modules.
+          '--skip-tests --use-libraries'
+        ]
+    needs: pod-lib-lint
+    steps:
+    - uses: actions/checkout@v2
+    - name: Setup Bundler
+      run: scripts/setup_bundler.sh
+    - name: PodLibLint MLModelDownloader Cron
+      run: scripts/third_party/travis/retry.sh scripts/pod_lib_lint.rb FirebaseMLModelDownloader.podspec --platforms=${{ matrix.target }} ${{ matrix.flags }}

+ 52 - 0
.swiftpm/xcode/xcshareddata/xcschemes/FirebaseMLModelDownloaderUnit.xcscheme

@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1200"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "FirebaseMLModelDownloaderUnit"
+               BuildableName = "FirebaseMLModelDownloaderUnit"
+               BlueprintName = "FirebaseMLModelDownloaderUnit"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 48 - 0
FirebaseMLModelDownloader.podspec

@@ -0,0 +1,48 @@
+Pod::Spec.new do |s|
+  s.name             = 'FirebaseMLModelDownloader'
+  s.version          = '7.0.0'
+  s.summary          = 'Firebase ML Model Downloader'
+
+  s.description      = <<-DESC
+  This is the new ML Model Downloader CocoaPod.
+                       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 => 'MLModelDownloader-' + s.version.to_s
+  }
+  s.social_media_url = 'https://twitter.com/Firebase'
+  s.swift_version = '5.0'
+  s.ios.deployment_target = '10.0'
+  s.osx.deployment_target = '10.12'
+  s.tvos.deployment_target = '10.0'
+  s.watchos.deployment_target = '6.0'
+
+  s.cocoapods_version = '>= 1.4.0'
+  s.static_framework = true
+  s.prefix_header_file = false
+
+  s.source_files = [
+    'FirebaseMLModelDownloader/Sources/**/*.swift',
+  ]
+
+  s.framework = 'Foundation'
+  s.dependency 'FirebaseCore', '~> 7.0'
+  s.dependency 'FirebaseInstallations', '~> 7.0'
+
+  s.pod_target_xcconfig = {
+    'GCC_C_LANGUAGE_STANDARD' => 'c99',
+    'GCC_PREPROCESSOR_DEFINITIONS' => 'FIRMLModelDownloader_VERSION=' + s.version.to_s,
+    'OTHER_CFLAGS' => '-fno-autolink',
+  }
+
+  s.test_spec 'unit' do |unit_tests|
+    unit_tests.platforms = {:ios => '10.0', :osx => '10.12', :tvos => '10.0'}
+    unit_tests.source_files = 'FirebaseMLModelDownloader/Tests/Unit/**/*.swift'
+    unit_tests.requires_app_host = true
+  end
+end

+ 27 - 0
FirebaseMLModelDownloader/Sources/CustomModel.swift

@@ -0,0 +1,27 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import Foundation
+
+/// A custom model that is stored remotely on the server and downloaded to the device.
+public struct CustomModel: Hashable {
+  /// Name of the model.
+  public let name: String
+  /// Size of the custom model, provided by the server.
+  public let size: Int
+  /// Path where the model is stored on device.
+  public let path: String
+  /// Hash for the model, used for model verification.
+  public let hash: String
+}

+ 18 - 0
FirebaseMLModelDownloader/Sources/ModelDownloadConditions.swift

@@ -0,0 +1,18 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import Foundation
+
+/// Model download conditions.
+public struct ModelDownloadConditions {}

+ 90 - 0
FirebaseMLModelDownloader/Sources/ModelDownloader.swift

@@ -0,0 +1,90 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import Foundation
+
+/// Possible errors with model downloading.
+public enum DownloadError: Error, Equatable {
+  /// No model with this name found on server.
+  case notFound
+  /// Caller does not have necessary permissions for this operation.
+  case permissionDenied
+  /// Conditions not met to perform download.
+  case failedPrecondition
+  /// Not enough space for model on device.
+  case notEnoughSpace
+  /// Malformed model name.
+  case invalidArgument
+  /// Other errors with description.
+  case internalError(description: String)
+}
+
+/// Possible errors with locating model on device.
+public enum DownloadedModelError: Error {
+  /// File system error.
+  case fileIOError
+  /// Model not found on device.
+  case notFound
+}
+
+/// Possible ways to get a custom model.
+public enum ModelDownloadType {
+  /// Get local model stored on device.
+  case localModel
+  /// Get local model on device and update to latest model from server in the background.
+  case localModelUpdateInBackground
+  /// Get latest model from server.
+  case latestModel
+}
+
+/// Downloader to manage custom model downloads.
+public struct ModelDownloader {
+  /// Downloads a custom model to device or gets a custom model already on device, w/ optional handler for progress.
+  public func getModel(name modelName: String, downloadType: ModelDownloadType,
+                       conditions: ModelDownloadConditions,
+                       progressHandler: ((Float) -> Void)? = nil,
+                       completion: @escaping (Result<CustomModel, DownloadError>) -> Void) {
+    // TODO: Model download
+    let modelSize = Int()
+    let modelPath = String()
+    let modelHash = String()
+
+    let customModel = CustomModel(
+      name: modelName,
+      size: modelSize,
+      path: modelPath,
+      hash: modelHash
+    )
+    completion(.success(customModel))
+    completion(.failure(.notFound))
+  }
+
+  /// Gets all downloaded models.
+  public func listDownloadedModels(completion: @escaping (Result<Set<CustomModel>,
+    DownloadedModelError>) -> Void) {
+    let customModels = Set<CustomModel>()
+    // TODO: List downloaded models
+    completion(.success(customModels))
+    completion(.failure(.notFound))
+  }
+
+  /// Deletes a custom model from device.
+  public func deleteDownloadedModel(name modelName: String,
+                                    completion: @escaping (Result<Void, DownloadedModelError>)
+                                      -> Void) {
+    // TODO: Delete previously downloaded model
+    completion(.success(()))
+    completion(.failure(.notFound))
+  }
+}

+ 210 - 0
FirebaseMLModelDownloader/Sources/ModelInfoRetriever.swift

@@ -0,0 +1,210 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import Foundation
+import FirebaseCore
+import FirebaseInstallations
+
+/// Model info object with details about pending or downloaded model.
+class ModelInfo: NSObject {
+  /// Model name.
+  var name: String
+
+  /// User defaults associated with model.
+  var defaults: UserDefaults
+
+  // TODO: revisit UserDefaultsBacked
+  /// Download URL for the model file, as returned by server.
+  @UserDefaultsBacked var downloadURL: String
+
+  /// Hash of the model, as returned by server.
+  @UserDefaultsBacked var modelHash: String
+
+  /// Size of the model, as returned by server.
+  @UserDefaultsBacked var size: Int
+
+  /// Local path of the model.
+  @UserDefaultsBacked var path: String?
+
+  /// Initialize model info and create user default keys.
+  init(app: FirebaseApp, name: String, defaults: UserDefaults = .firebaseMLDefaults) {
+    self.name = name
+    self.defaults = defaults
+    let bundleID = Bundle.main.bundleIdentifier ?? ""
+    let defaultsPrefix = "\(bundleID).\(app.name).\(name)"
+    _downloadURL = UserDefaultsBacked(
+      key: "\(defaultsPrefix).model-download-url",
+      storage: defaults
+    )
+    _modelHash = UserDefaultsBacked(key: "\(defaultsPrefix).model-hash", storage: defaults)
+    _size = UserDefaultsBacked(key: "\(defaultsPrefix).model-size", storage: defaults)
+    _path = UserDefaultsBacked(key: "\(defaultsPrefix).model-path", storage: defaults)
+  }
+}
+
+/// Model info retriever for a model from local user defaults or server.
+class ModelInfoRetriever: NSObject {
+  /// Current Firebase app.
+  var app: FirebaseApp
+  /// Model info associated with model.
+  var modelInfo: ModelInfo?
+  /// Project id.
+  var projectID: String
+  /// Model name.
+  var modelName: String
+  /// Firebase installations.
+  var installations: Installations
+
+  /// Associate model info retriever with current Firebase app, project ID, and model name.
+  init(app: FirebaseApp, projectID: String, modelName: String) {
+    self.app = app
+    self.projectID = projectID
+    self.modelName = modelName
+    installations = Installations.installations(app: app)
+  }
+
+  /// Build custom model object from model info.
+  func buildModel() -> CustomModel? {
+    /// Build custom model only if model info is filled out, and model file is already on device.
+    guard let info = modelInfo, let path = info.path else { return nil }
+    let model = CustomModel(
+      name: info.name,
+      size: info.size,
+      path: path,
+      hash: info.modelHash
+    )
+    return model
+  }
+}
+
+/// Extension to handle fetching model info from server.
+extension ModelInfoRetriever {
+  /// HTTP request headers.
+  static let fisTokenHTTPHeader = "x-goog-firebase-installations-auth"
+  static let hashMatchHTTPHeader = "if-none-match"
+  static let bundleIDHTTPHeader = "x-ios-bundle-identifier"
+
+  /// HTTP response headers.
+  static let etagHTTPHeader = "ETag"
+
+  /// Error descriptions.
+  static let tokenErrorDescription = "Error retrieving FIS token."
+  static let selfDeallocatedErrorDescription = "Self deallocated."
+  static let missingModelHashErrorDescription = "Model hash missing in server response."
+  static let invalidHTTPResponseErrorDescription =
+    "Could not get a valid HTTP response from server."
+
+  /// Construct model fetch base URL.
+  var modelInfoFetchURL: URL {
+    var components = URLComponents()
+    components.scheme = "https"
+    components.host = "firebaseml.googleapis.com"
+    components.path = "/v1beta2/projects/\(projectID)/models/\(modelName):download"
+    // TODO: handle nil
+    return components.url!
+  }
+
+  /// Construct model fetch URL request.
+  func getModelInfoFetchURLRequest(token: String) -> URLRequest {
+    var request = URLRequest(url: modelInfoFetchURL)
+    request.httpMethod = "GET"
+    // TODO: Check if bundle ID needs to be part of the request header.
+    let bundleID = Bundle.main.bundleIdentifier ?? ""
+    request.setValue(bundleID, forHTTPHeaderField: ModelInfoRetriever.bundleIDHTTPHeader)
+    request.setValue(token, forHTTPHeaderField: ModelInfoRetriever.fisTokenHTTPHeader)
+    if let info = modelInfo, info.modelHash.count > 0 {
+      request.setValue(info.modelHash, forHTTPHeaderField: ModelInfoRetriever.hashMatchHTTPHeader)
+    }
+    return request
+  }
+
+  /// Get model info from server.
+  func downloadModelInfo(completion: @escaping (DownloadError?) -> Void) {
+    /// Get FIS token.
+    installations.authToken { [weak self] tokenResult, error in
+      guard let self = self else {
+        completion(.internalError(description: ModelInfoRetriever.selfDeallocatedErrorDescription))
+        return
+      }
+      guard let result = tokenResult
+      else {
+        completion(.internalError(description: ModelInfoRetriever.tokenErrorDescription))
+        return
+      }
+      /// Get model info fetch URL with appropriate HTTP headers.
+      let request = self.getModelInfoFetchURLRequest(token: result.authToken)
+
+      /// Download model info.
+      // TODO: Consider moving request to a separate method
+      let session = URLSession(configuration: .ephemeral)
+      let dataTask = session.dataTask(with: request) { [weak self]
+        data, response, error in
+        guard let self = self else {
+          completion(.internalError(description: ModelInfoRetriever
+              .selfDeallocatedErrorDescription))
+          return
+        }
+        if let downloadError = error {
+          completion(.internalError(description: downloadError.localizedDescription))
+        } else {
+          guard let httpResponse = response as? HTTPURLResponse else {
+            completion(.internalError(description: ModelInfoRetriever
+                .invalidHTTPResponseErrorDescription))
+            return
+          }
+
+          // TODO: Handle more http status codes
+          switch httpResponse.statusCode {
+          case 200:
+            guard let modelHash = httpResponse
+              .allHeaderFields[ModelInfoRetriever.etagHTTPHeader] as? String else {
+              completion(.internalError(description: ModelInfoRetriever
+                  .missingModelHashErrorDescription))
+              return
+            }
+
+            guard let data = data else {
+              completion(.internalError(description: ModelInfoRetriever
+                  .invalidHTTPResponseErrorDescription))
+              return
+            }
+            self.saveModelInfo(data: data, modelHash: modelHash)
+            completion(nil)
+          case 304:
+            completion(nil)
+          default:
+            completion(.notFound)
+          }
+        }
+      }
+      dataTask.resume()
+    }
+  }
+
+  /// Save model info to user defaults.
+  func saveModelInfo(data: Data, modelHash: String) {
+    // TODO: Save model info to user defaults
+    modelInfo?.modelHash = modelHash
+  }
+}
+
+/// Named user defaults for FirebaseML.
+extension UserDefaults {
+  static var firebaseMLDefaults: UserDefaults {
+    let suiteName = "com.google.firebase.ml"
+    // TODO: handle nil gracefully
+    let defaults = UserDefaults(suiteName: suiteName)!
+    return defaults
+  }
+}

+ 63 - 0
FirebaseMLModelDownloader/Sources/UserDefaultsBacked.swift

@@ -0,0 +1,63 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+private protocol OptionalProtocol {
+  var isNil: Bool { get }
+}
+
+extension Optional: OptionalProtocol {
+  public var isNil: Bool { self == nil }
+}
+
+/// Property initializer for user defaults. Value is always read from or written to a named user defaults store.
+@propertyWrapper struct UserDefaultsBacked<Value> {
+  let key: String
+  let defaultValue: Value
+  let storage: UserDefaults
+
+  var wrappedValue: Value {
+    get {
+      let value = storage.value(forKey: key) as? Value
+      return value ?? defaultValue
+    }
+    set {
+      if let optional = newValue as? OptionalProtocol, optional.isNil {
+        storage.removeObject(forKey: key)
+      } else {
+        storage.setValue(newValue, forKey: key)
+      }
+    }
+  }
+}
+
+/// Initialize and set default value for user default backed properties that can be optional (model path).
+extension UserDefaultsBacked where Value: ExpressibleByNilLiteral {
+  init(key: String, storage: UserDefaults) {
+    self.init(key: key, defaultValue: nil, storage: storage)
+  }
+}
+
+/// Initialize and set default value for user default backed properties that are strings (model download url, model hash).
+extension UserDefaultsBacked where Value: ExpressibleByStringLiteral {
+  init(key: String, storage: UserDefaults) {
+    self.init(key: key, defaultValue: "", storage: storage)
+  }
+}
+
+/// Initialize and set default value for user default backed properties that are int (model size).
+extension UserDefaultsBacked where Value: ExpressibleByIntegerLiteral {
+  init(key: String, storage: UserDefaults) {
+    self.init(key: key, defaultValue: 0, storage: storage)
+  }
+}

+ 160 - 0
FirebaseMLModelDownloader/Tests/Unit/ModelDownloaderTests.swift

@@ -0,0 +1,160 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import XCTest
+@testable import FirebaseCore
+@testable import FirebaseMLModelDownloader
+
+enum Constants {
+  enum App {
+    static let defaultName = "__FIRAPP_DEFAULT"
+    static let googleAppIDKey = "FIRGoogleAppIDKey"
+    static let nameKey = "FIRAppNameKey"
+    static let isDefaultAppKey = "FIRAppIsDefaultAppKey"
+  }
+
+  enum Options {
+    static let apiKey = "correct_api_key"
+    static let bundleID = "com.google.FirebaseSDKTests"
+    static let clientID = "correct_client_id"
+    static let trackingID = "correct_tracking_id"
+    static let gcmSenderID = "correct_gcm_sender_id"
+    static let projectID = "correct_project_id"
+    static let androidClientID = "correct_android_client_id"
+    static let googleAppID = "correct_app_id"
+    static let databaseURL = "https://abc-xyz-123.firebaseio.com"
+    static let deepLinkURLScheme = "comgoogledeeplinkurl"
+    static let storageBucket = "project-id-123.storage.firebase.com"
+    static let appGroupID: String? = nil
+  }
+}
+
+extension UserDefaults {
+  /// For testing: returns a new cleared instance of user defaults.
+  static func getTestInstance() -> UserDefaults {
+    let suiteName = "com.google.firebase.ml.test"
+    // TODO: reconsider force unwrapping
+    let defaults = UserDefaults(suiteName: suiteName)!
+    defaults.removePersistentDomain(forName: suiteName)
+    return defaults
+  }
+}
+
+final class ModelDownloaderTests: XCTestCase {
+  override class func setUp() {
+    super.setUp()
+    let options = FirebaseOptions(googleAppID: Constants.Options.googleAppID,
+                                  gcmSenderID: Constants.Options.gcmSenderID)
+    options.apiKey = Constants.Options.apiKey
+    options.projectID = Constants.Options.projectID
+    options.clientID = Constants.Options.clientID
+    // TODO: Replace with custom options
+    FirebaseApp.configure()
+  }
+
+  /// Unit test for reading and writing to user defaults.
+  func testUserDefaults() {
+    let testApp = FirebaseApp.app()!
+    let functionName = #function
+    let testModelName = "\(functionName)-test-model"
+    let modelInfoRetriever = ModelInfoRetriever(
+      app: testApp,
+      projectID: Constants.Options.projectID,
+      modelName: testModelName
+    )
+
+    modelInfoRetriever.modelInfo = ModelInfo(
+      app: testApp,
+      name: testModelName,
+      defaults: .getTestInstance()
+    )
+    XCTAssertEqual(modelInfoRetriever.modelInfo?.downloadURL, "")
+    modelInfoRetriever.modelInfo?.downloadURL = "testurl.com"
+    XCTAssertEqual(modelInfoRetriever.modelInfo?.downloadURL, "testurl.com")
+    XCTAssertEqual(modelInfoRetriever.modelInfo?.modelHash, "")
+    XCTAssertEqual(modelInfoRetriever.modelInfo?.size, 0)
+    XCTAssertEqual(modelInfoRetriever.modelInfo?.path, nil)
+  }
+
+  func testDownloadModelInfo() {
+    let testApp = FirebaseApp.app()!
+    let functionName = #function
+    let testModelName = "\(functionName)-test-model"
+    let modelInfoRetriever = ModelInfoRetriever(
+      app: testApp,
+      projectID: Constants.Options.projectID,
+      modelName: testModelName
+    )
+    let expectation = self.expectation(description: "Wait for model info to download.")
+    modelInfoRetriever.downloadModelInfo(completion: { error in
+      guard let downloadError = error else { return }
+      XCTAssertEqual(downloadError, .notFound)
+      print("ERROR: Model not found on server.")
+      expectation.fulfill()
+    })
+    waitForExpectations(timeout: 10, handler: nil)
+  }
+
+  func testExample() {
+    // This is an example of a functional test case.
+    // Use XCTAssert and related functions to verify your tests produce the correct
+    // results.
+    let modelDownloader = ModelDownloader()
+    let conditions = ModelDownloadConditions()
+
+    // Download model w/ progress handler
+    modelDownloader.getModel(
+      name: "your_model_name",
+      downloadType: .latestModel,
+      conditions: conditions,
+      progressHandler: { progress in
+        // Handle progress
+      }
+    ) { result in
+      switch result {
+      case .success:
+        // Use model with your inference API
+        // let interpreter = Interpreter(modelPath: customModel.modelPath)
+        break
+      case .failure:
+        // Handle download error
+        break
+      }
+    }
+
+    // Access array of downloaded models
+    modelDownloader.listDownloadedModels { result in
+      switch result {
+      case .success:
+        // Pick model(s) for further use
+        break
+      case .failure:
+        // Handle failure
+        break
+      }
+    }
+
+    // Delete downloaded model
+    modelDownloader.deleteDownloadedModel(name: "your_model_name") { result in
+      switch result {
+      case .success():
+        // Apply any other clean up
+        break
+      case .failure:
+        // Handle failure
+        break
+      }
+    }
+  }
+}

+ 47 - 0
Package.swift

@@ -560,6 +560,53 @@ let package = Package(
       ]
     ),
 
+    .target(
+      name: "FirebaseMLModelDownloader",
+      dependencies: [
+        "FirebaseCore",
+      ],
+      path: "FirebaseMLModelDownloader/Sources",
+      cSettings: [
+        .define("FIRMLModelDownloader_VERSION", to: firebaseVersion),
+      ]
+    ),
+    .testTarget(
+      name: "FirebaseMLModelDownloaderUnit",
+      dependencies: ["FirebaseMLModelDownloader"],
+      path: "FirebaseMLModelDownloader/Tests/Unit"
+    ),
+
+    .target(
+      name: "FirebaseMessaging",
+      dependencies: [
+        "FirebaseCore",
+        "FirebaseInstanceID",
+        "GoogleUtilities_AppDelegateSwizzler",
+        "GoogleUtilities_Environment",
+        "GoogleUtilities_Reachability",
+        "GoogleUtilities_UserDefaults",
+      ],
+      path: "FirebaseMessaging/Sources",
+      publicHeadersPath: "Public",
+      cSettings: [
+        .headerSearchPath("../../"),
+      ],
+      linkerSettings: [
+        .linkedFramework("SystemConfiguration", .when(platforms: .some([.iOS, .macOS, .tvOS]))),
+      ]
+    ),
+    .testTarget(
+      name: "MessagingUnit",
+      dependencies: ["FirebaseMessaging", "OCMock"],
+      path: "FirebaseMessaging/Tests/UnitTests",
+      exclude: [
+        "FIRMessagingContextManagerServiceTest.m", // TODO: Adapt its NSBundle usage to SPM.
+      ],
+      cSettings: [
+        .headerSearchPath("../../.."),
+      ]
+    ),
+
     .target(
       name: "FirebaseMessaging",
       dependencies: [