diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml index 5b1a38a0b07..078339d0ae7 100644 --- a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml +++ b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml @@ -60,7 +60,6 @@ body: - Functions - In-App Messaging - Installations - - ML Model Downloader - Messaging - Performance - Remote Config diff --git a/.github/workflows/sdk.mlmodeldownloader.yml b/.github/workflows/sdk.mlmodeldownloader.yml deleted file mode 100644 index 31484034d1b..00000000000 --- a/.github/workflows/sdk.mlmodeldownloader.yml +++ /dev/null @@ -1,79 +0,0 @@ -name: sdk.mlmodeldownloader - -permissions: - contents: read - -on: - workflow_dispatch: - pull_request: - paths: - - 'FirebaseMLModelDownloader**' - - '.github/workflows/sdk.mlmodeldownloader.yml' - - '.github/workflows/_spm.yml' - - '.github/workflows/_cocoapods.yml' - - '.github/workflows/_catalyst.yml' - - '.github/workflows/_cocoapods.cron.yml' - - 'Gemfile*' - schedule: - # Run every day at 12am (PDT) / 3am (EDT) - cron uses UTC times - - cron: '0 7 * * *' - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} - cancel-in-progress: true - -jobs: - spm: - uses: ./.github/workflows/_spm.yml - with: - target: FirebaseMLModelDownloaderUnit - - catalyst: - uses: ./.github/workflows/_catalyst.yml - with: - product: FirebaseMLModelDownloader - target: FirebaseMLModelDownloader-Unit-unit - - pod_lib_lint: - if: github.event.pull_request.head.repo.fork == false - uses: ./.github/workflows/_cocoapods.yml - with: - product: FirebaseMLModelDownloader - allow_warnings: true - setup_command: | - mkdir FirebaseMLModelDownloader/Tests/Integration/Resources - scripts/decrypt_gha_secret.sh scripts/gha-encrypted/MLModelDownloader/GoogleService-Info.plist.gpg \ - FirebaseMLModelDownloader/Tests/Integration/Resources/GoogleService-Info.plist "$plist_secret" - secrets: - plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - - mlmodeldownloader-cron-only: - if: github.event.pull_request.head.repo.fork == false - needs: pod_lib_lint - uses: ./.github/workflows/_cocoapods.cron.yml - with: - product: FirebaseMLModelDownloader - platforms: '[ "ios", "tvos", "macos" ]' - flags: '[ "--use-static-frameworks --allow-warnings" ]' - setup_command: | - scripts/configure_test_keychain.sh - mkdir FirebaseMLModelDownloader/Tests/Integration/Resources - scripts/decrypt_gha_secret.sh scripts/gha-encrypted/MLModelDownloader/GoogleService-Info.plist.gpg \ - FirebaseMLModelDownloader/Tests/Integration/Resources/GoogleService-Info.plist "$plist_secret" - secrets: - plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - - mlmodeldownloader-sample-build-test: - if: github.event.pull_request.head.repo.fork == false - uses: ./.github/workflows/_build.yml - with: - product: MLModelDownloaderSample - platform: iOS - method: xcodebuild - setup_command: | - mkdir -p FirebaseMLModelDownloader/Apps/Sample/Resources - scripts/decrypt_gha_secret.sh scripts/gha-encrypted/MLModelDownloader/GoogleService-Info.plist.gpg \ - FirebaseMLModelDownloader/Apps/Sample/Resources/GoogleService-Info.plist "$plist_secret" - scripts/install_prereqs.sh MLModelDownloaderSample iOS - secrets: - plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} diff --git a/.gitignore b/.gitignore index 718e6519ad0..cb61f462c12 100644 --- a/.gitignore +++ b/.gitignore @@ -31,10 +31,6 @@ FirebaseMessaging/Apps/SampleStandaloneWatchApp/SampleStandaloneWatchAppWatchApp FirebaseStorage/Tests/ObjCIntegration/Credentials.h FirebaseStorage/Tests/Integration/Credentials.swift -# FirebaseMLModelDownloader integration tests GoogleService-Info.plist -FirebaseMLModelDownloader/Tests/Integration/Resources/GoogleService-Info.plist -FirebaseMLModelDownloader/Apps/Sample/**/GoogleService-Info.plist - # FirebasePerformance dev test App and integration tests GoogleService-Info.plist FirebasePerformance/**/GoogleService-Info.plist diff --git a/Carthage.md b/Carthage.md index b0e35f07d52..e92fb413152 100644 --- a/Carthage.md +++ b/Carthage.md @@ -43,7 +43,6 @@ binary "https://dl.google.com/dl/firebase/ios/carthage/FirebaseFirestoreBinary.j binary "https://dl.google.com/dl/firebase/ios/carthage/FirebaseFunctionsBinary.json" binary "https://dl.google.com/dl/firebase/ios/carthage/FirebaseGoogleSignInBinary.json" binary "https://dl.google.com/dl/firebase/ios/carthage/FirebaseInAppMessagingBinary.json" -binary "https://dl.google.com/dl/firebase/ios/carthage/FirebaseMLModelDownloaderBinary.json" binary "https://dl.google.com/dl/firebase/ios/carthage/FirebaseMessagingBinary.json" binary "https://dl.google.com/dl/firebase/ios/carthage/FirebasePerformanceBinary.json" binary "https://dl.google.com/dl/firebase/ios/carthage/FirebaseRemoteConfigBinary.json" diff --git a/CoreOnly/NOTICES b/CoreOnly/NOTICES index b174e631884..d0cd1f6bfc3 100644 --- a/CoreOnly/NOTICES +++ b/CoreOnly/NOTICES @@ -14,7 +14,6 @@ FirebaseCoreInternal FirebaseFunctions FirebaseInAppMessaging FirebaseInstallations -FirebaseMLModelDownloader FirebaseMessaging FirebaseMessagingInterop FirebasePerformance diff --git a/Firebase.podspec b/Firebase.podspec index bfb689040b5..d5d2eb26f59 100644 --- a/Firebase.podspec +++ b/Firebase.podspec @@ -164,16 +164,6 @@ Simplify your app development, grow your user base, and monetize more effectivel ss.watchos.deployment_target = '7.0' end - s.subspec 'MLModelDownloader' do |ss| - ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseMLModelDownloader', '~> 12.18.0-beta' - # Standard platforms PLUS watchOS. - ss.ios.deployment_target = '15.0' - ss.osx.deployment_target = '10.15' - ss.tvos.deployment_target = '15.0' - ss.watchos.deployment_target = '7.0' - end - s.subspec 'Performance' do |ss| ss.dependency 'Firebase/CoreOnly' ss.ios.dependency 'FirebasePerformance', '~> 12.18.0' diff --git a/FirebaseCore/CHANGELOG.md b/FirebaseCore/CHANGELOG.md index ad4071a498b..1f83a6b7c17 100644 --- a/FirebaseCore/CHANGELOG.md +++ b/FirebaseCore/CHANGELOG.md @@ -1,3 +1,7 @@ +# Unreleased +- [removed] **Breaking change**: FirebaseMLModelDownloader has been removed. See + https://firebase.google.com/docs/ml for more info. + # Firebase 12.17.0 - [changed] Removed the (never activated) `recaptchaSiteKey` property from `FirebaseOptions`. This feature is part of the public preview reCAPTCHA provider. diff --git a/FirebaseMLModelDownloader.podspec b/FirebaseMLModelDownloader.podspec deleted file mode 100644 index 4f986adecb5..00000000000 --- a/FirebaseMLModelDownloader.podspec +++ /dev/null @@ -1,63 +0,0 @@ -Pod::Spec.new do |s| - s.name = 'FirebaseMLModelDownloader' - s.version = '12.18.0-beta' - s.summary = 'Firebase ML Model Downloader' - s.deprecated = true - - s.description = <<-DESC - This is the new ML Model Downloader CocoaPod. - DESC - - s.homepage = 'https://firebase.google.com' - s.license = { :type => 'Apache-2.0', :file => 'LICENSE' } - s.authors = 'Google, Inc.' - - s.source = { - :git => 'https://github.com/firebase/firebase-ios-sdk.git', - :tag => 'CocoaPods-' + s.version.to_s - } - s.social_media_url = 'https://twitter.com/Firebase' - s.swift_version = '5.9' - - ios_deployment_target = '15.0' - osx_deployment_target = '10.15' - tvos_deployment_target = '15.0' - watchos_deployment_target = '7.0' - - s.ios.deployment_target = ios_deployment_target - s.osx.deployment_target = osx_deployment_target - s.tvos.deployment_target = tvos_deployment_target - s.watchos.deployment_target = watchos_deployment_target - - s.cocoapods_version = '>= 1.12.0' - s.prefix_header_file = false - - s.source_files = [ - 'FirebaseMLModelDownloader/Sources/**/*.swift', - ] - - s.framework = 'Foundation' - s.dependency 'FirebaseCore', '~> 12.18.0' - s.dependency 'FirebaseCoreExtension', '~> 12.18.0' - s.dependency 'FirebaseInstallations', '~> 12.18.0' - s.dependency 'GoogleUtilities/UserDefaults', '~> 8.1' - - s.pod_target_xcconfig = { - 'OTHER_CFLAGS' => '-fno-autolink', - } - - s.test_spec 'unit' do |unit_tests| - unit_tests.scheme = { :code_coverage => true } - unit_tests.platforms = {:ios => ios_deployment_target, :osx => osx_deployment_target, :tvos => tvos_deployment_target} - unit_tests.source_files = 'FirebaseMLModelDownloader/Tests/Unit/**/*.swift' - unit_tests.requires_app_host = true - end - - s.test_spec 'integration' do |int_tests| - int_tests.scheme = { :code_coverage => true } - int_tests.platforms = {:ios => ios_deployment_target, :osx => osx_deployment_target, :tvos => tvos_deployment_target} - int_tests.source_files = 'FirebaseMLModelDownloader/Tests/Integration/**/*.swift' - int_tests.resources = 'FirebaseMLModelDownloader/Tests/Integration/Resources/GoogleService-Info.plist' - int_tests.requires_app_host = true - end -end diff --git a/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp.xcodeproj/project.pbxproj b/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp.xcodeproj/project.pbxproj deleted file mode 100644 index da77f4f1847..00000000000 --- a/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp.xcodeproj/project.pbxproj +++ /dev/null @@ -1,804 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 51; - objects = { - -/* Begin PBXBuildFile section */ - 07BB5A04CA1312F86A973058 /* Pods_MLDownloaderTestApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DBA0C7A8283CC88DBA487171 /* Pods_MLDownloaderTestApp.framework */; }; - 6915E92311191C32B3B80134 /* Pods_MLDownloaderTestApp_MLDownloaderTestAppTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF958C46F61DC40BD1D25898 /* Pods_MLDownloaderTestApp_MLDownloaderTestAppTests.framework */; }; - A61B7A3825C0E69800D643C5 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = A61B7A3725C0E69800D643C5 /* GoogleService-Info.plist */; }; - A6338C3E25893DFB001A6401 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6338C3D25893DFB001A6401 /* AppDelegate.swift */; }; - A6338C4225893DFB001A6401 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6338C4125893DFB001A6401 /* ContentView.swift */; }; - A6338C4425893DFE001A6401 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A6338C4325893DFE001A6401 /* Assets.xcassets */; }; - A6338C4725893DFE001A6401 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A6338C4625893DFE001A6401 /* Preview Assets.xcassets */; }; - A6338C5525893DFE001A6401 /* MLDownloaderTestAppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6338C5425893DFE001A6401 /* MLDownloaderTestAppTests.swift */; }; - A6338C6025893DFE001A6401 /* MLDownloaderTestAppUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6338C5F25893DFE001A6401 /* MLDownloaderTestAppUITests.swift */; }; - A67690C9259E94AE000AA74D /* Downloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A67690C8259E94AD000AA74D /* Downloader.swift */; }; - A67690CA259E94AE000AA74D /* Downloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A67690C8259E94AD000AA74D /* Downloader.swift */; }; - A67690CB259E94AE000AA74D /* Downloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A67690C8259E94AD000AA74D /* Downloader.swift */; }; - A6769158259EB2DC000AA74D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6769157259EB2DC000AA74D /* SceneDelegate.swift */; }; - A676916C259EB420000AA74D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A676916A259EB420000AA74D /* LaunchScreen.storyboard */; }; - C6508420214F3CF5AA953487 /* Pods_MLDownloaderTestApp_MLDownloaderTestAppUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 28C835CA2BBE48AABE55BC71 /* Pods_MLDownloaderTestApp_MLDownloaderTestAppUITests.framework */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - A6338C5125893DFE001A6401 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = A6338C3225893DFB001A6401 /* Project object */; - proxyType = 1; - remoteGlobalIDString = A6338C3925893DFB001A6401; - remoteInfo = MLDownloaderTestApp; - }; - A6338C5C25893DFE001A6401 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = A6338C3225893DFB001A6401 /* Project object */; - proxyType = 1; - remoteGlobalIDString = A6338C3925893DFB001A6401; - remoteInfo = MLDownloaderTestApp; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - A6CFE30F258AAC2E003ABA7F /* Embed Watch Content */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = "$(CONTENTS_FOLDER_PATH)/Watch"; - dstSubfolderSpec = 16; - files = ( - ); - name = "Embed Watch Content"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 28B90F3CBFDC23142FA82356 /* Pods-MLDownloaderTestApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MLDownloaderTestApp.debug.xcconfig"; path = "Target Support Files/Pods-MLDownloaderTestApp/Pods-MLDownloaderTestApp.debug.xcconfig"; sourceTree = ""; }; - 28C835CA2BBE48AABE55BC71 /* Pods_MLDownloaderTestApp_MLDownloaderTestAppUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MLDownloaderTestApp_MLDownloaderTestAppUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 8AD845EE89ABB7AB1CA74744 /* Pods-MLDownloaderTestApp-MLDownloaderTestAppUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MLDownloaderTestApp-MLDownloaderTestAppUITests.debug.xcconfig"; path = "Target Support Files/Pods-MLDownloaderTestApp-MLDownloaderTestAppUITests/Pods-MLDownloaderTestApp-MLDownloaderTestAppUITests.debug.xcconfig"; sourceTree = ""; }; - A61B7A3725C0E69800D643C5 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; name = "GoogleService-Info.plist"; path = "Resources/GoogleService-Info.plist"; sourceTree = SOURCE_ROOT; }; - A6338C3A25893DFB001A6401 /* MLDownloaderTestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MLDownloaderTestApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; - A6338C3D25893DFB001A6401 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - A6338C4125893DFB001A6401 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; - A6338C4325893DFE001A6401 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - A6338C4625893DFE001A6401 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - A6338C4B25893DFE001A6401 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - A6338C5025893DFE001A6401 /* MLDownloaderTestAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MLDownloaderTestAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - A6338C5425893DFE001A6401 /* MLDownloaderTestAppTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MLDownloaderTestAppTests.swift; sourceTree = ""; }; - A6338C5625893DFE001A6401 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - A6338C5B25893DFE001A6401 /* MLDownloaderTestAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MLDownloaderTestAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - A6338C5F25893DFE001A6401 /* MLDownloaderTestAppUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MLDownloaderTestAppUITests.swift; sourceTree = ""; }; - A6338C6125893DFE001A6401 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - A67690C8259E94AD000AA74D /* Downloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Downloader.swift; sourceTree = ""; }; - A6769157259EB2DC000AA74D /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - A676916B259EB420000AA74D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - A78C0FF286D9919CEF734166 /* Pods-MLDownloaderTestApp-MLDownloaderTestAppTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MLDownloaderTestApp-MLDownloaderTestAppTests.release.xcconfig"; path = "Target Support Files/Pods-MLDownloaderTestApp-MLDownloaderTestAppTests/Pods-MLDownloaderTestApp-MLDownloaderTestAppTests.release.xcconfig"; sourceTree = ""; }; - B39EA79D99402F5322014B52 /* Pods-MLDownloaderTestApp-MLDownloaderWatchTestApp Extension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MLDownloaderTestApp-MLDownloaderWatchTestApp Extension.release.xcconfig"; path = "Target Support Files/Pods-MLDownloaderTestApp-MLDownloaderWatchTestApp Extension/Pods-MLDownloaderTestApp-MLDownloaderWatchTestApp Extension.release.xcconfig"; sourceTree = ""; }; - BF958C46F61DC40BD1D25898 /* Pods_MLDownloaderTestApp_MLDownloaderTestAppTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MLDownloaderTestApp_MLDownloaderTestAppTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - D8D4472243EB359D834597B3 /* Pods-MLDownloaderTestApp-MLDownloaderTestAppTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MLDownloaderTestApp-MLDownloaderTestAppTests.debug.xcconfig"; path = "Target Support Files/Pods-MLDownloaderTestApp-MLDownloaderTestAppTests/Pods-MLDownloaderTestApp-MLDownloaderTestAppTests.debug.xcconfig"; sourceTree = ""; }; - DBA0C7A8283CC88DBA487171 /* Pods_MLDownloaderTestApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MLDownloaderTestApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - E030EB120D34BE1BF596CC34 /* Pods-MLDownloaderTestApp-MLDownloaderTestAppUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MLDownloaderTestApp-MLDownloaderTestAppUITests.release.xcconfig"; path = "Target Support Files/Pods-MLDownloaderTestApp-MLDownloaderTestAppUITests/Pods-MLDownloaderTestApp-MLDownloaderTestAppUITests.release.xcconfig"; sourceTree = ""; }; - F4FD55F74773D1C86D69D673 /* Pods-MLDownloaderTestApp-MLDownloaderWatchTestApp Extension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MLDownloaderTestApp-MLDownloaderWatchTestApp Extension.debug.xcconfig"; path = "Target Support Files/Pods-MLDownloaderTestApp-MLDownloaderWatchTestApp Extension/Pods-MLDownloaderTestApp-MLDownloaderWatchTestApp Extension.debug.xcconfig"; sourceTree = ""; }; - F85918555030DEB31ECD5E7D /* Pods-MLDownloaderTestApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MLDownloaderTestApp.release.xcconfig"; path = "Target Support Files/Pods-MLDownloaderTestApp/Pods-MLDownloaderTestApp.release.xcconfig"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - A6338C3725893DFB001A6401 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 07BB5A04CA1312F86A973058 /* Pods_MLDownloaderTestApp.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A6338C4D25893DFE001A6401 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 6915E92311191C32B3B80134 /* Pods_MLDownloaderTestApp_MLDownloaderTestAppTests.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A6338C5825893DFE001A6401 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - C6508420214F3CF5AA953487 /* Pods_MLDownloaderTestApp_MLDownloaderTestAppUITests.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 50E87A89C7DBAC6DC280F7BB /* Pods */ = { - isa = PBXGroup; - children = ( - 28B90F3CBFDC23142FA82356 /* Pods-MLDownloaderTestApp.debug.xcconfig */, - F85918555030DEB31ECD5E7D /* Pods-MLDownloaderTestApp.release.xcconfig */, - D8D4472243EB359D834597B3 /* Pods-MLDownloaderTestApp-MLDownloaderTestAppTests.debug.xcconfig */, - A78C0FF286D9919CEF734166 /* Pods-MLDownloaderTestApp-MLDownloaderTestAppTests.release.xcconfig */, - 8AD845EE89ABB7AB1CA74744 /* Pods-MLDownloaderTestApp-MLDownloaderTestAppUITests.debug.xcconfig */, - E030EB120D34BE1BF596CC34 /* Pods-MLDownloaderTestApp-MLDownloaderTestAppUITests.release.xcconfig */, - F4FD55F74773D1C86D69D673 /* Pods-MLDownloaderTestApp-MLDownloaderWatchTestApp Extension.debug.xcconfig */, - B39EA79D99402F5322014B52 /* Pods-MLDownloaderTestApp-MLDownloaderWatchTestApp Extension.release.xcconfig */, - ); - path = Pods; - sourceTree = ""; - }; - A6338C3125893DFB001A6401 = { - isa = PBXGroup; - children = ( - A6338C3C25893DFB001A6401 /* MLDownloaderTestApp */, - A6338C5325893DFE001A6401 /* MLDownloaderTestAppTests */, - A6338C5E25893DFE001A6401 /* MLDownloaderTestAppUITests */, - A6338C3B25893DFB001A6401 /* Products */, - 50E87A89C7DBAC6DC280F7BB /* Pods */, - BDAA1A20A1068C1D9F3FFE4A /* Frameworks */, - ); - sourceTree = ""; - }; - A6338C3B25893DFB001A6401 /* Products */ = { - isa = PBXGroup; - children = ( - A6338C3A25893DFB001A6401 /* MLDownloaderTestApp.app */, - A6338C5025893DFE001A6401 /* MLDownloaderTestAppTests.xctest */, - A6338C5B25893DFE001A6401 /* MLDownloaderTestAppUITests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - A6338C3C25893DFB001A6401 /* MLDownloaderTestApp */ = { - isa = PBXGroup; - children = ( - A61B7A3725C0E69800D643C5 /* GoogleService-Info.plist */, - A676916A259EB420000AA74D /* LaunchScreen.storyboard */, - A6769157259EB2DC000AA74D /* SceneDelegate.swift */, - A6338C3D25893DFB001A6401 /* AppDelegate.swift */, - A6338C4125893DFB001A6401 /* ContentView.swift */, - A67690C8259E94AD000AA74D /* Downloader.swift */, - A6338C4325893DFE001A6401 /* Assets.xcassets */, - A6338C4B25893DFE001A6401 /* Info.plist */, - A6338C4525893DFE001A6401 /* Preview Content */, - ); - path = MLDownloaderTestApp; - sourceTree = ""; - }; - A6338C4525893DFE001A6401 /* Preview Content */ = { - isa = PBXGroup; - children = ( - A6338C4625893DFE001A6401 /* Preview Assets.xcassets */, - ); - path = "Preview Content"; - sourceTree = ""; - }; - A6338C5325893DFE001A6401 /* MLDownloaderTestAppTests */ = { - isa = PBXGroup; - children = ( - A6338C5425893DFE001A6401 /* MLDownloaderTestAppTests.swift */, - A6338C5625893DFE001A6401 /* Info.plist */, - ); - path = MLDownloaderTestAppTests; - sourceTree = ""; - }; - A6338C5E25893DFE001A6401 /* MLDownloaderTestAppUITests */ = { - isa = PBXGroup; - children = ( - A6338C5F25893DFE001A6401 /* MLDownloaderTestAppUITests.swift */, - A6338C6125893DFE001A6401 /* Info.plist */, - ); - path = MLDownloaderTestAppUITests; - sourceTree = ""; - }; - BDAA1A20A1068C1D9F3FFE4A /* Frameworks */ = { - isa = PBXGroup; - children = ( - DBA0C7A8283CC88DBA487171 /* Pods_MLDownloaderTestApp.framework */, - BF958C46F61DC40BD1D25898 /* Pods_MLDownloaderTestApp_MLDownloaderTestAppTests.framework */, - 28C835CA2BBE48AABE55BC71 /* Pods_MLDownloaderTestApp_MLDownloaderTestAppUITests.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - A6338C3925893DFB001A6401 /* MLDownloaderTestApp */ = { - isa = PBXNativeTarget; - buildConfigurationList = A6338C6425893DFE001A6401 /* Build configuration list for PBXNativeTarget "MLDownloaderTestApp" */; - buildPhases = ( - 169C74864E072B2767C5FC4C /* [CP] Check Pods Manifest.lock */, - A6338C3625893DFB001A6401 /* Sources */, - A6338C3725893DFB001A6401 /* Frameworks */, - A6338C3825893DFB001A6401 /* Resources */, - 51CE470B55BA59D1B9D44221 /* [CP] Embed Pods Frameworks */, - A6CFE30F258AAC2E003ABA7F /* Embed Watch Content */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = MLDownloaderTestApp; - productName = MLDownloaderTestApp; - productReference = A6338C3A25893DFB001A6401 /* MLDownloaderTestApp.app */; - productType = "com.apple.product-type.application"; - }; - A6338C4F25893DFE001A6401 /* MLDownloaderTestAppTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = A6338C6725893DFE001A6401 /* Build configuration list for PBXNativeTarget "MLDownloaderTestAppTests" */; - buildPhases = ( - 54E6E344826DB0449BD00A94 /* [CP] Check Pods Manifest.lock */, - A6338C4C25893DFE001A6401 /* Sources */, - A6338C4D25893DFE001A6401 /* Frameworks */, - A6338C4E25893DFE001A6401 /* Resources */, - 92F84D16168AFB93DA5765E3 /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - A6338C5225893DFE001A6401 /* PBXTargetDependency */, - ); - name = MLDownloaderTestAppTests; - productName = MLDownloaderTestAppTests; - productReference = A6338C5025893DFE001A6401 /* MLDownloaderTestAppTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - A6338C5A25893DFE001A6401 /* MLDownloaderTestAppUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = A6338C6A25893DFE001A6401 /* Build configuration list for PBXNativeTarget "MLDownloaderTestAppUITests" */; - buildPhases = ( - B78384A960BE5236EA10CFF3 /* [CP] Check Pods Manifest.lock */, - A6338C5725893DFE001A6401 /* Sources */, - A6338C5825893DFE001A6401 /* Frameworks */, - A6338C5925893DFE001A6401 /* Resources */, - B6A2977D799A8B97E09000C4 /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - A6338C5D25893DFE001A6401 /* PBXTargetDependency */, - ); - name = MLDownloaderTestAppUITests; - productName = MLDownloaderTestAppUITests; - productReference = A6338C5B25893DFE001A6401 /* MLDownloaderTestAppUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - A6338C3225893DFB001A6401 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 1200; - LastUpgradeCheck = 1200; - TargetAttributes = { - A6338C3925893DFB001A6401 = { - CreatedOnToolsVersion = 12.0; - }; - A6338C4F25893DFE001A6401 = { - CreatedOnToolsVersion = 12.0; - TestTargetID = A6338C3925893DFB001A6401; - }; - A6338C5A25893DFE001A6401 = { - CreatedOnToolsVersion = 12.0; - TestTargetID = A6338C3925893DFB001A6401; - }; - }; - }; - buildConfigurationList = A6338C3525893DFB001A6401 /* Build configuration list for PBXProject "MLDownloaderTestApp" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = A6338C3125893DFB001A6401; - productRefGroup = A6338C3B25893DFB001A6401 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - A6338C3925893DFB001A6401 /* MLDownloaderTestApp */, - A6338C4F25893DFE001A6401 /* MLDownloaderTestAppTests */, - A6338C5A25893DFE001A6401 /* MLDownloaderTestAppUITests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - A6338C3825893DFB001A6401 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - A6338C4725893DFE001A6401 /* Preview Assets.xcassets in Resources */, - A61B7A3825C0E69800D643C5 /* GoogleService-Info.plist in Resources */, - A676916C259EB420000AA74D /* LaunchScreen.storyboard in Resources */, - A6338C4425893DFE001A6401 /* Assets.xcassets in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A6338C4E25893DFE001A6401 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A6338C5925893DFE001A6401 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 169C74864E072B2767C5FC4C /* [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-MLDownloaderTestApp-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; - }; - 51CE470B55BA59D1B9D44221 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-MLDownloaderTestApp/Pods-MLDownloaderTestApp-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-MLDownloaderTestApp/Pods-MLDownloaderTestApp-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-MLDownloaderTestApp/Pods-MLDownloaderTestApp-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 54E6E344826DB0449BD00A94 /* [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-MLDownloaderTestApp-MLDownloaderTestAppTests-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; - }; - 92F84D16168AFB93DA5765E3 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-MLDownloaderTestApp-MLDownloaderTestAppTests/Pods-MLDownloaderTestApp-MLDownloaderTestAppTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-MLDownloaderTestApp-MLDownloaderTestAppTests/Pods-MLDownloaderTestApp-MLDownloaderTestAppTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-MLDownloaderTestApp-MLDownloaderTestAppTests/Pods-MLDownloaderTestApp-MLDownloaderTestAppTests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - B6A2977D799A8B97E09000C4 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-MLDownloaderTestApp-MLDownloaderTestAppUITests/Pods-MLDownloaderTestApp-MLDownloaderTestAppUITests-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-MLDownloaderTestApp-MLDownloaderTestAppUITests/Pods-MLDownloaderTestApp-MLDownloaderTestAppUITests-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-MLDownloaderTestApp-MLDownloaderTestAppUITests/Pods-MLDownloaderTestApp-MLDownloaderTestAppUITests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - B78384A960BE5236EA10CFF3 /* [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-MLDownloaderTestApp-MLDownloaderTestAppUITests-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; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - A6338C3625893DFB001A6401 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - A6338C3E25893DFB001A6401 /* AppDelegate.swift in Sources */, - A67690C9259E94AE000AA74D /* Downloader.swift in Sources */, - A6338C4225893DFB001A6401 /* ContentView.swift in Sources */, - A6769158259EB2DC000AA74D /* SceneDelegate.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A6338C4C25893DFE001A6401 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - A6338C5525893DFE001A6401 /* MLDownloaderTestAppTests.swift in Sources */, - A67690CA259E94AE000AA74D /* Downloader.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A6338C5725893DFE001A6401 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - A6338C6025893DFE001A6401 /* MLDownloaderTestAppUITests.swift in Sources */, - A67690CB259E94AE000AA74D /* Downloader.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - A6338C5225893DFE001A6401 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = A6338C3925893DFB001A6401 /* MLDownloaderTestApp */; - targetProxy = A6338C5125893DFE001A6401 /* PBXContainerItemProxy */; - }; - A6338C5D25893DFE001A6401 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = A6338C3925893DFB001A6401 /* MLDownloaderTestApp */; - targetProxy = A6338C5C25893DFE001A6401 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - A676916A259EB420000AA74D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - A676916B259EB420000AA74D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - A6338C6225893DFE001A6401 /* 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - 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; - 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 = 15.0; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - A6338C6325893DFE001A6401 /* 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - 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; - 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 = 15.0; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - A6338C6525893DFE001A6401 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 28B90F3CBFDC23142FA82356 /* Pods-MLDownloaderTestApp.debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_IDENTITY = "iPhone Developer"; - CODE_SIGN_STYLE = Manual; - DEVELOPMENT_ASSET_PATHS = "\"MLDownloaderTestApp/Preview Content\""; - DEVELOPMENT_TEAM = EQHXZ8M8AV; - ENABLE_PREVIEWS = YES; - INFOPLIST_FILE = MLDownloaderTestApp/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.google.MLDownloaderTestApp; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = "Google Development"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - A6338C6625893DFE001A6401 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = F85918555030DEB31ECD5E7D /* Pods-MLDownloaderTestApp.release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_IDENTITY = "iPhone Developer"; - CODE_SIGN_STYLE = Manual; - DEVELOPMENT_ASSET_PATHS = "\"MLDownloaderTestApp/Preview Content\""; - DEVELOPMENT_TEAM = EQHXZ8M8AV; - ENABLE_PREVIEWS = YES; - INFOPLIST_FILE = MLDownloaderTestApp/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.google.MLDownloaderTestApp; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = "Google Development"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - A6338C6825893DFE001A6401 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = D8D4472243EB359D834597B3 /* Pods-MLDownloaderTestApp-MLDownloaderTestAppTests.debug.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = MLDownloaderTestAppTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.google.MLDownloaderTestAppTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MLDownloaderTestApp.app/MLDownloaderTestApp"; - }; - name = Debug; - }; - A6338C6925893DFE001A6401 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = A78C0FF286D9919CEF734166 /* Pods-MLDownloaderTestApp-MLDownloaderTestAppTests.release.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = MLDownloaderTestAppTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.google.MLDownloaderTestAppTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MLDownloaderTestApp.app/MLDownloaderTestApp"; - }; - name = Release; - }; - A6338C6B25893DFE001A6401 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 8AD845EE89ABB7AB1CA74744 /* Pods-MLDownloaderTestApp-MLDownloaderTestAppUITests.debug.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = MLDownloaderTestAppUITests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.google.MLDownloaderTestAppUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = MLDownloaderTestApp; - }; - name = Debug; - }; - A6338C6C25893DFE001A6401 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = E030EB120D34BE1BF596CC34 /* Pods-MLDownloaderTestApp-MLDownloaderTestAppUITests.release.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = MLDownloaderTestAppUITests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.google.MLDownloaderTestAppUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = MLDownloaderTestApp; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - A6338C3525893DFB001A6401 /* Build configuration list for PBXProject "MLDownloaderTestApp" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - A6338C6225893DFE001A6401 /* Debug */, - A6338C6325893DFE001A6401 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - A6338C6425893DFE001A6401 /* Build configuration list for PBXNativeTarget "MLDownloaderTestApp" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - A6338C6525893DFE001A6401 /* Debug */, - A6338C6625893DFE001A6401 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - A6338C6725893DFE001A6401 /* Build configuration list for PBXNativeTarget "MLDownloaderTestAppTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - A6338C6825893DFE001A6401 /* Debug */, - A6338C6925893DFE001A6401 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - A6338C6A25893DFE001A6401 /* Build configuration list for PBXNativeTarget "MLDownloaderTestAppUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - A6338C6B25893DFE001A6401 /* Debug */, - A6338C6C25893DFE001A6401 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = A6338C3225893DFB001A6401 /* Project object */; -} diff --git a/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp.xcodeproj/xcshareddata/xcschemes/MLDownloaderTestApp.xcscheme b/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp.xcodeproj/xcshareddata/xcschemes/MLDownloaderTestApp.xcscheme deleted file mode 100644 index 067f531de60..00000000000 --- a/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp.xcodeproj/xcshareddata/xcschemes/MLDownloaderTestApp.xcscheme +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp/AppDelegate.swift b/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp/AppDelegate.swift deleted file mode 100644 index b23bb33fd65..00000000000 --- a/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp/AppDelegate.swift +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import FirebaseCore -import FirebaseMLModelDownloader -import UIKit - -@main -class AppDelegate: UIResponder, UIApplicationDelegate { - func application(_ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication - .LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. - FirebaseApp.configure() - FirebaseConfiguration.shared.setLoggerLevel(.debug) - return true - } - - // MARK: UISceneSession Lifecycle - - func application(_ application: UIApplication, - configurationForConnecting connectingSceneSession: UISceneSession, - options: UIScene.ConnectionOptions) -> UISceneConfiguration { - // Called when a new scene session is being created. - // Use this method to select a configuration to create the new scene with. - return UISceneConfiguration( - name: "Default Configuration", - sessionRole: connectingSceneSession.role - ) - } - - func application(_ application: UIApplication, - didDiscardSceneSessions sceneSessions: Set) { - // Called when the user discards a scene session. - // If any sessions were discarded while the application was not running, this will be called - // shortly after application:didFinishLaunchingWithOptions. - // Use this method to release any resources that were specific to the discarded scenes, as they - // will not return. - } -} diff --git a/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp/Assets.xcassets/AccentColor.colorset/Contents.json b/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb878970081..00000000000 --- a/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 9221b9bb1a3..00000000000 --- a/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "images" : [ - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "20x20" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "20x20" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "29x29" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "29x29" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "40x40" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "40x40" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "60x60" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "60x60" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "20x20" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "20x20" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "29x29" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "29x29" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "40x40" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "40x40" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "76x76" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "76x76" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "83.5x83.5" - }, - { - "idiom" : "ios-marketing", - "scale" : "1x", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp/Assets.xcassets/Contents.json b/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp/Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596a7f..00000000000 --- a/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp/Base.lproj/LaunchScreen.storyboard b/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index 865e9329f37..00000000000 --- a/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp/ContentView.swift b/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp/ContentView.swift deleted file mode 100644 index 5bd988afca8..00000000000 --- a/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp/ContentView.swift +++ /dev/null @@ -1,172 +0,0 @@ -// 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 FirebaseMLModelDownloader -import SwiftUI - -struct ContentView: View { - var downloadTotal: Float = 1.0 - @ObservedObject var downloader = Downloader() - - private var buttons: some View { - VStack(spacing: 10) { - Button(action: downloader.downloadModelHelper(downloadType: .localModel), label: { - Text("Local Model") - }) - .buttonStyle(CustomDownloadButtonStyle()) - - Button( - action: downloader.downloadModelHelper(downloadType: .localModelUpdateInBackground), - label: { - Text("Local Model (Background Update)") - } - ) - .buttonStyle(CustomDownloadButtonStyle()) - - Button(action: downloader.downloadModelHelper(downloadType: .latestModel), label: { - Text("Latest Model") - }) - .buttonStyle(CustomDownloadButtonStyle()) - - Button(action: downloader.listModelHelper(), label: { - Text("List Models") - }) - .buttonStyle(CustomListButtonStyle()) - .padding() - - Button(action: downloader.deleteModelHelper(), label: { - Text("Delete Model") - }) - .buttonStyle(CustomDeleteButtonStyle()) - } - } - - private var download: some View { - VStack { - if downloader.downloadProgress >= downloadTotal { - Text("Model downloaded!") - .foregroundColor(.green) - } else { - Text("Model already on device!") - .foregroundColor(.green) - } - Text("Model saved at \(downloader.filePath).") - .foregroundColor(.gray) - .fixedSize(horizontal: false, vertical: /*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) - .font(.footnote) - } - } - - private var delete: some View { - Text("Model deleted.") - .foregroundColor(.purple) - .padding() - } - - private var list: some View { - VStack { - Text("These models are currently on device.") - .foregroundColor(.green) - List(downloader.modelNames, id: \.self) { name in - Text(name) - .foregroundColor(.pink) - .padding() - } - } - } - - var body: some View { - VStack(spacing: 10) { - Text("Download Model") - .font(.title) - - TextField("Enter model name", text: $downloader.modelName) - .frame(width: 200, height: 100) - .clipped() - .autocapitalization(.none) - .textFieldStyle(RoundedBorderTextFieldStyle()) - - buttons - - ProgressView("Downloading...", value: downloader.downloadProgress, total: downloadTotal) - .progressViewStyle(CustomProgressViewStyle(progress: downloader.downloadProgress, - total: downloadTotal)) - - if downloader.isDownloaded { - download - } - - if downloader.isDeleted { - delete - } - - if downloader.modelNames.count > 0 { - list - } - - if downloader.isError { - Text(downloader.error) - } - } - } -} - -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - ContentView() - } -} - -struct CustomProgressViewStyle: ProgressViewStyle { - var downloadProgressColor: Color = .blue - var downloadCompletecolor: Color = .green - var progress: Float - var total: Float - - func makeBody(configuration: Configuration) -> some View { - ProgressView(configuration) - .padding(.horizontal, 30) - .opacity(progress > 0 && progress < total ? 0.8 : 0.0) - } -} - -struct CustomDownloadButtonStyle: ButtonStyle { - func makeBody(configuration: Configuration) -> some View { - configuration.label - .padding(5) - .foregroundColor(.white) - .background(configuration.isPressed ? Color.orange : Color.blue) - .shadow(radius: 5) - } -} - -struct CustomListButtonStyle: ButtonStyle { - func makeBody(configuration: Configuration) -> some View { - configuration.label - .padding(5) - .foregroundColor(.white) - .background(configuration.isPressed ? Color.orange : Color.green) - .shadow(radius: 5) - } -} - -struct CustomDeleteButtonStyle: ButtonStyle { - func makeBody(configuration: Configuration) -> some View { - configuration.label - .padding(5) - .foregroundColor(.white) - .background(configuration.isPressed ? Color.orange : Color.red) - .shadow(radius: 5) - } -} diff --git a/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp/Downloader.swift b/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp/Downloader.swift deleted file mode 100644 index 4881a234a05..00000000000 --- a/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp/Downloader.swift +++ /dev/null @@ -1,135 +0,0 @@ -// 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 FirebaseMLModelDownloader -import Foundation -import TensorFlowLite - -class Downloader: ObservableObject { - @Published var downloadProgress: Float = 0.0 - @Published var modelName = "" - @Published var filePath = "" - @Published var error = "" - @Published var isDownloaded = false - @Published var isDeleted = false - @Published var isError = false - @Published var modelNames = [String]() - - private func resetState() { - isDownloaded = false - isDeleted = false - downloadProgress = 0.0 - filePath = "" - error = "" - isError = false - modelNames = [] - } - - func downloadModelHelper(downloadType: ModelDownloadType) -> () -> Void { - return { - self.resetState() - self.downloadModel(downloadType: downloadType) - } - } - - func downloadModel(downloadType: ModelDownloadType) { - let modelDownloader = ModelDownloader.modelDownloader() - let conditions = ModelDownloadConditions() - modelDownloader.getModel( - name: modelName, - downloadType: downloadType, - conditions: conditions, - progressHandler: { progress in - self.downloadProgress = progress - } - ) { result in - switch result { - case let .success(model): - self.isDownloaded = true - self.filePath = model.path - let fileURL = URL(fileURLWithPath: self.filePath) - do { - _ = try fileURL.checkResourceIsReachable() - let attr = try FileManager.default.attributesOfItem(atPath: self.filePath) - if let size = attr[FileAttributeKey.size] { - print("File size: \(size)") - } else { - print("Error - could not get file size.") - } - } catch { - print("File access error - \(error)") - } - do { - _ = try Interpreter(modelPath: self.filePath) - } catch { - print("Tensorflow error - \(error)") - } - case let .failure(error): - self.isDownloaded = false - self.isError = true - self.error = "Model download failed with error: \(error)" - } - } - } - - func deleteModelHelper() -> () -> Void { - return { - self.resetState() - self.deleteModel() - } - } - - func deleteModel() { - let modelDownloader = ModelDownloader.modelDownloader() - modelDownloader.deleteDownloadedModel(name: modelName) { result in - switch result { - case .success: - self.isDeleted = true - self.isDownloaded = false - self.filePath = "" - case let .failure(error): - self.isDeleted = false - self.isError = true - self.error = "Model deletion failed with error: \(error)" - } - } - } - - func listModelHelper() -> () -> Void { - return { - self.resetState() - self.listModel() - } - } - - func listModel() { - let modelDownloader = ModelDownloader.modelDownloader() - modelDownloader.listDownloadedModels { result in - switch result { - case let .success(models): - if models.count == 0 { - self.isError = true - self.error = "No models found on device." - } else { - for model in models { - self.modelNames.append(model.name) - } - } - case let .failure(error): - self.isError = true - self.error = "Listing models failed with error: \(error)" - } - } - } -} diff --git a/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp/Info.plist b/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp/Info.plist deleted file mode 100644 index 2688b32b32f..00000000000 --- a/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp/Info.plist +++ /dev/null @@ -1,62 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - - - - - UIApplicationSupportsIndirectInputEvents - - UILaunchStoryboardName - LaunchScreen - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - - diff --git a/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp/Preview Content/Preview Assets.xcassets/Contents.json b/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp/Preview Content/Preview Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596a7f..00000000000 --- a/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp/Preview Content/Preview Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp/SceneDelegate.swift b/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp/SceneDelegate.swift deleted file mode 100644 index 647f208fb0c..00000000000 --- a/FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp/SceneDelegate.swift +++ /dev/null @@ -1,73 +0,0 @@ -// 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 SwiftUI -import UIKit - -class SceneDelegate: UIResponder, UIWindowSceneDelegate { - var window: UIWindow? - - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, - options connectionOptions: UIScene.ConnectionOptions) { - // Use this method to optionally configure and attach the UIWindow `window` to the provided - // UIWindowScene `scene`. - // If using a storyboard, the `window` property will automatically be initialized and attached - // to the scene. - // This delegate does not imply the connecting scene or session are new (see - // `application:configurationForConnectingSceneSession` instead). - - // Create the SwiftUI view that provides the window contents. - let contentView = ContentView() - - // Use a UIHostingController as window root view controller. - if let windowScene = scene as? UIWindowScene { - let window = UIWindow(windowScene: windowScene) - window.rootViewController = UIHostingController(rootView: contentView) - self.window = window - window.makeKeyAndVisible() - } - } - - func sceneDidDisconnect(_ scene: UIScene) { - // Called as the scene is being released by the system. - // This occurs shortly after the scene enters the background, or when its session is discarded. - // Release any resources associated with this scene that can be re-created the next time the - // scene connects. - // The scene may re-connect later, as its session was not necessarily discarded (see - // `application:didDiscardSceneSessions` instead). - } - - func sceneDidBecomeActive(_ scene: UIScene) { - // Called when the scene has moved from an inactive state to an active state. - // Use this method to restart any tasks that were paused (or not yet started) when the scene was - // inactive. - } - - func sceneWillResignActive(_ scene: UIScene) { - // Called when the scene will move from an active state to an inactive state. - // This may occur due to temporary interruptions (ex. an incoming phone call). - } - - func sceneWillEnterForeground(_ scene: UIScene) { - // Called as the scene transitions from the background to the foreground. - // Use this method to undo the changes made on entering the background. - } - - func sceneDidEnterBackground(_ scene: UIScene) { - // Called as the scene transitions from the foreground to the background. - // Use this method to save data, release shared resources, and store enough scene-specific state - // information - // to restore the scene back to its current state. - } -} diff --git a/FirebaseMLModelDownloader/Apps/Sample/Podfile b/FirebaseMLModelDownloader/Apps/Sample/Podfile deleted file mode 100644 index 58922757f53..00000000000 --- a/FirebaseMLModelDownloader/Apps/Sample/Podfile +++ /dev/null @@ -1,20 +0,0 @@ -platform :ios, '15.0' -use_frameworks! - -source 'https://github.com/firebase/SpecsDev.git' -source 'https://github.com/firebase/SpecsStaging.git' -source 'https://cdn.cocoapods.org/' - -pod 'FirebaseCore', :path => '../../../' -pod 'FirebaseCoreInternal', :path => '../../../' -pod 'FirebaseMLModelDownloader', :path => '../../../' -pod 'TensorFlowLiteSwift' - -target 'MLDownloaderTestApp' do - target 'MLDownloaderTestAppTests' do - end - - target 'MLDownloaderTestAppUITests' do - end - -end diff --git a/FirebaseMLModelDownloader/Apps/Sample/README.md b/FirebaseMLModelDownloader/Apps/Sample/README.md deleted file mode 100644 index 88228115908..00000000000 --- a/FirebaseMLModelDownloader/Apps/Sample/README.md +++ /dev/null @@ -1,8 +0,0 @@ -### MLDownloader Sample App Setup - -To run this app, you'll need a GoogleService-Info.plist file. - -1. Go to the [Firebase Console](https://console.firebase.google.com/) -2. Create a Firebase project if you don't have one already. -3. Create a new iOS App if you don't have one already. -4. Go to Project Overview -> General -> Your apps, select your iOS app and download the GoogleService-Info.plist file. diff --git a/FirebaseMLModelDownloader/CHANGELOG.md b/FirebaseMLModelDownloader/CHANGELOG.md index ddd17478742..79c0245062a 100644 --- a/FirebaseMLModelDownloader/CHANGELOG.md +++ b/FirebaseMLModelDownloader/CHANGELOG.md @@ -1,3 +1,7 @@ +# 13.0.0 +- [removed] **Breaking change**: FirebaseMLModelDownloader has been removed. See + https://firebase.google.com/docs/ml for more info. + # 12.17.0 - [deprecated] Firebase ML is deprecated and will be shut down on June 15, 2027. To host custom models, you must migrate to another solution. You can use Cloud diff --git a/FirebaseMLModelDownloader/Sources/CustomModel.swift b/FirebaseMLModelDownloader/Sources/CustomModel.swift deleted file mode 100644 index 7fa2976af9c..00000000000 --- a/FirebaseMLModelDownloader/Sources/CustomModel.swift +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import Foundation - -/// A custom model that is stored remotely on the server and downloaded to the device. -@available( - *, - deprecated, - message: "Firebase ML is deprecated and will be shut down on June 15, 2027. To host custom models, you must migrate to another solution. You can use Cloud Storage for Firebase as an alternative for hosting custom models. For more info, see https://firebase.google.com/docs/ml/migrate-to-cloud-storage" -) -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 - - init(name: String, size: Int, path: String, hash: String) { - self.name = name - self.size = size - self.path = path - self.hash = hash - } - - /// Convenience init to create model from local model info. - init(localModelInfo: LocalModelInfo, path: String) { - self.init( - name: localModelInfo.name, - size: localModelInfo.size, - path: path, - hash: localModelInfo.modelHash - ) - } -} diff --git a/FirebaseMLModelDownloader/Sources/DeviceLogger.swift b/FirebaseMLModelDownloader/Sources/DeviceLogger.swift deleted file mode 100644 index bbcd78a67c2..00000000000 --- a/FirebaseMLModelDownloader/Sources/DeviceLogger.swift +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import Foundation - -internal import FirebaseCoreExtension - -/// Enum of log messages. -enum LoggerMessageCode: Int { - case downloaderInstanceCreated = 1 - case downloaderInstanceRetrieved - case downloaderInstanceDeleted - case modelDownloaded - case modelDownloadError - case retryDownload - case backgroundModelDownloaded - case backgroundDownloadError - case disableBackupError - case downloadedModelFileSaved - case downloadedModelSaveError - case anotherDownloadInProgressError - case invalidDownloadSessionError - case mergeRequests - case localModelFound - case allLocalModelsFound - case listModelsError - case modelNameParseError - case noLocalModelInfo - case noLocalModelFile - case outdatedModelPathError - case modelDeleted - case modelDeletionFailed - case validHTTPResponse - case validAuthToken - case invalidOptions - case invalidModelInfoFetchURL - case downloadedModelInfoSaved - case missingModelHash - case invalidModelInfoJSON - case modelInfoDeleted - case modelInfoDownloaded - case modelInfoUnmodified - case authTokenError - case expiredModelInfo - case modelHashMismatchError - case noModelHash - case modelInfoRetrievalError - case modelNotFound - case invalidArgument - case permissionDenied - case resourceExhausted - case notEnoughSpace - case hostnameError - case invalidHTTPResponse - case analyticsEventEncodeError - case telemetryInitError - case testError -} - -/// On-device logger. -enum DeviceLogger { - /// Log identifier. - static let service = "[Firebase/MLModelDownloader]" - - static func logEvent(level: FirebaseLoggerLevel, message: String, - messageCode: LoggerMessageCode) { - let code = String(format: "I-MLM%06d", messageCode.rawValue) - FirebaseLogger.log(level: level, service: DeviceLogger.service, code: code, message: message) - } -} diff --git a/FirebaseMLModelDownloader/Sources/DownloaderUserDefaults.swift b/FirebaseMLModelDownloader/Sources/DownloaderUserDefaults.swift deleted file mode 100644 index d1e0e79902e..00000000000 --- a/FirebaseMLModelDownloader/Sources/DownloaderUserDefaults.swift +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import Foundation -#if SWIFT_PACKAGE - internal import GoogleUtilities_UserDefaults -#else - internal import GoogleUtilities -#endif // SWIFT_PACKAGE - -/// Protocol to save or delete model info in user defaults. -protocol DownloaderUserDefaultsWriteable { - func writeToDefaults(_ defaults: GULUserDefaults, appName: String) - func removeFromDefaults(_ defaults: GULUserDefaults, appName: String) -} diff --git a/FirebaseMLModelDownloader/Sources/FileDownloader.swift b/FirebaseMLModelDownloader/Sources/FileDownloader.swift deleted file mode 100644 index b0fb4b8ed77..00000000000 --- a/FirebaseMLModelDownloader/Sources/FileDownloader.swift +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import Foundation - -/// File downloader response. -struct FileDownloaderResponse { - var urlResponse: HTTPURLResponse - var fileURL: URL -} - -/// Possible file downloader errors. -enum FileDownloaderError: Error { - case unexpectedResponseType - case networkError(Error) -} - -/// Protocol to download a file from server. -protocol FileDownloader { - typealias CompletionHandler = (Result) -> Void - typealias ProgressHandler = (_ bytesWritten: Int64, _ bytesExpectedToWrite: Int64) -> Void - - func downloadFile(with url: URL, - progressHandler: @escaping ProgressHandler, - completion: @escaping CompletionHandler) -} - -/// Downloader to get model files from server. -class ModelFileDownloader: NSObject, FileDownloader { - /// Model conditions for download. - private let conditions: ModelDownloadConditions - - /// URL session configuration. - private let configuration: URLSessionConfiguration - - /// Task to handle model file download. - private var downloadTask: URLSessionDownloadTask? - - /// URLSession to handle model downloads. - private lazy var downloadSession: URLSession = .init( - configuration: configuration, - delegate: self, - delegateQueue: nil - ) - /// Successful download completion handler. - private var completion: FileDownloader.CompletionHandler? - - /// Download progress handler. - private var progressHandler: FileDownloader.ProgressHandler? - - init(conditions: ModelDownloadConditions) { - self.conditions = conditions - configuration = URLSessionConfiguration.ephemeral - /// Wait for network connectivity. - configuration.waitsForConnectivity = true - /// Wait for 10 minutes. - configuration.timeoutIntervalForResource = 600 - configuration.allowsCellularAccess = conditions.allowsCellularAccess - } - - func downloadFile(with url: URL, - progressHandler: @escaping (Int64, Int64) -> Void, - completion: @escaping (Result) -> Void) { - // TODO: Fail if download already in progress. - self.completion = completion - self.progressHandler = progressHandler - let downloadTask = downloadSession.downloadTask(with: url) - // Begin or resume model download. - downloadTask.resume() - self.downloadTask = downloadTask - } -} - -/// Extension to handle delegate methods. -extension ModelFileDownloader: URLSessionDownloadDelegate { - // Handle client-side errors. - func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { - guard let error = error else { return } - session.finishTasksAndInvalidate() - // Unable to resolve hostname or connect to host. - completion?(.failure(FileDownloaderError.networkError(error))) - } - - /// Download completion. - func urlSession(_ session: URLSession, - downloadTask: URLSessionDownloadTask, - didFinishDownloadingTo location: URL) { - guard let response = downloadTask.response, - let urlResponse = response as? HTTPURLResponse else { - completion?(.failure(FileDownloaderError.unexpectedResponseType)) - return - } - let downloaderResponse = FileDownloaderResponse(urlResponse: urlResponse, fileURL: location) - session.finishTasksAndInvalidate() - completion?(.success(downloaderResponse)) - } - - /// Download progress. - func urlSession(_ session: URLSession, - downloadTask: URLSessionDownloadTask, - didWriteData bytesWritten: Int64, - totalBytesWritten: Int64, - totalBytesExpectedToWrite: Int64) { - progressHandler?(totalBytesWritten, totalBytesExpectedToWrite) - } -} diff --git a/FirebaseMLModelDownloader/Sources/LocalModelInfo.swift b/FirebaseMLModelDownloader/Sources/LocalModelInfo.swift deleted file mode 100644 index cd5aaedc301..00000000000 --- a/FirebaseMLModelDownloader/Sources/LocalModelInfo.swift +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import Foundation -#if SWIFT_PACKAGE - internal import GoogleUtilities_UserDefaults -#else - internal import GoogleUtilities -#endif // SWIFT_PACKAGE - -/// Model info object with details about downloaded and locally available model. -class LocalModelInfo { - /// Model name. - let name: String - - /// Hash of the model, as returned by server. - let modelHash: String - - /// Size of the model, as returned by server. - let size: Int - - init(name: String, modelHash: String, size: Int) { - self.name = name - self.modelHash = modelHash - self.size = size - } - - /// Convenience init to create local model info from remotely downloaded model info. - convenience init(from remoteModelInfo: RemoteModelInfo) { - self.init( - name: remoteModelInfo.name, - modelHash: remoteModelInfo.modelHash, - size: remoteModelInfo.size - ) - } - - /// Convenience init to create local model info from stored info in user defaults. - convenience init?(fromDefaults defaults: GULUserDefaults, name: String, appName: String) { - let defaultsPrefix = LocalModelInfo.getUserDefaultsKeyPrefix(appName: appName, modelName: name) - guard let modelHash = defaults.string(forKey: "\(defaultsPrefix).model-hash") else { - return nil - } - let size = defaults.integer(forKey: "\(defaultsPrefix).model-size") - self.init(name: name, modelHash: modelHash, size: size) - } -} - -/// Extension to write local model info to user defaults. -extension LocalModelInfo: DownloaderUserDefaultsWriteable { - // Get user defaults key prefix. - private static func getUserDefaultsKeyPrefix(appName: String, modelName: String) -> String { - let bundleID = Bundle.main.bundleIdentifier ?? "" - return "\(bundleID).\(appName).\(modelName)" - } - - /// Write local model info to user defaults. - func writeToDefaults(_ defaults: GULUserDefaults, appName: String) { - let defaultsPrefix = LocalModelInfo.getUserDefaultsKeyPrefix(appName: appName, modelName: name) - defaults.setObject(modelHash, forKey: "\(defaultsPrefix).model-hash") - defaults.setObject(size, forKey: "\(defaultsPrefix).model-size") - } - - func removeFromDefaults(_ defaults: GULUserDefaults, appName: String) { - let defaultsPrefix = LocalModelInfo.getUserDefaultsKeyPrefix(appName: appName, modelName: name) - defaults.removeObject(forKey: "\(defaultsPrefix).model-hash") - defaults.removeObject(forKey: "\(defaultsPrefix).model-size") - } -} - -/// Named user defaults for FirebaseML. -extension GULUserDefaults { - static var firebaseMLDefaults: GULUserDefaults { - return GULUserDefaults(suiteName: "com.google.firebase.ml") - } -} diff --git a/FirebaseMLModelDownloader/Sources/ModelDownloadConditions.swift b/FirebaseMLModelDownloader/Sources/ModelDownloadConditions.swift deleted file mode 100644 index f212790abd4..00000000000 --- a/FirebaseMLModelDownloader/Sources/ModelDownloadConditions.swift +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import Foundation - -/// Model download conditions. -@available( - *, - deprecated, - message: "Firebase ML is deprecated and will be shut down on June 15, 2027. To host custom models, you must migrate to another solution. You can use Cloud Storage for Firebase as an alternative for hosting custom models. For more info, see https://firebase.google.com/docs/ml/migrate-to-cloud-storage" -) -public struct ModelDownloadConditions { - let allowsCellularAccess: Bool - - /// Conditions that need to be met to start a model file download. - /// - Parameter allowsCellularAccess: Allow model downloading on a cellular connection. Default is - /// `true`. - public init(allowsCellularAccess: Bool = true) { - self.allowsCellularAccess = allowsCellularAccess - } -} diff --git a/FirebaseMLModelDownloader/Sources/ModelDownloadTask.swift b/FirebaseMLModelDownloader/Sources/ModelDownloadTask.swift deleted file mode 100644 index 1132d49143c..00000000000 --- a/FirebaseMLModelDownloader/Sources/ModelDownloadTask.swift +++ /dev/null @@ -1,309 +0,0 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import Foundation -#if SWIFT_PACKAGE - internal import GoogleUtilities_UserDefaults -#else - internal import GoogleUtilities -#endif // SWIFT_PACKAGE - -/// Task to download model file to device. -class ModelDownloadTask { - /// Name of the app associated with this instance of ModelDownloadTask. - private let appName: String - - /// Model info downloaded from server. - private(set) var remoteModelInfo: RemoteModelInfo - - /// User defaults to which local model info should ultimately be written. - private let defaults: GULUserDefaults - - /// Keeps track of download associated with this model download task. - private(set) var downloadStatus: ModelDownloadStatus = .ready - - /// Downloader instance. - private let downloader: FileDownloader - - /// Download progress handler. - typealias ProgressHandler = (Float) -> Void - private var progressHandler: ProgressHandler? - - /// Download completion handler. - typealias Completion = (Result) -> Void - private var completion: Completion - - init(remoteModelInfo: RemoteModelInfo, - appName: String, - defaults: GULUserDefaults, - downloader: FileDownloader, - progressHandler: ProgressHandler? = nil, - completion: @escaping Completion) { - self.remoteModelInfo = remoteModelInfo - self.appName = appName - self.defaults = defaults - self.downloader = downloader - self.progressHandler = progressHandler - self.completion = completion - } -} - -extension ModelDownloadTask { - /// Check if requests can be merged. - func canMergeRequests() -> Bool { - return downloadStatus != .complete - } - - /// Merge duplicate requests. This method is not thread-safe. - func merge(newProgressHandler: ProgressHandler? = nil, newCompletion: @escaping Completion) { - let originalProgressHandler = progressHandler - progressHandler = { progress in - originalProgressHandler?(progress) - newProgressHandler?(progress) - } - let originalCompletion = completion - completion = { result in - originalCompletion(result) - newCompletion(result) - } - } - - /// Check if download task can be resumed. - func canResume() -> Bool { - return downloadStatus == .ready - } - - /// Download model file. - func resume() { - // Prevent multiple concurrent downloads. - guard downloadStatus != .downloading else { - DeviceLogger.logEvent(level: .debug, - message: ModelDownloadTask.ErrorDescription.anotherDownloadInProgress, - messageCode: .anotherDownloadInProgressError) - return - } - downloadStatus = .downloading - downloader.downloadFile(with: remoteModelInfo.downloadURL, - progressHandler: { downloadedBytes, totalBytes in - /// Fraction of model file downloaded. - let calculatedProgress = Float(downloadedBytes) / Float(totalBytes) - self.progressHandler?(calculatedProgress) - }) { result in - self.downloadStatus = .complete - switch result { - case let .success(response): - DeviceLogger.logEvent(level: .debug, - message: ModelDownloadTask.DebugDescription - .receivedServerResponse, - messageCode: .validHTTPResponse) - self.handleResponse( - response: response.urlResponse, - tempURL: response.fileURL, - completion: self.completion - ) - case let .failure(error): - var downloadError: DownloadError - switch error { - case let FileDownloaderError.networkError(error): - let description = ModelDownloadTask.ErrorDescription - .invalidHostName(error.localizedDescription) - downloadError = .failedPrecondition - DeviceLogger.logEvent(level: .debug, - message: description, - messageCode: .hostnameError) - case FileDownloaderError.unexpectedResponseType: - let description = ModelDownloadTask.ErrorDescription.invalidHTTPResponse - downloadError = .internalError(description: description) - DeviceLogger.logEvent(level: .debug, - message: description, - messageCode: .invalidHTTPResponse) - default: - let description = ModelDownloadTask.ErrorDescription.unknownDownloadError - downloadError = .internalError(description: description) - DeviceLogger.logEvent(level: .debug, - message: description, - messageCode: .modelDownloadError) - } - self.completion(.failure(downloadError)) - } - } - } - - /// Handle model download response. - func handleResponse(response: HTTPURLResponse, tempURL: URL, completion: @escaping Completion) { - guard (200 ..< 299).contains(response.statusCode) else { - switch response.statusCode { - case 400: - // Possible failure due to download URL expiry. Check if download URL has expired. - guard remoteModelInfo.urlExpiryTime < Date() else { - DeviceLogger.logEvent(level: .debug, - message: ModelDownloadTask.ErrorDescription - .invalidArgument(remoteModelInfo.name), - messageCode: .invalidArgument) - completion(.failure(.invalidArgument)) - return - } - DeviceLogger.logEvent(level: .debug, - message: ModelDownloadTask.ErrorDescription.expiredModelInfo, - messageCode: .expiredModelInfo) - completion(.failure(.expiredDownloadURL)) - case 401, 403: - DeviceLogger.logEvent(level: .debug, - message: ModelDownloadTask.ErrorDescription.permissionDenied, - messageCode: .permissionDenied) - completion(.failure(.permissionDenied)) - case 404: - DeviceLogger.logEvent(level: .debug, - message: ModelDownloadTask.ErrorDescription - .modelNotFound(remoteModelInfo.name), - messageCode: .modelNotFound) - completion(.failure(.notFound)) - default: - let description = ModelDownloadTask.ErrorDescription - .modelDownloadFailed(response.statusCode) - DeviceLogger.logEvent(level: .debug, - message: description, - messageCode: .modelDownloadError) - completion(.failure(.internalError(description: description))) - } - return - } - - /// Construct local model file URL. - guard var modelFileURL = ModelFileManager.getDownloadedModelFileURL( - appName: appName, - modelName: remoteModelInfo.name - ) else { - // Could not create Application Support directory to store model files. - let description = ModelDownloadTask.ErrorDescription.noModelsDirectory - DeviceLogger.logEvent(level: .debug, - message: description, - messageCode: .downloadedModelSaveError) - // Downloading the file succeeding but saving failed. - completion(.failure(.internalError(description: description))) - return - } - - do { - // Try disabling iCloud backup for model files, because UserDefaults is not backed up. - var resourceValue = URLResourceValues() - resourceValue.isExcludedFromBackup = true - do { - try modelFileURL.setResourceValues(resourceValue) - } catch { - DeviceLogger.logEvent(level: .debug, - message: ModelDownloadTask.ErrorDescription.disableBackupError, - messageCode: .disableBackupError) - } - // Save model file to device. - try ModelFileManager.moveFile( - at: tempURL, - to: modelFileURL, - size: Int64(remoteModelInfo.size) - ) - DeviceLogger.logEvent(level: .debug, - message: ModelDownloadTask.DebugDescription.savedModelFile, - messageCode: .downloadedModelFileSaved) - // Generate local model info. - let localModelInfo = LocalModelInfo(from: remoteModelInfo) - // Write model info to user defaults. - localModelInfo.writeToDefaults(defaults, appName: appName) - DeviceLogger.logEvent(level: .debug, - message: ModelDownloadTask.DebugDescription.savedLocalModelInfo, - messageCode: .downloadedModelInfoSaved) - // Build model from model info and local path. - let model = CustomModel(localModelInfo: localModelInfo, path: modelFileURL.path) - DeviceLogger.logEvent(level: .debug, - message: ModelDownloadTask.DebugDescription.modelDownloaded, - messageCode: .modelDownloaded) - completion(.success(model)) - } catch let error as DownloadError { - if error == .notEnoughSpace { - DeviceLogger.logEvent(level: .debug, - message: ModelDownloadTask.ErrorDescription.notEnoughSpace, - messageCode: .notEnoughSpace) - } else { - DeviceLogger.logEvent(level: .debug, - message: ModelDownloadTask.ErrorDescription.modelSaveError, - messageCode: .downloadedModelSaveError) - } - // Downloading the file succeeding but saving failed. - completion(.failure(error)) - return - } catch { - DeviceLogger.logEvent(level: .debug, - message: ModelDownloadTask.ErrorDescription.modelSaveError, - messageCode: .downloadedModelSaveError) - // Downloading the file succeeding but saving failed. - completion(.failure(.internalError(description: error.localizedDescription))) - return - } - } -} - -/// Possible states of model downloading. -enum ModelDownloadStatus { - case ready - case downloading - case complete -} - -/// Download error codes. -enum ModelDownloadErrorCode { - case noError - case urlExpired - case noConnection - case downloadFailed - case httpError(code: Int) -} - -/// Possible debug and error messages for model downloading. -extension ModelDownloadTask { - /// Debug descriptions. - private enum DebugDescription { - static let receivedServerResponse = "Received a valid response from download server." - static let savedModelFile = "Model file saved successfully to device." - static let savedLocalModelInfo = "Downloaded model info saved successfully to user defaults." - static let modelDownloaded = "Model download completed successfully." - } - - /// Error descriptions. - private enum ErrorDescription { - static let invalidHostName = { (error: String) in - "Unable to resolve hostname or connect to host: \(error)" - } - - static let sessionInvalidated = "Session invalidated due to failed pre-conditions." - static let invalidHTTPResponse = "Could not get valid HTTP response for model downloading." - static let anotherDownloadInProgress = "Download already in progress." - static let modelNotFound = { (name: String) in - "No model found with name: \(name)" - } - - static let invalidArgument = { (name: String) in - "Invalid argument for model name: \(name)" - } - - static let expiredModelInfo = "Unable to update expired model info." - static let permissionDenied = "Invalid or missing permissions to download model." - static let notEnoughSpace = "Not enough space on device." - static let disableBackupError = "Unable to disable model file backup." - static let noModelsDirectory = "Could not create directory for model storage." - static let modelSaveError = "Unable to save downloaded remote model file." - static let unknownDownloadError = "Unable to download model due to unknown error." - static let modelDownloadFailed = { (code: Int) in - "Model download failed with HTTP error code: \(code)" - } - } -} diff --git a/FirebaseMLModelDownloader/Sources/ModelDownloader.swift b/FirebaseMLModelDownloader/Sources/ModelDownloader.swift deleted file mode 100644 index 6f9f610f0b2..00000000000 --- a/FirebaseMLModelDownloader/Sources/ModelDownloader.swift +++ /dev/null @@ -1,637 +0,0 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import FirebaseCore -import FirebaseInstallations -import Foundation -#if SWIFT_PACKAGE - internal import GoogleUtilities_UserDefaults -#else - internal import GoogleUtilities -#endif // SWIFT_PACKAGE - -/// Possible ways to get a custom model. -@available( - *, - deprecated, - message: "Firebase ML is deprecated and will be shut down on June 15, 2027. To host custom models, you must migrate to another solution. You can use Cloud Storage for Firebase as an alternative for hosting custom models. For more info, see https://firebase.google.com/docs/ml/migrate-to-cloud-storage" -) -public enum ModelDownloadType { - /// Get local model stored on device if available. If no local model on device, this is the same - /// as `latestModel`. - case localModel - /// Get local model on device if available and update to latest model from server in the - /// background. If no local model on device, this is the same as `latestModel`. - case localModelUpdateInBackground - /// Get latest model from server. Does not make a network call for model file download if local - /// model matches the latest version on server. - case latestModel -} - -/// Downloader to manage custom model downloads. -@available( - *, - deprecated, - message: "Firebase ML is deprecated and will be shut down on June 15, 2027. To host custom models, you must migrate to another solution. You can use Cloud Storage for Firebase as an alternative for hosting custom models. For more info, see https://firebase.google.com/docs/ml/migrate-to-cloud-storage" -) -public class ModelDownloader { - /// Name of the app associated with this instance of ModelDownloader. - private let appName: String - - /// Current Firebase app options. - private let options: FirebaseOptions - - /// Installations instance for current Firebase app. - private let installations: Installations - - /// User defaults for model info. - private let userDefaults: GULUserDefaults - - /// Number of retries in case of model download URL expiry. - var numberOfRetries: Int = 1 - - /// Shared dictionary mapping app name to a specific instance of model downloader. - // TODO: Switch to using Firebase components. - private static var modelDownloaderDictionary: [String: ModelDownloader] = [:] - - /// Download task associated with the model currently being downloaded. - private var currentDownloadTask: [String: ModelDownloadTask] = [:] - - /// DispatchQueue to manage download task dictionary. - let taskSerialQueue = DispatchQueue(label: "downloadtask.serial.queue") - - /// Re-dispatch a function on the main queue. - func asyncOnMainQueue(_ work: @autoclosure @escaping () -> Void) { - DispatchQueue.main.async { - work() - } - } - - /// Private init for model downloader. - private init(app: FirebaseApp, defaults: GULUserDefaults = .firebaseMLDefaults) { - appName = app.name - options = app.options - installations = Installations.installations(app: app) - userDefaults = defaults - // Notification of app deletion. - let notificationName = "FIRAppDeleteNotification" - NotificationCenter.default.addObserver( - self, - selector: #selector(deleteModelDownloader), - name: Notification.Name(notificationName), - object: nil - ) - } - - /// Handles app deletion notification. - @objc private func deleteModelDownloader(notification: Notification) { - let userInfoKey = "FIRAppNameKey" - if let userInfo = notification.userInfo, - let appName = userInfo[userInfoKey] as? String { - ModelDownloader.modelDownloaderDictionary.removeValue(forKey: appName) - // TODO: Clean up user defaults. - // TODO: Clean up local instances of app. - DeviceLogger.logEvent(level: .debug, - message: ModelDownloader.DebugDescription.deleteModelDownloader, - messageCode: .downloaderInstanceDeleted) - } - } - - /// Model downloader with default app. - public static func modelDownloader() -> ModelDownloader { - guard let defaultApp = FirebaseApp.app() else { - fatalError(ModelDownloader.ErrorDescription.defaultAppNotConfigured) - } - return modelDownloader(app: defaultApp) - } - - /// Model Downloader with custom app. - public static func modelDownloader(app: FirebaseApp) -> ModelDownloader { - if let downloader = modelDownloaderDictionary[app.name] { - DeviceLogger.logEvent(level: .debug, - message: ModelDownloader.DebugDescription.retrieveModelDownloader, - messageCode: .downloaderInstanceRetrieved) - return downloader - } else { - let downloader = ModelDownloader(app: app) - modelDownloaderDictionary[app.name] = downloader - DeviceLogger.logEvent(level: .debug, - message: ModelDownloader.DebugDescription.createModelDownloader, - messageCode: .downloaderInstanceCreated) - return downloader - } - } - - /// Downloads a custom model to device or gets a custom model already on device, with an optional - /// handler for progress. - /// - Parameters: - /// - modelName: The name of the model, matching Firebase console. - /// - downloadType: ModelDownloadType used to get the model. - /// - conditions: Conditions needed to perform a model download. - /// - progressHandler: Optional. Returns a float in [0.0, 1.0] that can be used to monitor model - /// download progress. - /// - completion: Returns either a `CustomModel` on success, or a `DownloadError` on failure, at - /// the end of a model download. - public func getModel(name modelName: String, - downloadType: ModelDownloadType, - conditions: ModelDownloadConditions, - progressHandler: ((Float) -> Void)? = nil, - completion: @escaping (Result) -> Void) { - guard !modelName.isEmpty else { - asyncOnMainQueue(completion(.failure(.emptyModelName))) - return - } - - switch downloadType { - case .localModel: - if let localModel = getLocalModel(modelName: modelName) { - DeviceLogger.logEvent(level: .debug, - message: ModelDownloader.DebugDescription.localModelFound, - messageCode: .localModelFound) - asyncOnMainQueue(completion(.success(localModel))) - } else { - getRemoteModel( - modelName: modelName, - conditions: conditions, - progressHandler: progressHandler, - completion: completion - ) - } - - case .localModelUpdateInBackground: - if let localModel = getLocalModel(modelName: modelName) { - DeviceLogger.logEvent(level: .debug, - message: ModelDownloader.DebugDescription.localModelFound, - messageCode: .localModelFound) - asyncOnMainQueue(completion(.success(localModel))) - // Update local model in the background. - DispatchQueue.global(qos: .utility).async { [weak self] in - self?.getRemoteModel( - modelName: modelName, - conditions: conditions, - progressHandler: nil, - completion: { result in - switch result { - case .success: - DeviceLogger.logEvent(level: .debug, - message: ModelDownloader.DebugDescription - .backgroundModelDownloaded, - messageCode: .backgroundModelDownloaded) - case .failure: - DeviceLogger.logEvent(level: .debug, - message: ModelDownloader.ErrorDescription - .backgroundModelDownload, - messageCode: .backgroundDownloadError) - } - } - ) - } - } else { - getRemoteModel( - modelName: modelName, - conditions: conditions, - progressHandler: progressHandler, - completion: completion - ) - } - - case .latestModel: - getRemoteModel( - modelName: modelName, - conditions: conditions, - progressHandler: progressHandler, - completion: completion - ) - } - } - - /// Gets the set of all downloaded models saved on device. - /// - Parameter completion: Returns either a set of `CustomModel` models on success, or a - /// `DownloadedModelError` on failure. - public func listDownloadedModels(completion: @escaping (Result, - DownloadedModelError>) -> Void) { - do { - let modelURLs = try ModelFileManager.contentsOfModelsDirectory() - var customModels = Set() - // Retrieve model name from URL. - for url in modelURLs { - guard let modelName = ModelFileManager.getModelNameFromFilePath(url) else { - let description = ModelDownloader.ErrorDescription.parseModelName(url.path) - DeviceLogger.logEvent(level: .debug, - message: description, - messageCode: .modelNameParseError) - asyncOnMainQueue(completion(.failure(.internalError(description: description)))) - return - } - // Check if model information corresponding to model is stored in UserDefaults. - guard let modelInfo = getLocalModelInfo(modelName: modelName) else { - let description = ModelDownloader.ErrorDescription.noLocalModelInfo(modelName) - DeviceLogger.logEvent(level: .debug, - message: description, - messageCode: .noLocalModelInfo) - asyncOnMainQueue(completion(.failure(.internalError(description: description)))) - return - } - // Ensure that local model path is as expected, and reachable. - guard let modelURL = ModelFileManager.getDownloadedModelFileURL( - appName: appName, - modelName: modelName - ), - ModelFileManager.isFileReachable(at: modelURL) else { - DeviceLogger.logEvent(level: .debug, - message: ModelDownloader.ErrorDescription.outdatedModelPath, - messageCode: .outdatedModelPathError) - asyncOnMainQueue(completion(.failure(.internalError(description: ModelDownloader - .ErrorDescription.outdatedModelPath)))) - return - } - let model = CustomModel(localModelInfo: modelInfo, path: modelURL.path) - // Add model to result set. - customModels.insert(model) - } - DeviceLogger.logEvent(level: .debug, - message: ModelDownloader.DebugDescription.allLocalModelsFound, - messageCode: .allLocalModelsFound) - completion(.success(customModels)) - } catch let error as DownloadedModelError { - DeviceLogger.logEvent(level: .debug, - message: ModelDownloader.ErrorDescription.listModelsFailed(error), - messageCode: .listModelsError) - asyncOnMainQueue(completion(.failure(error))) - } catch { - DeviceLogger.logEvent(level: .debug, - message: ModelDownloader.ErrorDescription.listModelsFailed(error), - messageCode: .listModelsError) - asyncOnMainQueue(completion(.failure(.internalError(description: error - .localizedDescription)))) - } - } - - /// Deletes a custom model file from device as well as corresponding model information saved in - /// UserDefaults. - /// - Parameters: - /// - modelName: The name of the model, matching Firebase console and already downloaded to - /// device. - /// - completion: Returns a `DownloadedModelError` on failure. - public func deleteDownloadedModel(name modelName: String, - completion: @escaping (Result) - -> Void) { - // Ensure that there is a matching model file on device, with corresponding model information in - // UserDefaults. - guard let modelURL = ModelFileManager.getDownloadedModelFileURL( - appName: appName, - modelName: modelName - ), - let localModelInfo = getLocalModelInfo(modelName: modelName), - ModelFileManager.isFileReachable(at: modelURL) - else { - DeviceLogger.logEvent(level: .debug, - message: ModelDownloader.ErrorDescription.modelNotFound(modelName), - messageCode: .modelNotFound) - asyncOnMainQueue(completion(.failure(.notFound))) - return - } - do { - // Remove model file from device. - try ModelFileManager.removeFile(at: modelURL) - // Clear out corresponding local model info. - localModelInfo.removeFromDefaults(userDefaults, appName: appName) - DeviceLogger.logEvent(level: .debug, - message: ModelDownloader.DebugDescription.modelDeleted, - messageCode: .modelDeleted) - asyncOnMainQueue(completion(.success(()))) - } catch let error as DownloadedModelError { - DeviceLogger.logEvent(level: .debug, - message: ModelDownloader.ErrorDescription.modelDeletionFailed(error), - messageCode: .modelDeletionFailed) - asyncOnMainQueue(completion(.failure(error))) - } catch { - DeviceLogger.logEvent(level: .debug, - message: ModelDownloader.ErrorDescription.modelDeletionFailed(error), - messageCode: .modelDeletionFailed) - asyncOnMainQueue(completion(.failure(.internalError(description: error - .localizedDescription)))) - } - } -} - -extension ModelDownloader { - /// Get model information for model saved on device, if available. - private func getLocalModelInfo(modelName: String) -> LocalModelInfo? { - guard let localModelInfo = LocalModelInfo( - fromDefaults: userDefaults, - name: modelName, - appName: appName - ) else { - let description = ModelDownloader.DebugDescription.noLocalModelInfo(modelName) - DeviceLogger.logEvent(level: .debug, - message: description, - messageCode: .noLocalModelInfo) - return nil - } - /// Local model info is only considered valid if there is a corresponding model file on device. - guard let modelURL = ModelFileManager.getDownloadedModelFileURL( - appName: appName, - modelName: modelName - ), ModelFileManager.isFileReachable(at: modelURL) else { - let description = ModelDownloader.DebugDescription.noLocalModelFile(modelName) - DeviceLogger.logEvent(level: .debug, - message: description, - messageCode: .noLocalModelFile) - return nil - } - return localModelInfo - } - - /// Get model saved on device, if available. - private func getLocalModel(modelName: String) -> CustomModel? { - guard let modelURL = ModelFileManager.getDownloadedModelFileURL( - appName: appName, - modelName: modelName - ), let localModelInfo = getLocalModelInfo(modelName: modelName) else { return nil } - let model = CustomModel(localModelInfo: localModelInfo, path: modelURL.path) - return model - } - - /// Download and get model from server, unless the latest model is already available on device. - private func getRemoteModel(modelName: String, - conditions: ModelDownloadConditions, - progressHandler: ((Float) -> Void)? = nil, - completion: @escaping (Result) -> Void) { - let localModelInfo = getLocalModelInfo(modelName: modelName) - guard let projectID = options.projectID, let apiKey = options.apiKey else { - DeviceLogger.logEvent(level: .debug, - message: ModelDownloader.ErrorDescription.invalidOptions, - messageCode: .invalidOptions) - completion(.failure(.internalError(description: ModelDownloader.ErrorDescription - .invalidOptions))) - return - } - let modelInfoRetriever = ModelInfoRetriever( - modelName: modelName, - projectID: projectID, - apiKey: apiKey, - appName: appName, installations: installations, - localModelInfo: localModelInfo - ) - let downloader = ModelFileDownloader(conditions: conditions) - downloadInfoAndModel( - modelName: modelName, - modelInfoRetriever: modelInfoRetriever, - downloader: downloader, - conditions: conditions, - progressHandler: progressHandler, - completion: completion - ) - } - - /// Get model info and model file from server. - func downloadInfoAndModel(modelName: String, - modelInfoRetriever: ModelInfoRetriever, - downloader: FileDownloader, - conditions: ModelDownloadConditions, - progressHandler: ((Float) -> Void)? = nil, - completion: @escaping (Result) - -> Void) { - modelInfoRetriever.downloadModelInfo { result in - switch result { - case let .success(downloadModelInfoResult): - switch downloadModelInfoResult { - // New model info was downloaded from server. - case let .modelInfo(remoteModelInfo): - // Progress handler for model file download. - let taskProgressHandler: ModelDownloadTask.ProgressHandler = { progress in - if let progressHandler { - self.asyncOnMainQueue(progressHandler(progress)) - } - } - // Completion handler for model file download. - let taskCompletion: ModelDownloadTask.Completion = { result in - switch result { - case let .success(model): - self.asyncOnMainQueue(completion(.success(model))) - case let .failure(error): - switch error { - case .notFound: - self.asyncOnMainQueue(completion(.failure(.notFound))) - case .invalidArgument: - self.asyncOnMainQueue(completion(.failure(.invalidArgument))) - case .permissionDenied: - self.asyncOnMainQueue(completion(.failure(.permissionDenied))) - // This is the error returned when model download URL has expired. - case .expiredDownloadURL: - // Retry model info and model file download, if allowed. - guard self.numberOfRetries > 0 else { - self - .asyncOnMainQueue( - completion(.failure(.internalError(description: ModelDownloader - .ErrorDescription - .expiredModelInfo))) - ) - return - } - self.numberOfRetries -= 1 - DeviceLogger.logEvent(level: .debug, - message: ModelDownloader.DebugDescription.retryDownload, - messageCode: .retryDownload) - self.downloadInfoAndModel( - modelName: modelName, - modelInfoRetriever: modelInfoRetriever, - downloader: downloader, - conditions: conditions, - progressHandler: progressHandler, - completion: completion - ) - default: - self.asyncOnMainQueue(completion(.failure(error))) - } - } - self.taskSerialQueue.async { - // Stop keeping track of current download task. - self.currentDownloadTask.removeValue(forKey: modelName) - } - } - self.taskSerialQueue.sync { - // Merge duplicate requests if there is already a download in progress for the same - // model. - if let downloadTask = self.currentDownloadTask[modelName], - downloadTask.canMergeRequests() { - downloadTask.merge( - newProgressHandler: taskProgressHandler, - newCompletion: taskCompletion - ) - DeviceLogger.logEvent(level: .debug, - message: ModelDownloader.DebugDescription.mergingRequests, - messageCode: .mergeRequests) - if downloadTask.canResume() { - downloadTask.resume() - } - // TODO: Handle else. - } else { - // Create download task for model file download. - let downloadTask = ModelDownloadTask( - remoteModelInfo: remoteModelInfo, - appName: self.appName, - defaults: self.userDefaults, - downloader: downloader, - progressHandler: taskProgressHandler, - completion: taskCompletion - ) - // Keep track of current download task to allow for merging duplicate requests. - self.currentDownloadTask[modelName] = downloadTask - downloadTask.resume() - } - } - /// Local model info is the latest model info. - case .notModified: - guard let localModel = self.getLocalModel(modelName: modelName) else { - // This can only happen if either local model info or the model file was wiped out after - // model info request but before server response. - self - .asyncOnMainQueue(completion(.failure(.internalError(description: ModelDownloader - .ErrorDescription.deletedLocalModelInfoOrFile)))) - return - } - self.asyncOnMainQueue(completion(.success(localModel))) - } - // Error retrieving model info. - case let .failure(error): - self.asyncOnMainQueue(completion(.failure(error))) - } - } - } -} - -/// Possible errors with model downloading. -@available( - *, - deprecated, - message: "Firebase ML is deprecated and will be shut down on June 15, 2027. To host custom models, you must migrate to another solution. You can use Cloud Storage for Firebase as an alternative for hosting custom models. For more info, see https://firebase.google.com/docs/ml/migrate-to-cloud-storage" -) -public enum DownloadError: Error, Equatable { - /// No model with this name exists on server. - case notFound - /// Invalid, incomplete, or missing permissions for model download. - case permissionDenied - /// Conditions not met to perform download. - case failedPrecondition - /// Requests quota exhausted. - case resourceExhausted - /// Not enough space for model on device. - case notEnoughSpace - /// Malformed model name or Firebase app options. - case invalidArgument - /// Model name is empty. - case emptyModelName - /// Other errors with description. - case internalError(description: String) -} - -/// Possible errors with locating a model file on device. -@available( - *, - deprecated, - message: "Firebase ML is deprecated and will be shut down on June 15, 2027. To host custom models, you must migrate to another solution. You can use Cloud Storage for Firebase as an alternative for hosting custom models. For more info, see https://firebase.google.com/docs/ml/migrate-to-cloud-storage" -) -public enum DownloadedModelError: Error { - /// No model with this name exists on device. - case notFound - /// File system error. - case fileIOError(description: String) - /// Other errors with description. - case internalError(description: String) -} - -/// Extension to handle internally meaningful errors. -extension DownloadError { - /// Model download URL expired before model download. - // Model info retrieval and download is retried `numberOfRetries` times before failing. - static let expiredDownloadURL: DownloadError = - .internalError(description: "Expired model download URL.") -} - -/// Possible debug and error messages while using model downloader. -extension ModelDownloader { - /// Debug descriptions. - private enum DebugDescription { - static let createModelDownloader = - "Initialized with new downloader instance associated with this app." - static let retrieveModelDownloader = - "Initialized with existing downloader instance associated with this app." - static let deleteModelDownloader = "Model downloader instance deleted due to app deletion." - static let localModelFound = "Found local model on device." - static let allLocalModelsFound = "Found and listed all local models." - static let noLocalModelInfo = { (name: String) in - "No local model info for model named: \(name)." - } - - static let noLocalModelFile = { (name: String) in - "No local model file for model named: \(name)." - } - - static let backgroundModelDownloaded = "Downloaded latest model in the background." - static let modelDeleted = "Model deleted successfully." - static let mergingRequests = "Merging duplicate download requests." - static let retryDownload = "Retrying download." - } - - /// Error descriptions. - private enum ErrorDescription { - static let defaultAppNotConfigured = "Default Firebase app not configured." - static let invalidOptions = "Unable to retrieve project ID and/or API key for Firebase app." - static let modelDownloadFailed = { (error: Error) in - "Model download failed with error: \(error)" - } - - static let modelNotFound = { (name: String) in - "Model deletion failed due to no model found with name: \(name)" - } - - static let modelInfoRetrievalFailed = { (error: Error) in - "Model info retrieval failed with error: \(error)" - } - - static let backgroundModelDownload = "Failed to update model in background." - static let expiredModelInfo = "Unable to update expired model info." - static let listModelsFailed = { (error: Error) in - "Unable to list models, failed with error: \(error)" - } - - static let parseModelName = { (path: String) in - "List models failed due to unexpected model file name at \(path)." - } - - static let noLocalModelInfo = { (name: String) in - "List models failed due to no local model info for model file named: \(name)." - } - - static let deletedLocalModelInfoOrFile = - "Model unavailable due to deleted local model info or model file." - static let outdatedModelPath = - "List models failed due to outdated model paths in local storage." - static let modelDeletionFailed = { (error: Error) in - "Model deletion failed with error: \(error)" - } - } -} - -/// Model downloader extension for testing. -extension ModelDownloader { - /// Model downloader instance for testing. - static func modelDownloaderWithDefaults(_ defaults: GULUserDefaults, - app: FirebaseApp) -> ModelDownloader { - let downloader = ModelDownloader(app: app, defaults: defaults) - return downloader - } -} diff --git a/FirebaseMLModelDownloader/Sources/ModelFileManager.swift b/FirebaseMLModelDownloader/Sources/ModelFileManager.swift deleted file mode 100644 index 8e9acdef84a..00000000000 --- a/FirebaseMLModelDownloader/Sources/ModelFileManager.swift +++ /dev/null @@ -1,212 +0,0 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import Foundation - -/// Manager for common file operations. -enum ModelFileManager { - /// Separator in model file name components. - private static let nameSeparator = "@@" - - private static let modelNamePrefix = "fbml_model" - - private static let fileExtension = "tflite" - - private static let fileManager = FileManager.default - - /// Root directory of model file storage on device. - static var modelsDirectory: URL? { - let rootDirOptional: URL? = { - #if os(tvOS) - return fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first - #else - return fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first ?? nil - #endif - }() - - guard let rootDirURL = rootDirOptional else { - return nil - } - - let modelDirURL = rootDirURL.appendingPathComponent( - "com.firebase.FirebaseMLModelDownloader", - isDirectory: true - ) - - do { - if !fileManager.fileExists(atPath: modelDirURL.absoluteString) { - try fileManager.createDirectory(at: modelDirURL, withIntermediateDirectories: true) - } - } catch { - return nil - } - - return modelDirURL - } - - /// Name for model file stored on device. - static func getDownloadedModelFileName(appName: String, modelName: String) -> String { - return [modelNamePrefix, appName, modelName].joined(separator: nameSeparator) - } - - /// Model name from file path. - static func getModelNameFromFilePath(_ path: URL) -> String? { - let components = path.deletingPathExtension().lastPathComponent - .components(separatedBy: nameSeparator) - // The file name should have prefix, app name, and model name. - if components.count == 3 { - return components.last - } - return nil - } - - /// Full path of model file stored on device. - static func getDownloadedModelFileURL(appName: String, modelName: String) -> URL? { - let modelFileName = ModelFileManager.getDownloadedModelFileName( - appName: appName, - modelName: modelName - ) - guard let modelsDir = ModelFileManager.modelsDirectory else { return nil } - return modelsDir.appendingPathComponent(modelFileName).appendingPathExtension(fileExtension) - } - - /// Check if file is available at URL. - static func isFileReachable(at fileURL: URL) -> Bool { - do { - return try fileURL.checkResourceIsReachable() - } catch { - // File unreachable. - return false - } - } - - /// Move file at a location to another location. - static func moveFile(at sourceURL: URL, to destinationURL: URL, size: Int64) throws { - if isFileReachable(at: destinationURL) { - do { - try fileManager.removeItem(at: destinationURL) - } catch { - // TODO: Handle this - new model file downloaded but not saved due to FileManager error. - throw DownloadError - .internalError( - description: ModelFileManager.ErrorDescription - .replaceFile(error.localizedDescription) - ) - } - } - do { - try fileManager.moveItem(at: sourceURL, to: destinationURL) - } catch CocoaError.fileWriteOutOfSpace { - throw DownloadError.notEnoughSpace - } catch { - throw DownloadError - .internalError(description: ModelFileManager - .ErrorDescription.saveFile(error.localizedDescription)) - } - } - - /// Remove model file at a specific location. - static func removeFile(at url: URL) throws { - do { - try fileManager.removeItem(at: url) - } catch { - throw DownloadedModelError - .internalError( - description: ModelFileManager.ErrorDescription.deleteFile(error.localizedDescription) - ) - } - } - - /// Get all model files in models directory. - static func contentsOfModelsDirectory() throws -> [URL] { - guard let modelsDir = modelsDirectory else { - throw DownloadedModelError - .internalError( - description: ModelFileManager.ErrorDescription.noApplicationSupportDirectory - ) - } - do { - let directoryContents = try ModelFileManager.fileManager.contentsOfDirectory( - at: modelsDir, - includingPropertiesForKeys: nil, - options: .skipsHiddenFiles - ) - return directoryContents.filter { directoryItem in - directoryItem.path.contains(ModelFileManager.modelNamePrefix) && !directoryItem - .hasDirectoryPath - } - } catch { - throw DownloadedModelError - .internalError( - description: ModelFileManager.ErrorDescription - .retrieveFile(error.localizedDescription) - ) - } - } -} - -/// NOTE: Use for testing only. -extension ModelFileManager { - static func emptyModelsDirectory() throws { - do { - let directoryContents = try ModelFileManager.contentsOfModelsDirectory() - for path in directoryContents { - try ModelFileManager.removeFile(at: path) - } - } catch { - throw DownloadedModelError - .internalError( - description: ModelFileManager.ErrorDescription - .emptyDirectory(error.localizedDescription) - ) - } - } -} - -/// Possible error messages during file management. -extension ModelFileManager { - /// Error descriptions. - private enum ErrorDescription { - static let retrieveFile = { (error: String) in - "Could not retrieve model files in directory: \(error)" - } - - static let deleteFile = { (error: String) in - "Could not delete old model file: \(error)" - } - - static let saveFile = { (error: String) in - "Unable to save model file: \(error)" - } - - static let replaceFile = { (error: String) in - "Could not replace existing model file: \(error)" - } - - static let availableStorage = { (error: String?) -> String in - if let error { - return "Failed to check storage capacity on device: \(error)" - } else { - return "Failed to check storage capacity on device." - } - } - - static let noApplicationSupportDirectory = - "Could not locate Application Support directory for model storage." - - static let emptyDirectory = { (error: String) in - "Could not empty models directory: \(error)" - } - } -} diff --git a/FirebaseMLModelDownloader/Sources/ModelInfoRetriever.swift b/FirebaseMLModelDownloader/Sources/ModelInfoRetriever.swift deleted file mode 100644 index ceb12c928cb..00000000000 --- a/FirebaseMLModelDownloader/Sources/ModelInfoRetriever.swift +++ /dev/null @@ -1,463 +0,0 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import FirebaseCore -import FirebaseInstallations -import Foundation - -/// URL Session to use while retrieving model info. -protocol ModelInfoRetrieverSession { - func getModelInfo(with request: URLRequest, - completion: @escaping (Data?, URLResponse?, Error?) -> Void) -} - -/// Extension to customize data task requests. -extension URLSession: ModelInfoRetrieverSession { - func getModelInfo(with request: URLRequest, - completion: @escaping (Data?, URLResponse?, Error?) -> Void) { - let task = dataTask(with: request) { data, response, error in - completion(data, response, error) - } - task.resume() - } -} - -/// Model info result type. -/// Downloading model info will return new model info only if it different from local model info. -enum DownloadModelInfoResult { - case modelInfo(RemoteModelInfo) - case notModified -} - -/// Model info response. -private struct ModelInfoResponse: Codable { - let downloadURL: String - let urlExpiryTime: String - let size: String - /// Properties for server response keys. - enum CodingKeys: String, CodingKey { - case downloadURL = "downloadUri" - case urlExpiryTime = "expireTime" - case size = "sizeBytes" - } -} - -/// Fetch model info for a model from server. -class ModelInfoRetriever { - /// Model name. - private let modelName: String - - /// Current Firebase app project ID. - private let projectID: String - - /// Current Firebase app API key. - private let apiKey: String - - /// Current Firebase app name. - private let appName: String - - /// Auth token provider. - typealias AuthTokenProvider = (_ completion: @escaping (Result) -> Void) - -> Void - private let authTokenProvider: AuthTokenProvider - - /// URL session for model info request. - private let session: ModelInfoRetrieverSession - - /// Local model info to validate model freshness. - private let localModelInfo: LocalModelInfo? - - /// Associate model info retriever with current Firebase app, and model name. - init(modelName: String, - projectID: String, - apiKey: String, - appName: String, - authTokenProvider: @escaping AuthTokenProvider, - session: ModelInfoRetrieverSession? = nil, - localModelInfo: LocalModelInfo? = nil) { - self.modelName = modelName - self.projectID = projectID - self.apiKey = apiKey - self.appName = appName - self.authTokenProvider = authTokenProvider - self.session = session ?? URLSession(configuration: .ephemeral) - self.localModelInfo = localModelInfo - } - - /// Convenience init to use FirebaseInstallations as auth token provider. - convenience init(modelName: String, - projectID: String, - apiKey: String, - appName: String, - installations: Installations, - session: ModelInfoRetrieverSession? = nil, - localModelInfo: LocalModelInfo? = nil) { - self.init(modelName: modelName, - projectID: projectID, - apiKey: apiKey, - appName: appName, - authTokenProvider: ModelInfoRetriever.authTokenProvider(installation: installations), - session: session, - localModelInfo: localModelInfo) - } - - /// Auth token provider to validate credentials. - private static func authTokenProvider(installation: Installations) -> AuthTokenProvider { - return { completion in - installation.authToken { tokenResult, error in - guard let result = tokenResult - else { - completion(.failure(.internalError(description: ModelInfoRetriever.ErrorDescription - .authTokenError))) - return - } - completion(.success(result.authToken)) - } - } - } - - /// Get model info from server. - func downloadModelInfo(completion: @escaping (Result) - -> Void) { - authTokenProvider { result in - switch result { - // Successfully received FIS token. - case let .success(authToken): - DeviceLogger.logEvent(level: .debug, - message: ModelInfoRetriever.DebugDescription - .receivedAuthToken, - messageCode: .validAuthToken) - // Get model info fetch URL with appropriate HTTP headers. - guard let request = self.getModelInfoFetchURLRequest(token: authToken) else { - DeviceLogger.logEvent(level: .debug, - message: ModelInfoRetriever.ErrorDescription - .invalidModelInfoFetchURL, - messageCode: .invalidModelInfoFetchURL) - completion(.failure(.internalError(description: ModelInfoRetriever.ErrorDescription - .invalidModelInfoFetchURL))) - return - } - // Download model info. - self.session.getModelInfo(with: request) { - data, response, error in - if let downloadError = error { - let description = ModelInfoRetriever.ErrorDescription - .failedModelInfoRetrieval(downloadError.localizedDescription) - DeviceLogger.logEvent(level: .debug, - message: description, - messageCode: .modelInfoRetrievalError) - completion(.failure(.internalError(description: description))) - } else { - guard let httpResponse = response as? HTTPURLResponse else { - DeviceLogger.logEvent(level: .debug, - message: ModelInfoRetriever.ErrorDescription - .invalidHTTPResponse, - messageCode: .invalidHTTPResponse) - completion(.failure(.internalError(description: ModelInfoRetriever.ErrorDescription - .invalidHTTPResponse))) - return - } - DeviceLogger.logEvent(level: .debug, - message: ModelInfoRetriever.DebugDescription - .receivedServerResponse, - messageCode: .validHTTPResponse) - switch httpResponse.statusCode { - case 200: - guard let modelHash = httpResponse - .allHeaderFields[ModelInfoRetriever.etagHTTPHeader] as? String else { - DeviceLogger.logEvent(level: .debug, - message: ModelInfoRetriever.ErrorDescription.missingModelHash, - messageCode: .missingModelHash) - completion(.failure(.internalError(description: ModelInfoRetriever.ErrorDescription - .missingModelHash))) - return - } - guard let data = data else { - DeviceLogger.logEvent(level: .debug, - message: ModelInfoRetriever.ErrorDescription - .invalidHTTPResponse, - messageCode: .invalidHTTPResponse) - completion(.failure(.internalError(description: ModelInfoRetriever.ErrorDescription - .invalidHTTPResponse))) - return - } - do { - // Parse model info from HTTP response. - let modelInfo = try self.getRemoteModelInfoFromResponse(data, modelHash: modelHash) - DeviceLogger.logEvent(level: .debug, - message: ModelInfoRetriever.DebugDescription - .modelInfoDownloaded, - messageCode: .modelInfoDownloaded) - completion(.success(.modelInfo(modelInfo))) - } catch { - let description = ModelInfoRetriever.ErrorDescription - .invalidModelInfoJSON(error.localizedDescription) - DeviceLogger.logEvent(level: .debug, - message: description, - messageCode: .invalidModelInfoJSON) - completion( - .failure(.internalError(description: description)) - ) - } - case 304: - // For this case to occur, local model info has to already be available on device. - guard let localInfo = self.localModelInfo else { - DeviceLogger.logEvent(level: .debug, - message: ModelInfoRetriever.ErrorDescription - .unexpectedModelInfoDeletion, - messageCode: .modelInfoDeleted) - completion( - .failure(.internalError(description: ModelInfoRetriever.ErrorDescription - .unexpectedModelInfoDeletion)) - ) - return - } - guard let modelHash = httpResponse - .allHeaderFields[ModelInfoRetriever.etagHTTPHeader] as? String else { - DeviceLogger.logEvent(level: .debug, - message: ModelInfoRetriever.ErrorDescription - .missingModelHash, - messageCode: .noModelHash) - completion(.failure(.internalError(description: ModelInfoRetriever.ErrorDescription - .missingModelHash))) - return - } - // Ensure that there is local model info on device with matching hash. - guard modelHash == localInfo.modelHash else { - DeviceLogger.logEvent(level: .debug, - message: ModelInfoRetriever.ErrorDescription - .modelHashMismatch, - messageCode: .modelHashMismatchError) - completion(.failure(.internalError(description: ModelInfoRetriever.ErrorDescription - .modelHashMismatch))) - return - } - DeviceLogger.logEvent(level: .debug, - message: ModelInfoRetriever.DebugDescription - .modelInfoUnmodified, - messageCode: .modelInfoUnmodified) - completion(.success(.notModified)) - case 400: - let errorMessage = self.getErrorFromResponse(data) - let description = errorMessage ?? ModelInfoRetriever.ErrorDescription - .invalidArgument(self.modelName) - DeviceLogger.logEvent(level: .debug, - message: description, - messageCode: .invalidArgument) - completion(.failure(.invalidArgument)) - case 401, 403: - // Error could be due to FirebaseML API not enabled for project, or invalid - // permissions. - let errorMessage = self.getErrorFromResponse(data) - let description = errorMessage ?? ModelInfoRetriever.ErrorDescription.permissionDenied - DeviceLogger.logEvent(level: .debug, - message: description, - messageCode: .permissionDenied) - completion(.failure(.permissionDenied)) - case 404: - let errorMessage = self.getErrorFromResponse(data) - let description = errorMessage ?? ModelInfoRetriever.ErrorDescription - .modelNotFound(self.modelName) - DeviceLogger.logEvent(level: .debug, - message: description, - messageCode: .modelNotFound) - completion(.failure(.notFound)) - case 429: - let errorMessage = self.getErrorFromResponse(data) - let description = errorMessage ?? ModelInfoRetriever.ErrorDescription - .resourceExhausted - DeviceLogger.logEvent(level: .debug, - message: description, - messageCode: .resourceExhausted) - completion(.failure(.resourceExhausted)) - default: - let errorMessage = self.getErrorFromResponse(data) - let description = errorMessage ?? ModelInfoRetriever.ErrorDescription - .modelInfoRetrievalFailed(httpResponse.statusCode) - DeviceLogger.logEvent(level: .debug, - message: description, - messageCode: .modelInfoRetrievalError) - completion(.failure(.internalError(description: description))) - } - } - } - // Error retrieving auth token. - case .failure: - DeviceLogger.logEvent(level: .debug, - message: ModelInfoRetriever.ErrorDescription - .authTokenError, - messageCode: .authTokenError) - completion(.failure(.internalError(description: ModelInfoRetriever.ErrorDescription - .authTokenError))) - return - } - } - } -} - -/// Extension with helper methods to handle fetching model info from server. -extension ModelInfoRetriever { - /// HTTP request headers. - private static let fisTokenHTTPHeader = "x-goog-firebase-installations-auth" - - private static let hashMatchHTTPHeader = "if-none-match" - - private static let bundleIDHTTPHeader = "x-ios-bundle-identifier" - - /// HTTP response headers. - private static let etagHTTPHeader = "Etag" - - /// 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" - components.queryItems = [URLQueryItem(name: "key", value: apiKey)] - return components.url - } - - /// Construct model fetch URL request. - func getModelInfoFetchURLRequest(token: String) -> URLRequest? { - guard let fetchURL = modelInfoFetchURL else { return nil } - var request = URLRequest(url: fetchURL) - request.httpMethod = "GET" - let bundleID = Bundle.main.bundleIdentifier ?? "" - request.setValue(bundleID, forHTTPHeaderField: ModelInfoRetriever.bundleIDHTTPHeader) - request.setValue(token, forHTTPHeaderField: ModelInfoRetriever.fisTokenHTTPHeader) - // Get model hash if local model info is available on device. - if let localModelInfo { - request.setValue( - localModelInfo.modelHash, - forHTTPHeaderField: ModelInfoRetriever.hashMatchHTTPHeader - ) - } - return request - } - - /// Parse error message from server response. - private func getErrorFromResponse(_ data: Data?) -> String? { - if let data, - let responseJSON = try? JSONSerialization - .jsonObject(with: data, options: []) as? [String: Any], - let error = responseJSON["error"] as? [String: Any], - let errorMessage = error["message"] as? String { - return errorMessage - } - return nil - } - - /// Parse date from string - used to get download URL expiry time. - private static func getDateFromString(_ strDate: String) -> Date? { - let dateFormatter = ISO8601DateFormatter() - dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) - dateFormatter.formatOptions = [.withFractionalSeconds] - return dateFormatter.date(from: strDate) - } - - /// Return model info created from server response. - private func getRemoteModelInfoFromResponse(_ data: Data, - modelHash: String) throws -> RemoteModelInfo { - guard let modelInfoJSON = try? JSONDecoder().decode(ModelInfoResponse.self, from: data) else { - throw DownloadError - .internalError(description: ModelInfoRetriever.ErrorDescription.decodeModelInfoResponse) - } - guard let downloadURL = URL(string: modelInfoJSON.downloadURL) else { - throw DownloadError - .internalError(description: ModelInfoRetriever.ErrorDescription - .invalidModelDownloadURL) - } - guard let size = Int(modelInfoJSON.size) else { - throw DownloadError - .internalError(description: ModelInfoRetriever.ErrorDescription - .invalidModelSize) - } - guard let expiryTime = ModelInfoRetriever.getDateFromString(modelInfoJSON.urlExpiryTime) else { - throw DownloadError - .internalError(description: ModelInfoRetriever.ErrorDescription - .invalidModelDownloadURLExpiryTime) - } - return RemoteModelInfo( - name: modelName, - downloadURL: downloadURL, - modelHash: modelHash, - size: size, - urlExpiryTime: expiryTime - ) - } -} - -/// Model info retrieval error codes. -enum ModelInfoErrorCode { - case noError - case noHash - case httpError(code: Int) - case connectionFailed - case hashMismatch - case unknown -} - -/// Possible error messages for model info retrieval. -extension ModelInfoRetriever { - /// Debug descriptions. - private enum DebugDescription { - static let receivedAuthToken = "Generated valid auth token." - static let receivedServerResponse = "Received a valid response from model info server." - static let modelInfoDownloaded = "Successfully downloaded model info." - static let modelInfoUnmodified = "Local model info matches the latest on server." - } - - /// Error descriptions. - private enum ErrorDescription { - static let authTokenError = "Error retrieving auth token." - static let invalidModelInfoFetchURL = "Unable to create URL to fetch model info." - static let invalidHTTPResponse = - "Could not get a valid HTTP response for model info retrieval." - static let serverResponseError = { (errorCode: Int) in - "Server returned with HTTP error code: \(errorCode)." - } - - static let missingModelHash = "Model hash missing in model info server response." - static let modelHashMismatch = "Unexpected model hash value." - static let unexpectedModelInfoDeletion = "Model info was deleted unexpectedly." - static let modelNotFound = { (name: String) in - "No model found with name: \(name)" - } - - static let invalidArgument = { (name: String) in - "Invalid argument for model name: \(name)" - } - - static let permissionDenied = "Invalid or missing permissions to retrieve model info." - static let resourceExhausted = "Resource exhausted due to too many requests." - static let modelInfoRetrievalFailed = { (code: Int) in - "Model info retrieval failed with HTTP error code: \(code)" - } - - static let decodeModelInfoResponse = - "Unable to decode model info response from server." - static let invalidModelDownloadURL = - "Invalid model download URL from server." - static let invalidModelSize = "Invalid model size from server." - static let invalidModelDownloadURLExpiryTime = - "Invalid download URL expiry time from server." - static let invalidModelInfoJSON = { (error: String) in - "Failed to parse model info: \(error)" - } - - static let failedModelInfoRetrieval = { (error: String) in - "Failed to retrieve model info: \(error)" - } - } -} diff --git a/FirebaseMLModelDownloader/Sources/RemoteModelInfo.swift b/FirebaseMLModelDownloader/Sources/RemoteModelInfo.swift deleted file mode 100644 index 1b7b921ca27..00000000000 --- a/FirebaseMLModelDownloader/Sources/RemoteModelInfo.swift +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import Foundation - -/// Model info object with details about not-yet downloaded model. -struct RemoteModelInfo { - /// Model name. - let name: String - - /// Download URL for the model file, as returned by server. - let downloadURL: URL - - /// Hash of the model, as returned by server. - let modelHash: String - - /// Size of the model, as returned by server. - let size: Int - - /// Model download URL expiry time, as returned by server. - let urlExpiryTime: Date - - init(name: String, downloadURL: URL, modelHash: String, size: Int, urlExpiryTime: Date) { - self.name = name - self.downloadURL = downloadURL - self.modelHash = modelHash - self.size = size - self.urlExpiryTime = urlExpiryTime - } -} diff --git a/FirebaseMLModelDownloader/Tests/Integration/ModelDownloaderIntegrationTests.swift b/FirebaseMLModelDownloader/Tests/Integration/ModelDownloaderIntegrationTests.swift deleted file mode 100644 index 7bc09af0e1a..00000000000 --- a/FirebaseMLModelDownloader/Tests/Integration/ModelDownloaderIntegrationTests.swift +++ /dev/null @@ -1,604 +0,0 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Skip keychain tests on Catalyst and macOS. Tests are skipped because they -// involve interactions with the keychain that require a provisioning profile. -// This is because MLModelDownloader depends on Installations. -// See go/firebase-macos-keychain-popups for more details. -#if !targetEnvironment(macCatalyst) && !os(macOS) - - @testable import FirebaseCore - @testable import FirebaseInstallations - @testable import FirebaseMLModelDownloader - import XCTest - #if SWIFT_PACKAGE - internal import GoogleUtilities_UserDefaults - #else - internal import GoogleUtilities - #endif // SWIFT_PACKAGE - - extension GULUserDefaults { - /// Returns an instance of user defaults. - static func createTestInstance(testName: String) -> GULUserDefaults { - let suiteName = "com.google.firebase.ml.test.\(testName)" - // Clear the suite (`UserDefaults` and `GULUserDefaults` map to the same - // storage space and `GULUserDefaults` doesn't offer API to do this.) - UserDefaults(suiteName: suiteName)!.removePersistentDomain(forName: suiteName) - return GULUserDefaults(suiteName: suiteName) - } - - /// Returns the existing user defaults instance. - static func getTestInstance(testName: String) -> GULUserDefaults { - let suiteName = "com.google.firebase.ml.test.\(testName)" - return GULUserDefaults(suiteName: suiteName) - } - } - - final class ModelDownloaderIntegrationTests: XCTestCase { - override class func setUp() { - super.setUp() - // TODO: Use FirebaseApp internal for test app. - let bundle = Bundle(for: self) - if let plistPath = bundle.path(forResource: "GoogleService-Info", ofType: "plist"), - let options = FirebaseOptions(contentsOfFile: plistPath) { - FirebaseApp.configure(options: options) - } else { - XCTFail("Could not locate GoogleService-Info.plist.") - } - FirebaseConfiguration.shared.setLoggerLevel(.debug) - } - - override func setUp() { - do { - try ModelFileManager.emptyModelsDirectory() - } catch { - XCTFail("Could not empty models directory.") - } - } - - /// Test to download model info - makes an actual network call. - func testDownloadModelInfo() { - guard let testApp = FirebaseApp.app() else { - XCTFail("Default app was not configured.") - return - } - testApp.isDataCollectionDefaultEnabled = false - - let testName = String(#function.dropLast(2)) - let testModelName = "pose-detection" - - let modelInfoRetriever = ModelInfoRetriever( - modelName: testModelName, - projectID: testApp.options.projectID!, - apiKey: testApp.options.apiKey!, - appName: testApp.name, installations: Installations.installations(app: testApp) - ) - - let modelInfoDownloadExpectation = - expectation(description: "Wait for model info to download.") - modelInfoRetriever.downloadModelInfo(completion: { result in - switch result { - case let .success(modelInfoResult): - switch modelInfoResult { - case let .modelInfo(modelInfo): - XCTAssertNotNil(modelInfo.urlExpiryTime) - XCTAssertGreaterThan(modelInfo.downloadURL.absoluteString.count, 0) - XCTAssertGreaterThan(modelInfo.modelHash.count, 0) - XCTAssertGreaterThan(modelInfo.size, 0) - let localModelInfo = LocalModelInfo(from: modelInfo) - localModelInfo.writeToDefaults( - .createTestInstance(testName: testName), - appName: testApp.name - ) - case .notModified: - XCTFail("Failed to retrieve model info.") - } - case let .failure(error): - XCTAssertNotNil(error) - XCTFail("Failed to retrieve model info - \(error)") - } - modelInfoDownloadExpectation.fulfill() - }) - - wait(for: [modelInfoDownloadExpectation], timeout: 5) - - if let localInfo = LocalModelInfo( - fromDefaults: .getTestInstance(testName: testName), - name: testModelName, - appName: testApp.name - ) { - XCTAssertNotNil(localInfo) - testRetrieveModelInfo(localInfo: localInfo) - } else { - XCTFail("Could not save model info locally.") - } - } - - func testRetrieveModelInfo(localInfo: LocalModelInfo) { - guard let testApp = FirebaseApp.app() else { - XCTFail("Default app was not configured.") - return - } - let testModelName = "pose-detection" - - let modelInfoRetriever = ModelInfoRetriever( - modelName: testModelName, - projectID: testApp.options.projectID!, - apiKey: testApp.options.apiKey!, - appName: testApp.name, installations: Installations.installations(app: testApp), - localModelInfo: localInfo - ) - - let modelInfoRetrieveExpectation = - expectation(description: "Wait for model info to be retrieved.") - modelInfoRetriever.downloadModelInfo(completion: { result in - switch result { - case let .success(modelInfoResult): - switch modelInfoResult { - case .modelInfo: - XCTFail("Local model info is already the latest and should not be set again.") - case .notModified: break - } - case let .failure(error): - XCTAssertNotNil(error) - XCTFail("Failed to retrieve model info - \(error)") - } - modelInfoRetrieveExpectation.fulfill() - }) - - wait(for: [modelInfoRetrieveExpectation], timeout: 5) - } - - /// Disabled since this test has been flaky in CI. - /// From https://github.com/firebase/firebase-ios-sdk/actions/runs/9944896811/job/27471928997 - /// ModelDownloaderIntegrationTests.swift:190: XCTAssertGreaterThanOrEqual failed: ("-3106.0") - /// is less than ("0.0") - /// ModelDownloaderIntegrationTests.swift:190: XCTAssertGreaterThanOrEqual failed: ("-4902.0") - /// is less than ("0.0") - /// ModelDownloaderIntegrationTests.swift:204: error: - /// failed - Error: internalError(description: "Model download failed with HTTP error code: - /// 500") - /// - /// Test to download model file - makes an actual network call. - func SKIPtestModelDownload() throws { - guard let testApp = FirebaseApp.app() else { - XCTFail("Default app was not configured.") - return - } - testApp.isDataCollectionDefaultEnabled = false - - let testName = String(#function.dropLast(2)) - let testModelName = "\(testName)-test-model" - let urlString = - "https://tfhub.dev/tensorflow/lite-model/ssd_mobilenet_v1/1/metadata/1?lite-format=tflite" - let url = URL(string: urlString)! - - let remoteModelInfo = RemoteModelInfo( - name: testModelName, - downloadURL: url, - modelHash: "mock-valid-hash", - size: 10, - urlExpiryTime: Date() - ) - - let conditions = ModelDownloadConditions() - let downloadExpectation = expectation(description: "Wait for model to download.") - let downloader = ModelFileDownloader(conditions: conditions) - let taskProgressHandler: ModelDownloadTask.ProgressHandler = { progress in - XCTAssertLessThanOrEqual(progress, 1) - XCTAssertGreaterThanOrEqual(progress, 0) - } - let taskCompletion: ModelDownloadTask.Completion = { result in - switch result { - case let .success(model): - let modelURL = URL(fileURLWithPath: model.path) - XCTAssertTrue(ModelFileManager.isFileReachable(at: modelURL)) - // Remove downloaded model file. - do { - try ModelFileManager.removeFile(at: modelURL) - } catch { - XCTFail("Model removal failed - \(error)") - } - case let .failure(error): - XCTFail("Error: \(error)") - } - downloadExpectation.fulfill() - } - let modelDownloadManager = ModelDownloadTask( - remoteModelInfo: remoteModelInfo, - appName: testApp.name, - defaults: .createTestInstance(testName: testName), - downloader: downloader, - progressHandler: taskProgressHandler, - completion: taskCompletion - ) - - modelDownloadManager.resume() - wait(for: [downloadExpectation], timeout: 5) - XCTAssertEqual(modelDownloadManager.downloadStatus, .complete) - } - - func testGetModel() { - guard let testApp = FirebaseApp.app() else { - XCTFail("Default app was not configured.") - return - } - testApp.isDataCollectionDefaultEnabled = false - - let testName = String(#function.dropLast(2)) - let testModelName = "image-classification" - - let conditions = ModelDownloadConditions() - let modelDownloader = ModelDownloader.modelDownloaderWithDefaults( - .createTestInstance(testName: testName), - app: testApp - ) - - /// Test download type - latest model. - var downloadType: ModelDownloadType = .latestModel - let latestModelExpectation = expectation(description: "Get latest model.") - - modelDownloader.getModel( - name: testModelName, - downloadType: downloadType, - conditions: conditions, - progressHandler: { progress in - XCTAssertTrue(Thread.isMainThread, "Completion must be called on the main thread.") - - XCTAssertLessThanOrEqual(progress, 1) - XCTAssertGreaterThanOrEqual(progress, 0) - } - ) { result in - XCTAssertTrue(Thread.isMainThread, "Completion must be called on the main thread.") - - switch result { - case let .success(model): - XCTAssertNotNil(model.path) - let modelURL = URL(fileURLWithPath: model.path) - XCTAssertTrue(ModelFileManager.isFileReachable(at: modelURL)) - case let .failure(error): - XCTFail("Failed to download model - \(error)") - } - latestModelExpectation.fulfill() - } - - wait(for: [latestModelExpectation], timeout: 5) - - /// Test download type - local model update in background. - downloadType = .localModelUpdateInBackground - let backgroundModelExpectation = - expectation(description: "Get local model and update in background.") - - modelDownloader.getModel( - name: testModelName, - downloadType: downloadType, - conditions: conditions, - progressHandler: { progress in - XCTFail("Model is already available on device.") - } - ) { result in - switch result { - case let .success(model): - XCTAssertNotNil(model.path) - let modelURL = URL(fileURLWithPath: model.path) - XCTAssertTrue(ModelFileManager.isFileReachable(at: modelURL)) - case let .failure(error): - XCTFail("Failed to download model - \(error)") - } - backgroundModelExpectation.fulfill() - } - wait(for: [backgroundModelExpectation], timeout: 5) - - /// Test download type - local model. - downloadType = .localModel - let localModelExpectation = expectation(description: "Get local model.") - - modelDownloader.getModel( - name: testModelName, - downloadType: downloadType, - conditions: conditions, - progressHandler: { progress in - XCTFail("Model is already available on device.") - } - ) { result in - switch result { - case let .success(model): - XCTAssertNotNil(model.path) - let modelURL = URL(fileURLWithPath: model.path) - XCTAssertTrue(ModelFileManager.isFileReachable(at: modelURL)) - // Remove downloaded model file. - do { - try ModelFileManager.removeFile(at: modelURL) - } catch { - XCTFail("Model removal failed - \(error)") - } - case let .failure(error): - XCTFail("Failed to download model - \(error)") - } - localModelExpectation.fulfill() - } - wait(for: [localModelExpectation], timeout: 5) - } - - func testGetModelWhenNameIsEmpty() { - guard let testApp = FirebaseApp.app() else { - XCTFail("Default app was not configured.") - return - } - testApp.isDataCollectionDefaultEnabled = false - - let testName = String(#function.dropLast(2)) - let emptyModelName = "" - - let conditions = ModelDownloadConditions() - let modelDownloader = ModelDownloader.modelDownloaderWithDefaults( - .createTestInstance(testName: testName), - app: testApp - ) - - let completionExpectation = expectation(description: "getModel") - let progressExpectation = expectation(description: "progressHandler") - progressExpectation.isInverted = true - - modelDownloader.getModel( - name: emptyModelName, - downloadType: .latestModel, - conditions: conditions, - progressHandler: { progress in - progressExpectation.fulfill() - } - ) { result in - XCTAssertTrue(Thread.isMainThread, "Completion must be called on the main thread.") - - switch result { - case .failure(.emptyModelName): - // The expected error. - break - - default: - XCTFail("Unexpected result: \(result)") - } - completionExpectation.fulfill() - } - - wait(for: [completionExpectation, progressExpectation], timeout: 5) - } - - /// Delete previously downloaded model. - func testDeleteModel() { - guard let testApp = FirebaseApp.app() else { - XCTFail("Default app was not configured.") - return - } - testApp.isDataCollectionDefaultEnabled = false - - let testName = String(#function.dropLast(2)) - let testModelName = "pose-detection" - - let conditions = ModelDownloadConditions() - let modelDownloader = ModelDownloader.modelDownloaderWithDefaults( - .createTestInstance(testName: testName), - app: testApp - ) - - let downloadType: ModelDownloadType = .latestModel - let latestModelExpectation = expectation(description: "Get latest model for deletion.") - - modelDownloader.getModel( - name: testModelName, - downloadType: downloadType, - conditions: conditions, - progressHandler: { progress in - XCTAssertTrue(Thread.isMainThread, "Completion must be called on the main thread.") - XCTAssertLessThanOrEqual(progress, 1) - XCTAssertGreaterThanOrEqual(progress, 0) - } - ) { result in - XCTAssertTrue(Thread.isMainThread, "Completion must be called on the main thread.") - - switch result { - case let .success(model): - XCTAssertNotNil(model.path) - let filePath = URL(fileURLWithPath: model.path) - XCTAssertTrue(ModelFileManager.isFileReachable(at: filePath)) - case let .failure(error): - XCTFail("Failed to download model - \(error)") - } - latestModelExpectation.fulfill() - } - - wait(for: [latestModelExpectation], timeout: 5) - - let deleteExpectation = expectation(description: "Wait for model deletion.") - modelDownloader.deleteDownloadedModel(name: testModelName) { result in - deleteExpectation.fulfill() - XCTAssertTrue(Thread.isMainThread, "Completion must be called on the main thread.") - - switch result { - case .success: break - case let .failure(error): - XCTFail("Failed to delete model - \(error)") - } - } - - wait(for: [deleteExpectation], timeout: 5) - } - - /// Test listing models in model directory. - func testListModels() { - guard let testApp = FirebaseApp.app() else { - XCTFail("Default app was not configured.") - return - } - testApp.isDataCollectionDefaultEnabled = false - - let testName = String(#function.dropLast(2)) - let testModelName = "pose-detection" - - let conditions = ModelDownloadConditions() - let modelDownloader = ModelDownloader.modelDownloaderWithDefaults( - .createTestInstance(testName: testName), - app: testApp - ) - - let downloadType: ModelDownloadType = .latestModel - let latestModelExpectation = expectation(description: "Get latest model.") - - modelDownloader.getModel( - name: testModelName, - downloadType: downloadType, - conditions: conditions, - progressHandler: { progress in - XCTAssertLessThanOrEqual(progress, 1) - XCTAssertGreaterThanOrEqual(progress, 0) - } - ) { result in - switch result { - case let .success(model): - XCTAssertNotNil(model.path) - let filePath = URL(fileURLWithPath: model.path) - XCTAssertTrue(ModelFileManager.isFileReachable(at: filePath)) - case let .failure(error): - XCTFail("Failed to download model - \(error)") - } - latestModelExpectation.fulfill() - } - - wait(for: [latestModelExpectation], timeout: 5) - - let listExpectation = expectation(description: "Wait for list models.") - modelDownloader.listDownloadedModels { result in - listExpectation.fulfill() - switch result { - case let .success(models): - XCTAssertGreaterThan(models.count, 0) - case let .failure(error): - XCTFail("Failed to list models - \(error)") - } - } - - wait(for: [listExpectation], timeout: 5) - } - - /// Test logging telemetry event. - func testLogTelemetryEvent() { - guard let testApp = FirebaseApp.app() else { - XCTFail("Default app was not configured.") - return - } - // Flip this to `true` to test logging. - testApp.isDataCollectionDefaultEnabled = false - - let testModelName = "digit-classification" - let testName = String(#function.dropLast(2)) - - let conditions = ModelDownloadConditions() - let modelDownloader = ModelDownloader.modelDownloaderWithDefaults( - .createTestInstance(testName: testName), - app: testApp - ) - - let latestModelExpectation = expectation(description: "Test get model telemetry.") - - modelDownloader.getModel( - name: testModelName, - downloadType: .latestModel, - conditions: conditions - ) { result in - switch result { - case .success: break - case let .failure(error): - XCTFail("Failed to download model - \(error)") - } - latestModelExpectation.fulfill() - } - wait(for: [latestModelExpectation], timeout: 5) - - let deleteModelExpectation = expectation(description: "Test delete model telemetry.") - modelDownloader.deleteDownloadedModel(name: testModelName) { result in - switch result { - case .success(()): break - case let .failure(error): - XCTFail("Failed to delete model - \(error)") - } - deleteModelExpectation.fulfill() - } - - wait(for: [deleteModelExpectation], timeout: 5) - - let notFoundModelName = "fakeModelName" - let notFoundModelExpectation = - expectation(description: "Test get model telemetry with unknown model.") - - modelDownloader.getModel( - name: notFoundModelName, - downloadType: .latestModel, - conditions: conditions - ) { result in - switch result { - case .success: XCTFail("Unexpected model success.") - case let .failure(error): - XCTAssertEqual(error, .notFound) - } - notFoundModelExpectation.fulfill() - } - wait(for: [notFoundModelExpectation], timeout: 5) - } - - func testGetModelWithConditions() { - guard let testApp = FirebaseApp.app() else { - XCTFail("Default app was not configured.") - return - } - testApp.isDataCollectionDefaultEnabled = false - - let testModelName = "pose-detection" - let testName = String(#function.dropLast(2)) - - let conditions = ModelDownloadConditions(allowsCellularAccess: false) - - let modelDownloader = ModelDownloader.modelDownloaderWithDefaults( - .createTestInstance(testName: testName), - app: testApp - ) - - let latestModelExpectation = expectation(description: "Get latest model with conditions.") - - modelDownloader.getModel( - name: testModelName, - downloadType: .latestModel, - conditions: conditions, - progressHandler: { progress in - XCTAssertTrue(Thread.isMainThread, "Completion must be called on the main thread.") - XCTAssertLessThanOrEqual(progress, 1) - XCTAssertGreaterThanOrEqual(progress, 0) - } - ) { result in - XCTAssertTrue(Thread.isMainThread, "Completion must be called on the main thread.") - - switch result { - case let .success(model): - XCTAssertNotNil(model.path) - let filePath = URL(fileURLWithPath: model.path) - XCTAssertTrue(ModelFileManager.isFileReachable(at: filePath)) - case let .failure(error): - XCTFail("Failed to download model - \(error)") - } - latestModelExpectation.fulfill() - } - wait(for: [latestModelExpectation], timeout: 5) - } - } - -#endif // !targetEnvironment(macCatalyst) && !os(macOS) diff --git a/FirebaseMLModelDownloader/Tests/Unit/ModelDownloaderUnitTests.swift b/FirebaseMLModelDownloader/Tests/Unit/ModelDownloaderUnitTests.swift deleted file mode 100644 index 96e9f75cc05..00000000000 --- a/FirebaseMLModelDownloader/Tests/Unit/ModelDownloaderUnitTests.swift +++ /dev/null @@ -1,1411 +0,0 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Skip keychain tests on Catalyst and macOS. Tests are skipped because they -// involve interactions with the keychain that require a provisioning profile. -// This is because MLModelDownloader depends on Installations. -// See go/firebase-macos-keychain-popups for more details. -#if !targetEnvironment(macCatalyst) && !os(macOS) - - @testable import FirebaseCore - @testable import FirebaseInstallations - @testable import FirebaseMLModelDownloader - import XCTest - #if SWIFT_PACKAGE - internal import GoogleUtilities_UserDefaults - #else - internal import GoogleUtilities - #endif // SWIFT_PACKAGE - - /// Mock options to configure default Firebase app. - private enum MockOptions { - static let appID = "1:123:ios:123abc" - static let gcmSenderID = "mock-sender-id" - static let projectID = "mock-project-id" - static let apiKey = "ABcdEf-APIKeyWithValidFormat_0123456789" - } - - // TODO: Create separate files for each class tested. - final class ModelDownloaderUnitTests: XCTestCase { - let fakeModelName = "fakeModelName" - let fakeModelPath = "fakeModelPath" - let fakeModelHash = "fakeModelHash" - let fakeDownloadURL = URL(string: "www.fake-download-url.com")! - let fakeFileURL = URL(string: "www.fake-model-file.com")! - let fakeExpiryTime = "2021-01-20T04:20:10.220Z" - let fakeModelSize = 20 - let fakeProjectID = "fakeProjectID" - let fakeAPIKey = "fakeAPIKey" - let fakeAppName = "fakeAppName" - var fakeModelJSON: String { - """ - { - "downloadUri":"\(fakeDownloadURL)", - "expireTime":"\(fakeExpiryTime)", - "sizeBytes":"\(fakeModelSize)" - } - """ - } - - let successAuthTokenProvider = - { (completion: @escaping (Result) -> Void) in - completion(.success("fakeFISToken")) - } - - override class func setUp() { - let options = FirebaseOptions( - googleAppID: MockOptions.appID, - gcmSenderID: MockOptions.gcmSenderID - ) - options.apiKey = MockOptions.apiKey - options.projectID = MockOptions.projectID - FirebaseApp.configure(options: options) - FirebaseConfiguration.shared.setLoggerLevel(.debug) - } - - override func setUp() { - do { - try ModelFileManager.emptyModelsDirectory() - } catch { - XCTFail("Could not empty models directory.") - } - } - - override class func tearDown() { - try? FileManager.default.removeItem(atPath: NSTemporaryDirectory()) - FirebaseApp.app()?.delete { _ in } - } - - /// Test if the same downloader instance is returned for an app. - func testModelDownloaderInit() { - guard let testApp = FirebaseApp.app() else { - XCTFail("Default app was not configured.") - return - } - testApp.isDataCollectionDefaultEnabled = false - - let modelDownloader1 = ModelDownloader.modelDownloader(app: testApp) - let modelDownloader2 = ModelDownloader.modelDownloader() - XCTAssert(modelDownloader1 === modelDownloader2) - } - - /// Test if app deletion removes associated downloader instance. - func testAppDeletion() { - guard let testApp = FirebaseApp.app() else { - XCTFail("Default app was not configured.") - return - } - testApp.isDataCollectionDefaultEnabled = false - - let modelDownloader1 = ModelDownloader.modelDownloader() - testApp.delete { success in - XCTAssertTrue(success) - } - ModelDownloaderUnitTests.setUp() - let modelDownloader2 = ModelDownloader.modelDownloader() - XCTAssert(modelDownloader1 !== modelDownloader2) - } - - /// Test invalid model file deletion. - func testDeleteInvalidModel() { - guard let testApp = FirebaseApp.app() else { - XCTFail("Default app was not configured.") - return - } - testApp.isDataCollectionDefaultEnabled = false - - let modelDownloader = ModelDownloader.modelDownloader() - modelDownloader.deleteDownloadedModel(name: fakeModelName) { result in - switch result { - case .success: XCTFail("Unexpected successful model deletion.") - case let .failure(error): - XCTAssertNotNil(error) - } - } - } - - func testGetModelNameFromFilePath() { - var fakeURL = URL(fileURLWithPath: "modelDirectory/@@appName@@\(fakeModelName).tflite") - XCTAssertEqual(ModelFileManager.getModelNameFromFilePath(fakeURL), fakeModelName) - fakeURL = URL(fileURLWithPath: "modelDirectory/--appName--\(fakeModelName).tflite") - XCTAssertNil(ModelFileManager.getModelNameFromFilePath(fakeURL)) - } - - /// Test listing models. - func testListModels() { - guard let testApp = FirebaseApp.app() else { - XCTFail("Default app was not configured.") - return - } - testApp.isDataCollectionDefaultEnabled = false - - let testName = String(#function.dropLast(2)) - let modelDownloader = ModelDownloader.modelDownloaderWithDefaults( - .createUnitTestInstance(testName: testName), - app: testApp - ) - - let tempModelFileURL = tempModelFile() - let completionExpectation = expectation(description: "Completion handler") - - let localModelInfo = fakeLocalModelInfo() - localModelInfo.writeToDefaults( - .getUnitTestInstance(testName: testName), - appName: testApp.name - ) - - modelDownloader.listDownloadedModels { result in - completionExpectation.fulfill() - switch result { - case let .success(models): - /// Expected success since model info was written to user defaults. - XCTAssertEqual(models.first?.path, tempModelFileURL.path) - case let .failure(error): - XCTFail("Error - \(error)") - } - } - wait(for: [completionExpectation], timeout: 0.5) - try? ModelFileManager.removeFile(at: tempModelFileURL) - } - - /// Test invalid path in model directory. - func testListModelsInvalidPath() { - guard let testApp = FirebaseApp.app() else { - XCTFail("Default app was not configured.") - return - } - testApp.isDataCollectionDefaultEnabled = false - - let modelDownloader = ModelDownloader.modelDownloader() - let invalidTempModelFileURL = tempModelFile(invalid: true) - let completionExpectation = expectation(description: "Completion handler") - - modelDownloader.listDownloadedModels { result in - completionExpectation.fulfill() - switch result { - case .success: - XCTFail("Unexpected success of list models.") - case let .failure(error): - switch error { - case let .internalError(description): - XCTAssertTrue(description - .contains("List models failed due to unexpected model file name")) - default: XCTFail("Expected failure due to bad model name.") - } - } - } - wait(for: [completionExpectation], timeout: 0.5) - try? ModelFileManager.removeFile(at: invalidTempModelFileURL) - } - - /// Test listing models without local info in user defaults. - func testListModelsNoLocalInfo() { - guard let testApp = FirebaseApp.app() else { - XCTFail("Default app was not configured.") - return - } - testApp.isDataCollectionDefaultEnabled = false - - let testName = String(#function.dropLast(2)) - let tempModelFileURL = tempModelFile() - let modelDownloader = ModelDownloader.modelDownloaderWithDefaults( - .createUnitTestInstance(testName: testName), - app: testApp - ) - - let completionExpectation = expectation(description: "Completion handler") - - modelDownloader.listDownloadedModels { result in - completionExpectation.fulfill() - switch result { - case .success: - XCTFail("Unexpected success of list models.") - case let .failure(error): - switch error { - /// Expected failure since no model info was written to user defaults. - case let .internalError(description): - XCTAssertTrue(description.contains("List models failed due to no local model info")) - default: XCTFail("Expected failure due to bad model name.") - } - } - } - wait(for: [completionExpectation], timeout: 0.5) - try? ModelFileManager.removeFile(at: tempModelFileURL) - } - - /// Test how URL conversion behaves if there are spaces in the path. - func testURLConversion() { - let fakeURLWithSpace = URL(string: "file:///fakeDir1/fake%20Dir2/fakeFile")! - - XCTAssertEqual(fakeURLWithSpace, URL(string: fakeURLWithSpace.absoluteString)) - XCTAssertEqual(fakeURLWithSpace, URL(fileURLWithPath: fakeURLWithSpace.path)) - - /// Strings without spaces should work fine either way. - let fakeURLWithoutSpace = URL(string: "fakeDir1/fakeDir2/fakeFile")! - XCTAssertEqual(fakeURLWithoutSpace, URL(string: fakeURLWithoutSpace.absoluteString)!) - XCTAssertEqual(fakeURLWithoutSpace, URL(string: fakeURLWithoutSpace.path)!) - } - - /// Test on-device logging. - func testDeviceLogging() { - DeviceLogger.logEvent(level: .error, - message: "This is a test error logged to device.", - messageCode: .testError) - } - - /// Get model info if server returns a new model info. - func testGetModelInfoWith200() { - let session = fakeModelInfoSessionWithURL(fakeDownloadURL, statusCode: 200) - let modelInfoRetriever = fakeModelRetriever(fakeSession: session) - let completionExpectation = expectation(description: "Completion handler") - - modelInfoRetriever.downloadModelInfo { result in - completionExpectation.fulfill() - switch result { - case let .success(fakeModelInfoResult): - switch fakeModelInfoResult { - case let .modelInfo(remoteModelInfo): - XCTAssertEqual(remoteModelInfo.name, self.fakeModelName) - XCTAssertEqual(remoteModelInfo.downloadURL, self.fakeDownloadURL) - XCTAssertEqual(remoteModelInfo.size, self.fakeModelSize) - XCTAssertEqual(remoteModelInfo.modelHash, self.fakeModelHash) - case .notModified: XCTFail("Expected new model info from server.") - } - case let .failure(error): XCTFail("Failed with error - \(error)") - } - } - wait(for: [completionExpectation], timeout: 0.5) - } - - /// Get model info if model info is not modified. - func testGetModelInfoWith304() { - let session = fakeModelInfoSessionWithURL(fakeDownloadURL, statusCode: 304) - let localModelInfo = fakeLocalModelInfo(mismatch: false) - let modelInfoRetriever = fakeModelRetriever( - fakeSession: session, - fakeLocalModelInfo: localModelInfo - ) - let completionExpectation = expectation(description: "Completion handler") - - modelInfoRetriever.downloadModelInfo { result in - completionExpectation.fulfill() - switch result { - case let .success(fakeModelInfoResult): - switch fakeModelInfoResult { - case .modelInfo: XCTFail("Unexpected new model info from server.") - case .notModified: break - } - case let .failure(error): XCTFail("Failed with error - \(error)") - } - } - wait(for: [completionExpectation], timeout: 0.5) - } - - /// Get model info if model info is not modified but local model info is not set. - func testGetModelInfoWith304MissingLocalInfo() { - let session = fakeModelInfoSessionWithURL(fakeDownloadURL, statusCode: 304) - let modelInfoRetriever = fakeModelRetriever(fakeSession: session) - let completionExpectation = expectation(description: "Completion handler") - - modelInfoRetriever.downloadModelInfo { result in - completionExpectation.fulfill() - switch result { - case let .success(fakeModelInfoResult): - switch fakeModelInfoResult { - case .modelInfo: XCTFail("Unexpected new model info from server.") - case .notModified: XCTFail("Expected failure since local model info was not set.") - } - case let .failure(error): - switch error { - case let .internalError(description: errorMessage): XCTAssertEqual(errorMessage, - "Model info was deleted unexpectedly.") - default: XCTFail("Expected failure since local model info was not set.") - } - } - } - wait(for: [completionExpectation], timeout: 0.5) - } - - /// Get model info if model info is not modified but local model info is not set. - func testGetModelInfoWith304HashMismatch() { - let session = fakeModelInfoSessionWithURL(fakeDownloadURL, statusCode: 304) - let localModelInfo = fakeLocalModelInfo(mismatch: true) - let modelInfoRetriever = fakeModelRetriever( - fakeSession: session, - fakeLocalModelInfo: localModelInfo - ) - let completionExpectation = expectation(description: "Completion handler") - - modelInfoRetriever.downloadModelInfo { result in - completionExpectation.fulfill() - switch result { - case let .success(fakeModelInfoResult): - switch fakeModelInfoResult { - case .modelInfo: XCTFail("Unexpected new model info from server.") - case .notModified: XCTFail("Expected failure since model hash does not match.") - } - case let .failure(error): - switch error { - case let .internalError(description: errorMessage): XCTAssertEqual(errorMessage, - "Unexpected model hash value.") - default: XCTFail("Expected failure since local model info was not set.") - } - } - } - wait(for: [completionExpectation], timeout: 0.5) - } - - /// Get model info if model name is invalid. - func testGetModelInfoWith400() { - let session = fakeModelInfoSessionWithURL(fakeDownloadURL, statusCode: 400) - let localModelInfo = fakeLocalModelInfo(mismatch: false) - let modelInfoRetriever = fakeModelRetriever( - fakeSession: session, - fakeLocalModelInfo: localModelInfo - ) - let completionExpectation = expectation(description: "Completion handler") - - modelInfoRetriever.downloadModelInfo { result in - completionExpectation.fulfill() - switch result { - case let .success(fakeModelInfoResult): - switch fakeModelInfoResult { - case .modelInfo: XCTFail("Unexpected new model info from server.") - case .notModified: XCTFail("Expected failure since model name is invalid.") - } - case let .failure(error): - XCTAssertEqual(error, .invalidArgument) - } - } - wait(for: [completionExpectation], timeout: 0.5) - } - - /// Get model info if model name does not exist. - func testGetModelInfoWith404() { - let session = fakeModelInfoSessionWithURL(fakeDownloadURL, statusCode: 404) - let localModelInfo = fakeLocalModelInfo(mismatch: false) - let modelInfoRetriever = fakeModelRetriever( - fakeSession: session, - fakeLocalModelInfo: localModelInfo - ) - let completionExpectation = expectation(description: "Completion handler") - - modelInfoRetriever.downloadModelInfo { result in - completionExpectation.fulfill() - switch result { - case let .success(fakeModelInfoResult): - switch fakeModelInfoResult { - case .modelInfo: XCTFail("Unexpected new model info from server.") - case .notModified: XCTFail("Expected failure since model name is not available.") - } - case let .failure(error): - XCTAssertEqual(error, .notFound) - } - } - wait(for: [completionExpectation], timeout: 0.5) - } - - /// Get model info if resource exhausted due to too many requests. - func testGetModelInfoWith429() { - let session = fakeModelInfoSessionWithURL(fakeDownloadURL, statusCode: 429) - let localModelInfo = fakeLocalModelInfo(mismatch: false) - let modelInfoRetriever = fakeModelRetriever( - fakeSession: session, - fakeLocalModelInfo: localModelInfo - ) - let completionExpectation = expectation(description: "Completion handler") - - modelInfoRetriever.downloadModelInfo { result in - completionExpectation.fulfill() - switch result { - case let .success(fakeModelInfoResult): - switch fakeModelInfoResult { - case .modelInfo: XCTFail("Unexpected new model info from server.") - case .notModified: XCTFail("Expected failure since resource exhausted.") - } - case let .failure(error): - XCTAssertEqual(error, .resourceExhausted) - } - } - wait(for: [completionExpectation], timeout: 0.5) - } - - /// Get model if server returns a model file. - func testModelDownloadWith200() { - let tempFileURL = tempFile() - let remoteModelInfo = fakeModelInfo(expired: false, oversized: false) - let fakeResponse = HTTPURLResponse(url: fakeFileURL, - statusCode: 200, - httpVersion: nil, - headerFields: nil)! - let downloader = MockModelFileDownloader() - let fileDownloaderExpectation = expectation(description: "File downloader") - downloader.downloadFileHandler = { url in - fileDownloaderExpectation.fulfill() - XCTAssertEqual(url, self.fakeDownloadURL) - } - let progressExpectation = expectation(description: "Progress handler") - progressExpectation.expectedFulfillmentCount = 2 - - let completionExpectation = expectation(description: "Completion handler") - let taskProgressHandler: ModelDownloadTask.ProgressHandler = { - progress in - progressExpectation.fulfill() - XCTAssertEqual(progress, 0.4) - } - let taskCompletion: ModelDownloadTask.Completion = { result in - completionExpectation.fulfill() - switch result { - case let .success(model): - self.checkModel(model: model) - self.deleteFile(at: model.path) - case let .failure(error): - XCTFail("Error - \(error)") - } - } - let testName = String(#function.dropLast(2)) - let modelDownloadTask = ModelDownloadTask(remoteModelInfo: remoteModelInfo, - appName: "fakeAppName", - defaults: .createUnitTestInstance( - testName: testName - ), - downloader: downloader, - progressHandler: taskProgressHandler, - completion: taskCompletion) - - modelDownloadTask.resume() - wait(for: [fileDownloaderExpectation], timeout: 0.1) - - downloader.progressHandler?(100, 250) - downloader.progressHandler?(100, 250) - wait(for: [progressExpectation], timeout: 0.5) - - downloader - .completion?(.success(FileDownloaderResponse(urlResponse: fakeResponse, - fileURL: tempFileURL))) - wait(for: [completionExpectation], timeout: 0.5) - - try? ModelFileManager.removeFile(at: tempFileURL) - } - - /// Get model if server returns an error due to expired url. - func testModelDownloadWith400Expired() { - let tempFileURL = tempFile() - let remoteModelInfo = fakeModelInfo(expired: true, oversized: false) - let fakeResponse = HTTPURLResponse(url: fakeFileURL, - statusCode: 400, - httpVersion: nil, - headerFields: nil)! - let downloader = MockModelFileDownloader() - let fileDownloaderExpectation = expectation(description: "File downloader") - downloader.downloadFileHandler = { url in - fileDownloaderExpectation.fulfill() - XCTAssertEqual(url, self.fakeDownloadURL) - } - - let completionExpectation = expectation(description: "Completion handler") - let taskCompletion: ModelDownloadTask.Completion = { result in - completionExpectation.fulfill() - switch result { - case .success: XCTFail("Unexpected successful model download.") - case let .failure(error): - XCTAssertEqual(error, .expiredDownloadURL) - } - } - let testName = String(#function.dropLast(2)) - let modelDownloadTask = ModelDownloadTask(remoteModelInfo: remoteModelInfo, - appName: "fakeAppName", - defaults: .createUnitTestInstance( - testName: testName - ), - downloader: downloader, - completion: taskCompletion) - modelDownloadTask.resume() - wait(for: [fileDownloaderExpectation], timeout: 0.1) - - downloader - .completion?(.success(FileDownloaderResponse(urlResponse: fakeResponse, - fileURL: tempFileURL))) - wait(for: [completionExpectation], timeout: 0.5) - - try? ModelFileManager.removeFile(at: tempFileURL) - } - - /// Get model if server returns an error due to invalid model name. - func testModelDownloadWith400Invalid() { - let tempFileURL = tempFile() - let remoteModelInfo = fakeModelInfo(expired: false, oversized: false) - let fakeResponse = HTTPURLResponse(url: fakeFileURL, - statusCode: 400, - httpVersion: nil, - headerFields: nil)! - let downloader = MockModelFileDownloader() - let fileDownloaderExpectation = expectation(description: "File downloader") - downloader.downloadFileHandler = { url in - fileDownloaderExpectation.fulfill() - XCTAssertEqual(url, self.fakeDownloadURL) - } - - let completionExpectation = expectation(description: "Completion handler") - let taskCompletion: ModelDownloadTask.Completion = { result in - completionExpectation.fulfill() - switch result { - case .success: XCTFail("Unexpected successful model download.") - case let .failure(error): - XCTAssertEqual(error, .invalidArgument) - } - } - let testName = String(#function.dropLast(2)) - let modelDownloadTask = ModelDownloadTask(remoteModelInfo: remoteModelInfo, - appName: "fakeAppName", - defaults: .createUnitTestInstance( - testName: testName - ), - downloader: downloader, - completion: taskCompletion) - modelDownloadTask.resume() - wait(for: [fileDownloaderExpectation], timeout: 0.1) - - downloader - .completion?(.success(FileDownloaderResponse(urlResponse: fakeResponse, - fileURL: tempFileURL))) - wait(for: [completionExpectation], timeout: 0.5) - - try? ModelFileManager.removeFile(at: tempFileURL) - } - - /// Get model info if Firebase ML API is not enabled or permission denied. - func testGetModelInfoWith403() { - let errorMessage = - """ - { - "error": - { - "message":"API Disabled." - } - } - """ - let session = fakeModelInfoSessionWithURL( - fakeDownloadURL, - statusCode: 403, - customJSON: errorMessage - ) - let localModelInfo = fakeLocalModelInfo(mismatch: false) - let modelInfoRetriever = fakeModelRetriever( - fakeSession: session, - fakeLocalModelInfo: localModelInfo - ) - let completionExpectation = expectation(description: "Completion handler") - - modelInfoRetriever.downloadModelInfo { result in - completionExpectation.fulfill() - switch result { - case .success: - XCTFail("Unexpected new model info from server.") - case let .failure(error): - XCTAssertEqual(error, .permissionDenied) - } - } - wait(for: [completionExpectation], timeout: 0.5) - } - - /// Get model if multiple duplicate requests are made. - func testModelDownloadWithMergeRequests() { - let tempFileURL = tempFile() - let numberOfRequests = 5 - let remoteModelInfo = fakeModelInfo(expired: false, oversized: false) - let fakeResponse = HTTPURLResponse(url: fakeFileURL, - statusCode: 200, - httpVersion: nil, - headerFields: nil)! - let downloader = MockModelFileDownloader() - let fileDownloaderExpectation = expectation(description: "File downloader") - - downloader.downloadFileHandler = { url in - fileDownloaderExpectation.fulfill() - XCTAssertEqual(url, self.fakeDownloadURL) - } - - let completionExpectation = expectation(description: "Completion handler") - completionExpectation.expectedFulfillmentCount = numberOfRequests + 1 - - let taskCompletion: ModelDownloadTask.Completion = { result in - completionExpectation.fulfill() - switch result { - case let .success(model): - self.checkModel(model: model) - case let .failure(error): - XCTFail("Error - \(error)") - } - } - - let testName = String(#function.dropLast(2)) - let modelDownloadTask = ModelDownloadTask(remoteModelInfo: remoteModelInfo, - appName: fakeAppName, - defaults: .createUnitTestInstance( - testName: testName - ), - downloader: downloader, - completion: taskCompletion) - - for i in 1 ... numberOfRequests { - let newTaskCompletion: ModelDownloadTask.Completion = { result in - completionExpectation.fulfill() - switch result { - case let .success(model): - self.checkModel(model: model) - if i == numberOfRequests { - self.deleteFile(at: model.path) - } - case let .failure(error): - XCTFail("Error - \(error)") - } - } - modelDownloadTask.resume() - modelDownloadTask.merge(newCompletion: newTaskCompletion) - } - - downloader - .completion?(.success(FileDownloaderResponse(urlResponse: fakeResponse, - fileURL: tempFileURL))) - wait(for: [fileDownloaderExpectation], timeout: 1) - wait(for: [completionExpectation], timeout: 2) - - try? ModelFileManager.removeFile(at: tempFileURL) - } - - /// Get model if multiple duplicate requests are made (test at the API surface). - func testGetModelWithMergeRequests() { - guard let testApp = FirebaseApp.app() else { - XCTFail("Default app was not configured.") - return - } - testApp.isDataCollectionDefaultEnabled = false - - let modelDownloader = ModelDownloader.modelDownloader() - let conditions = ModelDownloadConditions() - let session = fakeModelInfoSessionWithURL(fakeDownloadURL, statusCode: 200) - let modelInfoRetriever = fakeModelRetriever(fakeSession: session) - let tempFileURL = tempFile() - let numberOfRequests = 6 - let fakeResponse = HTTPURLResponse(url: fakeFileURL, - statusCode: 200, - httpVersion: nil, - headerFields: nil)! - let downloader = MockModelFileDownloader() - let fileDownloaderExpectation = expectation(description: "File downloader") - - downloader.downloadFileHandler = { url in - fileDownloaderExpectation.fulfill() - XCTAssertEqual(url, self.fakeDownloadURL) - } - - let completionExpectation = expectation(description: "Completion handler") - completionExpectation.expectedFulfillmentCount = numberOfRequests - - for i in 1 ... numberOfRequests { - modelDownloader.downloadInfoAndModel(modelName: "fakeModelName", - modelInfoRetriever: modelInfoRetriever, - downloader: downloader, - conditions: conditions) { result in - completionExpectation.fulfill() - switch result { - case let .success(model): - self.checkModel(model: model) - if i == numberOfRequests { - self.deleteFile(at: model.path) - } - case let .failure(error): - XCTFail("Error - \(error)") - } - } - } - wait(for: [fileDownloaderExpectation], timeout: 0.5) - - downloader - .completion?(.success(FileDownloaderResponse(urlResponse: fakeResponse, - fileURL: tempFileURL))) - - wait(for: [completionExpectation], timeout: 1) - - try? ModelFileManager.removeFile(at: tempFileURL) - } - - /// Get model if server returns an error due to model not found. - func testModelDownloadWith404() { - let tempFileURL = tempFile() - let remoteModelInfo = fakeModelInfo(expired: false, oversized: false) - let fakeResponse = HTTPURLResponse(url: fakeFileURL, - statusCode: 404, - httpVersion: nil, - headerFields: nil)! - let downloader = MockModelFileDownloader() - let fileDownloaderExpectation = expectation(description: "File downloader") - downloader.downloadFileHandler = { url in - fileDownloaderExpectation.fulfill() - XCTAssertEqual(url, self.fakeDownloadURL) - } - - let completionExpectation = expectation(description: "Completion handler") - let taskCompletion: ModelDownloadTask.Completion = { result in - completionExpectation.fulfill() - switch result { - case .success: XCTFail("Unexpected successful model download.") - case let .failure(error): - XCTAssertEqual(error, .notFound) - } - } - let testName = String(#function.dropLast(2)) - let modelDownloadTask = ModelDownloadTask(remoteModelInfo: remoteModelInfo, - appName: "fakeAppName", - defaults: .createUnitTestInstance( - testName: testName - ), - downloader: downloader, - completion: taskCompletion) - modelDownloadTask.resume() - wait(for: [fileDownloaderExpectation], timeout: 0.1) - - downloader - .completion?(.success(FileDownloaderResponse(urlResponse: fakeResponse, - fileURL: tempFileURL))) - wait(for: [completionExpectation], timeout: 0.5) - - try? ModelFileManager.removeFile(at: tempFileURL) - } - - /// Get model if server returns an error due to model not found. - func testModelDownloadNetworkError() { - let tempFileURL = tempFile() - let remoteModelInfo = fakeModelInfo(expired: false, oversized: false) - let downloader = MockModelFileDownloader() - let fileDownloaderExpectation = expectation(description: "File downloader") - downloader.downloadFileHandler = { url in - fileDownloaderExpectation.fulfill() - XCTAssertEqual(url, self.fakeDownloadURL) - } - - let completionExpectation = expectation(description: "Completion handler") - let taskCompletion: ModelDownloadTask.Completion = { result in - completionExpectation.fulfill() - switch result { - case .success: XCTFail("Unexpected successful model download.") - case let .failure(error): - XCTAssertEqual(error, .failedPrecondition) - } - } - let testName = String(#function.dropLast(2)) - let modelDownloadTask = ModelDownloadTask(remoteModelInfo: remoteModelInfo, - appName: "fakeAppName", - defaults: .createUnitTestInstance( - testName: testName - ), - downloader: downloader, - completion: taskCompletion) - modelDownloadTask.resume() - wait(for: [fileDownloaderExpectation], timeout: 0.1) - - let offlineError = DownloadError.internalError(description: "Network appears to be offline.") - downloader - .completion?(.failure(FileDownloaderError.networkError(offlineError))) - wait(for: [completionExpectation], timeout: 0.5) - - try? ModelFileManager.removeFile(at: tempFileURL) - } - - /// Get model with a valid server response. - func testGetModelSuccessful() { - guard let testApp = FirebaseApp.app() else { - XCTFail("Default app was not configured.") - return - } - testApp.isDataCollectionDefaultEnabled = false - - let modelDownloader = ModelDownloader.modelDownloader() - let conditions = ModelDownloadConditions() - let session = fakeModelInfoSessionWithURL(fakeDownloadURL, statusCode: 200) - let modelInfoRetriever = fakeModelRetriever(fakeSession: session) - let downloader = MockModelFileDownloader() - let tempFileURL = tempFile() - - let fakeResponseNotFound = HTTPURLResponse(url: fakeFileURL, - statusCode: 200, - httpVersion: nil, - headerFields: nil)! - - let fileDownloaderExpectation = expectation(description: "File downloader") - let completionExpectation = expectation(description: "Completion handler") - - // Handles initial response. - downloader.downloadFileHandler = { url in - fileDownloaderExpectation.fulfill() - XCTAssertEqual(url, self.fakeDownloadURL) - } - - modelDownloader.downloadInfoAndModel(modelName: fakeModelName, - modelInfoRetriever: modelInfoRetriever, - downloader: downloader, - conditions: conditions) { result in - completionExpectation.fulfill() - switch result { - case let .success(model): - self.checkModel(model: model) - self.deleteFile(at: model.path) - case let .failure(error): - XCTFail("Error - \(error)") - } - } - wait(for: [fileDownloaderExpectation], timeout: 0.5) - - downloader - .completion?(.success(FileDownloaderResponse(urlResponse: fakeResponseNotFound, - fileURL: tempFileURL))) - wait(for: [completionExpectation], timeout: 0.5) - - try? ModelFileManager.removeFile(at: tempFileURL) - } - - /// Get model if download url expired but retry was successful. - func testGetModelSuccessfulRetry() { - guard let testApp = FirebaseApp.app() else { - XCTFail("Default app was not configured.") - return - } - testApp.isDataCollectionDefaultEnabled = false - - let modelDownloader = ModelDownloader.modelDownloader() - let conditions = ModelDownloadConditions() - let session = fakeModelInfoSessionWithURL(fakeDownloadURL, statusCode: 200) - let modelInfoRetriever = fakeModelRetriever(fakeSession: session) - let downloader = MockModelFileDownloader() - let tempFileURL = tempFile() - - let fakeResponseExpired = HTTPURLResponse(url: fakeFileURL, - statusCode: 400, - httpVersion: nil, - headerFields: nil)! - - let fakeResponseSucceeded = HTTPURLResponse(url: fakeFileURL, - statusCode: 200, - httpVersion: nil, - headerFields: nil)! - - let fileDownloaderExpectation1 = expectation(description: "File downloader before retry") - let completionExpectation = expectation(description: "Completion handler.") - - // Handles initial response. - downloader.downloadFileHandler = { url in - fileDownloaderExpectation1.fulfill() - XCTAssertEqual(url, self.fakeDownloadURL) - } - - modelDownloader.downloadInfoAndModel(modelName: fakeModelName, - modelInfoRetriever: modelInfoRetriever, - downloader: downloader, - conditions: conditions) { result in - completionExpectation.fulfill() - switch result { - // Retry successful - model returned. - case let .success(model): - self.checkModel(model: model) - self.deleteFile(at: model.path) - case let .failure(error): - XCTFail("Error - \(error)") - } - } - wait(for: [fileDownloaderExpectation1], timeout: 0.5) - - // Handles response after one retry. - let fileDownloaderExpectation2 = expectation(description: "File downloader after retry") - downloader.downloadFileHandler = { url in - fileDownloaderExpectation2.fulfill() - XCTAssertEqual(url, self.fakeDownloadURL) - } - downloader - .completion?(.success(FileDownloaderResponse(urlResponse: fakeResponseExpired, - fileURL: tempFileURL))) - wait(for: [fileDownloaderExpectation2], timeout: 0.5) - - downloader - .completion?(.success(FileDownloaderResponse(urlResponse: fakeResponseSucceeded, - fileURL: tempFileURL))) - wait(for: [completionExpectation], timeout: 0.5) - - try? ModelFileManager.removeFile(at: tempFileURL) - } - - /// Get model if model doesn't exist. - func testGetModelNotFound() { - guard let testApp = FirebaseApp.app() else { - XCTFail("Default app was not configured.") - return - } - testApp.isDataCollectionDefaultEnabled = false - - let modelDownloader = ModelDownloader.modelDownloader() - let conditions = ModelDownloadConditions() - let session = fakeModelInfoSessionWithURL(fakeDownloadURL, statusCode: 200) - let modelInfoRetriever = fakeModelRetriever(fakeSession: session) - let downloader = MockModelFileDownloader() - let tempFileURL = tempFile() - - let fakeResponseNotFound = HTTPURLResponse(url: fakeFileURL, - statusCode: 404, - httpVersion: nil, - headerFields: nil)! - - let fileDownloaderExpectation = expectation(description: "File downloader before retry") - let completionExpectation = expectation(description: "Completion handler") - - // Handles initial response. - downloader.downloadFileHandler = { url in - fileDownloaderExpectation.fulfill() - XCTAssertEqual(url, self.fakeDownloadURL) - } - - modelDownloader.downloadInfoAndModel(modelName: fakeModelName, - modelInfoRetriever: modelInfoRetriever, - downloader: downloader, - conditions: conditions) { result in - completionExpectation.fulfill() - switch result { - // Retry failed due to another expired url - error returned. - case .success: XCTFail("Unexpected successful model download.") - case let .failure(error): - switch error { - case .notFound: break - default: XCTFail("Expected failure due to model not found.") - } - } - } - wait(for: [fileDownloaderExpectation], timeout: 0.5) - - downloader - .completion?(.success(FileDownloaderResponse(urlResponse: fakeResponseNotFound, - fileURL: tempFileURL))) - wait(for: [completionExpectation], timeout: 0.5) - - try? ModelFileManager.removeFile(at: tempFileURL) - } - - /// Get model if invalid or insufficient permissions. - func testGetModelUnauthorized() { - guard let testApp = FirebaseApp.app() else { - XCTFail("Default app was not configured.") - return - } - testApp.isDataCollectionDefaultEnabled = false - - let modelDownloader = ModelDownloader.modelDownloader() - let conditions = ModelDownloadConditions() - let session = fakeModelInfoSessionWithURL(fakeDownloadURL, statusCode: 200) - let modelInfoRetriever = fakeModelRetriever(fakeSession: session) - let downloader = MockModelFileDownloader() - let tempFileURL = tempFile() - - let fakeResponseNotFound = HTTPURLResponse(url: fakeFileURL, - statusCode: 403, - httpVersion: nil, - headerFields: nil)! - - let fileDownloaderExpectation = expectation(description: "File downloader") - let completionExpectation = expectation(description: "Completion handler") - - // Handles initial response. - downloader.downloadFileHandler = { url in - fileDownloaderExpectation.fulfill() - XCTAssertEqual(url, self.fakeDownloadURL) - } - - modelDownloader.downloadInfoAndModel(modelName: fakeModelName, - modelInfoRetriever: modelInfoRetriever, - downloader: downloader, - conditions: conditions) { result in - completionExpectation.fulfill() - switch result { - case .success: XCTFail("Unexpected successful model download.") - case let .failure(error): - switch error { - case .permissionDenied: break - default: XCTFail("Expected failure due to bad permissions.") - } - } - } - wait(for: [fileDownloaderExpectation], timeout: 0.5) - - downloader - .completion?(.success(FileDownloaderResponse(urlResponse: fakeResponseNotFound, - fileURL: tempFileURL))) - wait(for: [completionExpectation], timeout: 0.5) - - try? ModelFileManager.removeFile(at: tempFileURL) - } - - /// Get model if download url expired and retry url also expired. - func testGetModelFailedRetry() { - guard let testApp = FirebaseApp.app() else { - XCTFail("Default app was not configured.") - return - } - testApp.isDataCollectionDefaultEnabled = false - - let modelDownloader = ModelDownloader.modelDownloader() - let conditions = ModelDownloadConditions() - let session = fakeModelInfoSessionWithURL(fakeDownloadURL, statusCode: 200) - let modelInfoRetriever = fakeModelRetriever(fakeSession: session) - let downloader = MockModelFileDownloader() - let tempFileURL = tempFile() - - let fakeResponseExpired = HTTPURLResponse(url: fakeFileURL, - statusCode: 400, - httpVersion: nil, - headerFields: nil)! - - let fileDownloaderExpectation1 = expectation(description: "File downloader before retry") - let completionExpectation = expectation(description: "Completion handler") - - // Handles initial response. - downloader.downloadFileHandler = { url in - fileDownloaderExpectation1.fulfill() - XCTAssertEqual(url, self.fakeDownloadURL) - } - - modelDownloader.downloadInfoAndModel(modelName: fakeModelName, - modelInfoRetriever: modelInfoRetriever, - downloader: downloader, - conditions: conditions) { result in - completionExpectation.fulfill() - switch result { - // Retry failed due to another expired url - error returned. - case .success: XCTFail("Unexpected successful model download.") - case let .failure(error): - switch error { - case let .internalError(description: errorMessage): XCTAssertEqual(errorMessage, - "Unable to update expired model info.") - default: XCTFail("Expected failure due to expired model info.") - } - } - } - wait(for: [fileDownloaderExpectation1], timeout: 0.5) - - let fileDownloaderExpectation2 = expectation(description: "File downloader after retry") - // Handles response after one retry. - downloader.downloadFileHandler = { url in - fileDownloaderExpectation2.fulfill() - XCTAssertEqual(url, self.fakeDownloadURL) - } - downloader - .completion?(.success(FileDownloaderResponse(urlResponse: fakeResponseExpired, - fileURL: tempFileURL))) - wait(for: [fileDownloaderExpectation2], timeout: 0.5) - - downloader - .completion?(.success(FileDownloaderResponse(urlResponse: fakeResponseExpired, - fileURL: tempFileURL))) - wait(for: [completionExpectation], timeout: 0.5) - - try? ModelFileManager.removeFile(at: tempFileURL) - } - - /// Get model if multiple retries all returned expired urls. - func testGetModelMultipleFailedRetries() { - guard let testApp = FirebaseApp.app() else { - XCTFail("Default app was not configured.") - return - } - testApp.isDataCollectionDefaultEnabled = false - - let modelDownloader = ModelDownloader.modelDownloader() - modelDownloader.numberOfRetries = 2 - let conditions = ModelDownloadConditions() - let session = fakeModelInfoSessionWithURL(fakeDownloadURL, statusCode: 200) - let modelInfoRetriever = fakeModelRetriever(fakeSession: session) - let downloader = MockModelFileDownloader() - let tempFileURL = tempFile() - - let fakeResponseExpired = HTTPURLResponse(url: fakeFileURL, - statusCode: 400, - httpVersion: nil, - headerFields: nil)! - - let fileDownloaderExpectation1 = expectation(description: "File downloader before retry") - let completionExpectation = expectation(description: "Completion handler") - - // Handles initial response. - downloader.downloadFileHandler = { url in - fileDownloaderExpectation1.fulfill() - XCTAssertEqual(url, self.fakeDownloadURL) - } - - modelDownloader.downloadInfoAndModel(modelName: fakeModelName, - modelInfoRetriever: modelInfoRetriever, - downloader: downloader, - conditions: conditions) { result in - completionExpectation.fulfill() - switch result { - // Two retries failed due to expired urls - error returned. - case .success: XCTFail("Unexpected successful model download.") - case let .failure(error): - switch error { - case let .internalError(description: errorMessage): XCTAssertEqual(errorMessage, - "Unable to update expired model info.") - default: XCTFail("Expected failure due to expired model info.") - } - } - } - wait(for: [fileDownloaderExpectation1], timeout: 0.5) - - let fileDownloaderExpectation2 = expectation(description: "file downloader after first retry") - // Handles response after first retry. - downloader.downloadFileHandler = { url in - fileDownloaderExpectation2.fulfill() - XCTAssertEqual(url, self.fakeDownloadURL) - } - downloader - .completion?(.success(FileDownloaderResponse(urlResponse: fakeResponseExpired, - fileURL: tempFileURL))) - wait(for: [fileDownloaderExpectation2], timeout: 0.5) - - let fileDownloaderExpectation3 = - expectation(description: "File downloader after second retry") - // Handles response after second retry. - downloader.downloadFileHandler = { url in - fileDownloaderExpectation3.fulfill() - XCTAssertEqual(url, self.fakeDownloadURL) - } - downloader - .completion?(.success(FileDownloaderResponse(urlResponse: fakeResponseExpired, - fileURL: tempFileURL))) - wait(for: [fileDownloaderExpectation3], timeout: 0.5) - - downloader - .completion?(.success(FileDownloaderResponse(urlResponse: fakeResponseExpired, - fileURL: tempFileURL))) - wait(for: [completionExpectation], timeout: 0.5) - - try? ModelFileManager.removeFile(at: tempFileURL) - } - - /// Get model if a retry finally returns an unexpired download url. - func testGetModelMultipleRetriesWithSuccess() { - guard let testApp = FirebaseApp.app() else { - XCTFail("Default app was not configured.") - return - } - testApp.isDataCollectionDefaultEnabled = false - - let modelDownloader = ModelDownloader.modelDownloader() - modelDownloader.numberOfRetries = 4 - let conditions = ModelDownloadConditions() - let session = fakeModelInfoSessionWithURL(fakeDownloadURL, statusCode: 200) - let modelInfoRetriever = fakeModelRetriever(fakeSession: session) - let downloader = MockModelFileDownloader() - let tempFileURL = tempFile() - - let fakeResponseExpired = HTTPURLResponse(url: fakeFileURL, - statusCode: 400, - httpVersion: nil, - headerFields: nil)! - - let fakeResponseSucceeded = HTTPURLResponse(url: fakeFileURL, - statusCode: 200, - httpVersion: nil, - headerFields: nil)! - - let fileDownloaderExpectation1 = expectation(description: "File downloader before retry") - let completionExpectation = expectation(description: "Completion handler") - - // Handles initial response. - downloader.downloadFileHandler = { url in - fileDownloaderExpectation1.fulfill() - XCTAssertEqual(url, self.fakeDownloadURL) - } - - modelDownloader.downloadInfoAndModel(modelName: fakeModelName, - modelInfoRetriever: modelInfoRetriever, - downloader: downloader, - conditions: conditions) { result in - completionExpectation.fulfill() - switch result { - // One retry failed but the next retry succeeded - model returned. - case let .success(model): - self.checkModel(model: model) - self.deleteFile(at: model.path) - case let .failure(error): - XCTFail("Error - \(error)") - } - } - wait(for: [fileDownloaderExpectation1], timeout: 0.5) - - let fileDownloaderExpectation2 = expectation(description: "File downloader after first retry") - // Handles response after first retry. - downloader.downloadFileHandler = { url in - fileDownloaderExpectation2.fulfill() - XCTAssertEqual(url, self.fakeDownloadURL) - } - downloader - .completion?(.success(FileDownloaderResponse(urlResponse: fakeResponseExpired, - fileURL: tempFileURL))) - wait(for: [fileDownloaderExpectation2], timeout: 0.5) - - let fileDownloaderExpectation3 = - expectation(description: "File downloader after second retry") - // Handles response after second retry. - downloader.downloadFileHandler = { url in - fileDownloaderExpectation3.fulfill() - XCTAssertEqual(url, self.fakeDownloadURL) - } - downloader - .completion?(.success(FileDownloaderResponse(urlResponse: fakeResponseExpired, - fileURL: tempFileURL))) - wait(for: [fileDownloaderExpectation3], timeout: 0.5) - - downloader - .completion?(.success(FileDownloaderResponse(urlResponse: fakeResponseSucceeded, - fileURL: tempFileURL))) - wait(for: [completionExpectation], timeout: 0.5) - - try? ModelFileManager.removeFile(at: tempFileURL) - } - } - - /// Mock URL session for testing. - class MockModelInfoRetrieverSession: ModelInfoRetrieverSession { - var data: Data? - var response: URLResponse? - var error: Error? - - func getModelInfo(with request: URLRequest, - completion: @escaping (Data?, URLResponse?, Error?) -> Void) { - completion(data, response, error) - } - } - - class MockModelFileDownloader: FileDownloader { - var progressHandler: ProgressHandler? - var completion: CompletionHandler? - var downloadFileHandler: ((_ url: URL) -> Void)? - - func downloadFile(with url: URL, progressHandler: @escaping ProgressHandler, - completion: @escaping CompletionHandler) { - self.progressHandler = progressHandler - self.completion = completion - downloadFileHandler?(url) - } - } - - extension GULUserDefaults { - /// Returns a new instance of user defaults. - static func createUnitTestInstance(testName: String) -> GULUserDefaults { - let suiteName = "com.google.firebase.ml.test.\(testName)" - let defaults = GULUserDefaults(suiteName: suiteName) - return defaults - } - - /// Returns the existing user defaults instance. - static func getUnitTestInstance(testName: String) -> GULUserDefaults { - let suiteName = "com.google.firebase.ml.test.\(testName)" - return GULUserDefaults(suiteName: suiteName) - } - } - - // MARK: - Helpers - - extension ModelDownloaderUnitTests { - func fakeModelInfoSessionWithURL(_ url: URL, statusCode: Int, - customJSON: String? = nil) -> MockModelInfoRetrieverSession { - let fakeSession = MockModelInfoRetrieverSession() - let fakeJSON = customJSON ?? fakeModelJSON - fakeSession.data = fakeJSON.data(using: .utf8) - fakeSession.response = HTTPURLResponse( - url: url, - statusCode: statusCode, - httpVersion: nil, - headerFields: ["Etag": "fakeModelHash"] - ) - fakeSession.error = nil - return fakeSession - } - - func fakeModelRetriever(fakeSession: MockModelInfoRetrieverSession, - fakeLocalModelInfo: LocalModelInfo? = nil) -> ModelInfoRetriever { - // TODO: Replace with fake to check if it was used correctly by the download task. - let modelInfoRetriever = ModelInfoRetriever( - modelName: fakeModelName, - projectID: fakeProjectID, - apiKey: fakeAPIKey, - appName: fakeAppName, authTokenProvider: successAuthTokenProvider, - session: fakeSession, localModelInfo: fakeLocalModelInfo - ) - return modelInfoRetriever - } - - func checkModel(model: CustomModel) { - XCTAssertEqual(model.name, fakeModelName) - XCTAssertEqual(model.size, fakeModelSize) - XCTAssertEqual(model.hash, fakeModelHash) - let modelURL = URL(fileURLWithPath: model.path) - XCTAssertTrue(ModelFileManager.isFileReachable(at: modelURL)) - } - - func tempFile(name: String = "fake-model-file.tmp") -> URL { - let tempDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), - isDirectory: true) - let tempFileURL = tempDirectoryURL.appendingPathComponent(name) - let tempData: Data = "fakeModelData".data(using: .utf8)! - try? tempData.write(to: tempFileURL) - return tempFileURL - } - - func tempModelFile(invalid: Bool = false) -> URL { - let modelDirectoryURL = ModelFileManager.modelsDirectory! - let modelFileName = invalid ? "fbml_model_fake-model-file.tmp" : - "fbml_model@@__FIRAPP_DEFAULT@@\(fakeModelName)" - let tempFileURL = modelDirectoryURL.appendingPathComponent(modelFileName) - .appendingPathExtension("tflite") - let tempData: Data = "fakeModelData".data(using: .utf8)! - try? tempData.write(to: tempFileURL) - return tempFileURL - } - - func deleteFile(at path: String) { - let url = URL(fileURLWithPath: path) - do { - try ModelFileManager.removeFile(at: url) - } catch { - XCTFail("Failed to delete file at \(path).") - } - } - - func fakeLocalModelInfo(mismatch: Bool = false) -> LocalModelInfo { - let hash = mismatch ? "wrongModelHash" : fakeModelHash - return LocalModelInfo( - name: fakeModelName, - modelHash: hash, - size: 20 - ) - } - - func fakeModelInfo(expired: Bool, oversized: Bool) -> RemoteModelInfo { - let expiryTime = expired ? Date() : Date().addingTimeInterval(1000) - let size = oversized ? Int(Int64.max) / 4 : fakeModelSize - return RemoteModelInfo(name: fakeModelName, - downloadURL: fakeDownloadURL, - modelHash: fakeModelHash, - size: size, - urlExpiryTime: expiryTime) - } - } - -#endif // !targetEnvironment(macCatalyst) && !os(macOS) diff --git a/IntegrationTesting/ClientApp/ClientApp.xcodeproj/project.pbxproj b/IntegrationTesting/ClientApp/ClientApp.xcodeproj/project.pbxproj index 7c4fcaf425c..cca2cac3880 100644 --- a/IntegrationTesting/ClientApp/ClientApp.xcodeproj/project.pbxproj +++ b/IntegrationTesting/ClientApp/ClientApp.xcodeproj/project.pbxproj @@ -34,7 +34,6 @@ EA7DF5A529EF3327005664A7 /* FirebaseInAppMessaging-Beta in Frameworks */ = {isa = PBXBuildFile; platformFilters = (ios, maccatalyst, tvos, ); productRef = EA7DF5A429EF3327005664A7 /* FirebaseInAppMessaging-Beta */; }; EA7DF5A929EF3327005664A7 /* FirebaseInstallations in Frameworks */ = {isa = PBXBuildFile; productRef = EA7DF5A829EF3327005664A7 /* FirebaseInstallations */; }; EA7DF5AB29EF3327005664A7 /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = EA7DF5AA29EF3327005664A7 /* FirebaseMessaging */; }; - EA7DF5AD29EF3327005664A7 /* FirebaseMLModelDownloader in Frameworks */ = {isa = PBXBuildFile; productRef = EA7DF5AC29EF3327005664A7 /* FirebaseMLModelDownloader */; }; EA7DF5AF29EF3328005664A7 /* FirebasePerformance in Frameworks */ = {isa = PBXBuildFile; platformFilters = (ios, tvos, ); productRef = EA7DF5AE29EF3328005664A7 /* FirebasePerformance */; }; EA7DF5B129EF3328005664A7 /* FirebaseRemoteConfig in Frameworks */ = {isa = PBXBuildFile; productRef = EA7DF5B029EF3328005664A7 /* FirebaseRemoteConfig */; }; EA7DF5B729EF3328005664A7 /* FirebaseStorageCombine-Community in Frameworks */ = {isa = PBXBuildFile; productRef = EA7DF5B629EF3328005664A7 /* FirebaseStorageCombine-Community */; }; @@ -65,7 +64,6 @@ EA7DF5A329EF3327005664A7 /* FirebaseFunctionsCombine-Community in Frameworks */, EA7DF59B29EF3326005664A7 /* FirebaseFirestore in Frameworks */, EA7DF58329EF3326005664A7 /* FirebaseAnalytics in Frameworks */, - EA7DF5AD29EF3327005664A7 /* FirebaseMLModelDownloader in Frameworks */, EA7DF58B29EF3326005664A7 /* FirebaseAppCheck in Frameworks */, EA7DF5AB29EF3327005664A7 /* FirebaseMessaging in Frameworks */, EA7DF59329EF3326005664A7 /* FirebaseCrashlytics in Frameworks */, @@ -203,7 +201,7 @@ EA7DF5A429EF3327005664A7 /* FirebaseInAppMessaging-Beta */, EA7DF5A829EF3327005664A7 /* FirebaseInstallations */, EA7DF5AA29EF3327005664A7 /* FirebaseMessaging */, - EA7DF5AC29EF3327005664A7 /* FirebaseMLModelDownloader */, + EA7DF5AE29EF3328005664A7 /* FirebasePerformance */, EA7DF5B029EF3328005664A7 /* FirebaseRemoteConfig */, EA7DF5B629EF3328005664A7 /* FirebaseStorageCombine-Community */, @@ -672,10 +670,7 @@ isa = XCSwiftPackageProductDependency; productName = FirebaseMessaging; }; - EA7DF5AC29EF3327005664A7 /* FirebaseMLModelDownloader */ = { - isa = XCSwiftPackageProductDependency; - productName = FirebaseMLModelDownloader; - }; + EA7DF5AE29EF3328005664A7 /* FirebasePerformance */ = { isa = XCSwiftPackageProductDependency; productName = FirebasePerformance; diff --git a/IntegrationTesting/ClientApp/Podfile b/IntegrationTesting/ClientApp/Podfile index 58aecd4e06f..bd0676baa94 100644 --- a/IntegrationTesting/ClientApp/Podfile +++ b/IntegrationTesting/ClientApp/Podfile @@ -29,6 +29,5 @@ target 'ClientApp-CocoaPods' do pod 'FirebaseSessions', :path => '../../' pod 'FirebaseStorage', :path => '../../' pod 'FirebasePerformance', :path => '../../' - pod 'FirebaseMLModelDownloader', :path => '../../' pod 'Firebase', :path => '../../' end diff --git a/IntegrationTesting/ClientApp/Shared/swift-import-test.swift b/IntegrationTesting/ClientApp/Shared/swift-import-test.swift index 25cb2f01962..279320b1e62 100644 --- a/IntegrationTesting/ClientApp/Shared/swift-import-test.swift +++ b/IntegrationTesting/ClientApp/Shared/swift-import-test.swift @@ -42,7 +42,6 @@ import FirebaseFunctions #endif // SWIFT_PACKAGE import FirebaseInstallations import FirebaseMessaging -import FirebaseMLModelDownloader #if (os(iOS) && !targetEnvironment(macCatalyst)) || os(tvOS) import FirebaseInAppMessaging import FirebasePerformance diff --git a/Package.swift b/Package.swift index 301ed08edf2..027c3f35e93 100644 --- a/Package.swift +++ b/Package.swift @@ -127,10 +127,6 @@ func packageProducts() -> [Product] { name: "FirebaseMessaging", targets: ["FirebaseMessaging"] ), - .library( - name: "FirebaseMLModelDownloader", - targets: ["FirebaseMLModelDownloader"] - ), .library( name: "FirebasePerformance", targets: ["FirebasePerformanceTarget"] @@ -838,26 +834,6 @@ func packageTargets() -> [Target] { .linkedFramework("Security"), ] ), - - .target( - name: "FirebaseMLModelDownloader", - dependencies: [ - "FirebaseCore", - "FirebaseCoreExtension", - "FirebaseInstallations", - .product(name: "GULUserDefaults", package: "GoogleUtilities"), - ], - path: "FirebaseMLModelDownloader/Sources", - swiftSettings: [ - .swiftLanguageMode(SwiftLanguageMode.v5), - ] - ), - .testTarget( - name: "FirebaseMLModelDownloaderUnit", - dependencies: ["FirebaseMLModelDownloader"], - path: "FirebaseMLModelDownloader/Tests/Unit" - ), - .target( name: "FirebaseMessaging", dependencies: [ diff --git a/ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift b/ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift index f0d45102e44..12951f4e918 100755 --- a/ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift +++ b/ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift @@ -51,7 +51,6 @@ public let shared = Manifest( Pod("FirebaseMessaging", zip: true), Pod("FirebasePerformance", platforms: ["ios", "tvos"], zip: true), Pod("FirebaseStorage", zip: true), - Pod("FirebaseMLModelDownloader", isBeta: true, allowWarnings: true, zip: true), Pod("Firebase", allowWarnings: true, platforms: ["ios", "tvos", "macos"], zip: true), Pod("FirebaseCombineSwift", releasing: false, zip: false), ] diff --git a/docs/ContinuousIntegration.md b/docs/ContinuousIntegration.md index 7e9e3ece9f6..8363c2049f4 100644 --- a/docs/ContinuousIntegration.md +++ b/docs/ContinuousIntegration.md @@ -98,7 +98,7 @@ The previous setup will run podspecs testing nightly. This enables presubmits of podspecs and accelerates the testing process. This is to run presubmit tests for Firebase Apple SDKs in the SDK repo. A job to run `pod spec lint` is added to SDK testing workflows, including ABTesting, Analytics, Auth, Core, Crashlytics, Database, Firestore, Functions, GoogleUtilities, -InAppMessaging, Installations, Messaging, MLModelDownloader, Performance, RemoteConfig and Storage. +InAppMessaging, Installations, Messaging, Performance, RemoteConfig and Storage. These jobs will be triggered in presubmit and run pod spec lint with a source of Firebase/SpecsTesting repo, which is updated to the head of main nightly in the prerelease_cocoapods workflow. diff --git a/scripts/api_diff_report/icore_module.py b/scripts/api_diff_report/icore_module.py index 4f9d259a1b3..d7e56bc941c 100644 --- a/scripts/api_diff_report/icore_module.py +++ b/scripts/api_diff_report/icore_module.py @@ -36,7 +36,6 @@ 'FirebaseInAppMessaging' 'FirebaseInstallations', 'FirebaseMessaging', - 'FirebaseMLModelDownloader', 'FirebasePerformance', 'FirebaseRemoteConfig', # Not buildable. No scheme named "FirebaseSharedSwift" diff --git a/scripts/build.sh b/scripts/build.sh index 659ad732b7c..40c77fcb451 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -37,7 +37,6 @@ product can be one of: Messaging MessagingSample SwiftUISample - MLModelDownloaderSample RemoteConfig RemoteConfigSample Sessions @@ -501,16 +500,6 @@ case "$product-$platform-$method" in fi ;; - MLModelDownloaderSample-*-*) - if check_secrets; then - RunXcodebuild \ - -workspace 'FirebaseMLModelDownloader/Apps/Sample/MLDownloaderTestApp.xcworkspace' \ - -scheme "MLDownloaderTestApp" \ - "${xcb_flags[@]}" \ - build - fi - ;; - WatchOSSample-*-*) RunXcodebuild \ -workspace 'Example/watchOSSample/SampleWatchApp.xcworkspace' \ diff --git a/scripts/install_prereqs.sh b/scripts/install_prereqs.sh index 8e36a85b30a..14e6b05d798 100755 --- a/scripts/install_prereqs.sh +++ b/scripts/install_prereqs.sh @@ -149,11 +149,6 @@ case "$project-$platform-$method" in bundle exec pod install --project-directory=FirebaseMessaging/Apps/SampleStandaloneWatchApp --repo-update ;; - MLModelDownloaderSample-*) - install_xcpretty - bundle exec pod install --project-directory=FirebaseMLModelDownloader/Apps/Sample --repo-update - ;; - RemoteConfigSample-*) install_xcpretty bundle exec pod install --project-directory=FirebaseRemoteConfig/Tests/Sample --repo-update diff --git a/scripts/spec_testing/file_patterns.json b/scripts/spec_testing/file_patterns.json index ad7840f354d..e5e69e20cd0 100644 --- a/scripts/spec_testing/file_patterns.json +++ b/scripts/spec_testing/file_patterns.json @@ -125,14 +125,6 @@ "\\.github/workflows/messaging\\.yml" ] }, - { - "sdk": "mlmodeldownloader", - "podspecs": ["FirebaseMLModelDownloader.podspec"], - "filePatterns": [ - "^FirebaseMLModelDownloader/.*", - "FirebaseMLModelDownloader\\.podspec" - ] - }, { "sdk": "performance", "podspecs": ["FirebasePerformance.podspec"], diff --git a/scripts/spm_test_schemes/FirebaseMLModelDownloaderUnit.xcscheme b/scripts/spm_test_schemes/FirebaseMLModelDownloaderUnit.xcscheme deleted file mode 100644 index 1d68ba2d8e2..00000000000 --- a/scripts/spm_test_schemes/FirebaseMLModelDownloaderUnit.xcscheme +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -