From e2ad494da05168a73b1df997b85fc2b5d2aaad3a Mon Sep 17 00:00:00 2001 From: Guillaume Bernos Date: Wed, 4 Mar 2026 11:12:02 +0100 Subject: [PATCH 1/5] feat(ai): add proper headers for X-Android-Package, X-Android-Cert and x-ios-bundle-identifier --- .../firebase_ai/android/build.gradle | 38 ++++++ .../firebase_ai/android/local-config.gradle | 7 ++ .../android/src/main/AndroidManifest.xml | 3 + .../plugins/firebase/ai/FirebaseAiPlugin.kt | 101 ++++++++++++++++ .../firebase_ai/ios/firebase_ai.podspec | 28 +++++ .../firebase_ai/FirebaseAiPlugin.swift | 49 ++++++++ .../Sources/firebase_ai/Resources/.gitkeep | 0 .../firebase_ai/lib/src/base_model.dart | 5 + .../lib/src/platform_header_helper.dart | 51 ++++++++ .../firebase_ai/macos/firebase_ai.podspec | 28 +++++ .../firebase_ai/FirebaseAiPlugin.swift | 1 + .../Sources/firebase_ai/Resources/.gitkeep | 0 packages/firebase_ai/firebase_ai/pubspec.yaml | 11 ++ .../firebase_ai/test/base_model_test.dart | 62 ++++++++++ .../test/platform_header_helper_test.dart | 113 ++++++++++++++++++ tests/integration_test/e2e_test.dart | 3 + .../firebase_ai/firebase_ai_e2e_test.dart | 58 +++++++++ tests/pubspec.yaml | 1 + 18 files changed, 559 insertions(+) create mode 100644 packages/firebase_ai/firebase_ai/android/build.gradle create mode 100644 packages/firebase_ai/firebase_ai/android/local-config.gradle create mode 100644 packages/firebase_ai/firebase_ai/android/src/main/AndroidManifest.xml create mode 100644 packages/firebase_ai/firebase_ai/android/src/main/kotlin/io/flutter/plugins/firebase/ai/FirebaseAiPlugin.kt create mode 100644 packages/firebase_ai/firebase_ai/ios/firebase_ai.podspec create mode 100644 packages/firebase_ai/firebase_ai/ios/firebase_ai/Sources/firebase_ai/FirebaseAiPlugin.swift create mode 100644 packages/firebase_ai/firebase_ai/ios/firebase_ai/Sources/firebase_ai/Resources/.gitkeep create mode 100644 packages/firebase_ai/firebase_ai/lib/src/platform_header_helper.dart create mode 100644 packages/firebase_ai/firebase_ai/macos/firebase_ai.podspec create mode 120000 packages/firebase_ai/firebase_ai/macos/firebase_ai/Sources/firebase_ai/FirebaseAiPlugin.swift create mode 100644 packages/firebase_ai/firebase_ai/macos/firebase_ai/Sources/firebase_ai/Resources/.gitkeep create mode 100644 packages/firebase_ai/firebase_ai/test/platform_header_helper_test.dart create mode 100644 tests/integration_test/firebase_ai/firebase_ai_e2e_test.dart diff --git a/packages/firebase_ai/firebase_ai/android/build.gradle b/packages/firebase_ai/firebase_ai/android/build.gradle new file mode 100644 index 000000000000..b2cbd9a17adb --- /dev/null +++ b/packages/firebase_ai/firebase_ai/android/build.gradle @@ -0,0 +1,38 @@ +group 'io.flutter.plugins.firebase.ai' +version '1.0-SNAPSHOT' + +apply plugin: 'com.android.library' +apply from: file("local-config.gradle") + +buildscript { + repositories { + google() + mavenCentral() + } +} + +android { + if (project.android.hasProperty("namespace")) { + namespace 'io.flutter.plugins.firebase.ai' + } + + compileSdk project.ext.compileSdk + + defaultConfig { + minSdkVersion project.ext.minSdk + targetSdkVersion project.ext.targetSdk + } + + compileOptions { + sourceCompatibility project.ext.javaVersion + targetCompatibility project.ext.javaVersion + } + + kotlinOptions { + jvmTarget = project.ext.javaVersion + } + + lintOptions { + disable 'InvalidPackage' + } +} diff --git a/packages/firebase_ai/firebase_ai/android/local-config.gradle b/packages/firebase_ai/firebase_ai/android/local-config.gradle new file mode 100644 index 000000000000..2adcdf5c1729 --- /dev/null +++ b/packages/firebase_ai/firebase_ai/android/local-config.gradle @@ -0,0 +1,7 @@ +ext { + compileSdk=34 + minSdk=23 + targetSdk=34 + javaVersion = JavaVersion.toVersion(17) + androidGradlePluginVersion = '8.3.0' +} diff --git a/packages/firebase_ai/firebase_ai/android/src/main/AndroidManifest.xml b/packages/firebase_ai/firebase_ai/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..db54cec9bea5 --- /dev/null +++ b/packages/firebase_ai/firebase_ai/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/packages/firebase_ai/firebase_ai/android/src/main/kotlin/io/flutter/plugins/firebase/ai/FirebaseAiPlugin.kt b/packages/firebase_ai/firebase_ai/android/src/main/kotlin/io/flutter/plugins/firebase/ai/FirebaseAiPlugin.kt new file mode 100644 index 000000000000..46282110bfd3 --- /dev/null +++ b/packages/firebase_ai/firebase_ai/android/src/main/kotlin/io/flutter/plugins/firebase/ai/FirebaseAiPlugin.kt @@ -0,0 +1,101 @@ +// Copyright 2025 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. + +package io.flutter.plugins.firebase.ai + +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import android.util.Log +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException + +class FirebaseAiPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { + private lateinit var channel: MethodChannel + private lateinit var context: Context + + override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { + context = binding.applicationContext + channel = MethodChannel(binding.binaryMessenger, "plugins.flutter.io/firebase_ai") + channel.setMethodCallHandler(this) + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + "getPlatformHeaders" -> { + val headers = mapOf( + "X-Android-Package" to context.packageName, + "X-Android-Cert" to (getSigningCertFingerprint() ?: "") + ) + result.success(headers) + } + else -> result.notImplemented() + } + } + + @OptIn(ExperimentalStdlibApi::class) + private fun getSigningCertFingerprint(): String? { + val packageName = context.packageName + val signature = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + val packageInfo = try { + context.packageManager.getPackageInfo( + packageName, + PackageManager.GET_SIGNING_CERTIFICATES + ) + } catch (e: PackageManager.NameNotFoundException) { + Log.d(TAG, "PackageManager couldn't find the package \"$packageName\"") + return null + } + val signingInfo = packageInfo?.signingInfo ?: return null + if (signingInfo.hasMultipleSigners()) { + signingInfo.apkContentsSigners.firstOrNull() + } else { + signingInfo.signingCertificateHistory.lastOrNull() + } + } else { + @Suppress("DEPRECATION") + val packageInfo = try { + context.packageManager.getPackageInfo( + packageName, + PackageManager.GET_SIGNATURES + ) + } catch (e: PackageManager.NameNotFoundException) { + Log.d(TAG, "PackageManager couldn't find the package \"$packageName\"") + return null + } + @Suppress("DEPRECATION") + packageInfo?.signatures?.firstOrNull() + } ?: return null + + return try { + val messageDigest = MessageDigest.getInstance("SHA-1") + val digest = messageDigest.digest(signature.toByteArray()) + digest.toHexString(HexFormat.UpperCase) + } catch (e: NoSuchAlgorithmException) { + Log.w(TAG, "No support for SHA-1 algorithm found.", e) + null + } + } + + companion object { + private const val TAG = "FirebaseAiPlugin" + } +} diff --git a/packages/firebase_ai/firebase_ai/ios/firebase_ai.podspec b/packages/firebase_ai/firebase_ai/ios/firebase_ai.podspec new file mode 100644 index 000000000000..25d4ecfe3e5d --- /dev/null +++ b/packages/firebase_ai/firebase_ai/ios/firebase_ai.podspec @@ -0,0 +1,28 @@ +require 'yaml' + +pubspec = YAML.load_file(File.join('..', 'pubspec.yaml')) +library_version = pubspec['version'].gsub('+', '-') + +Pod::Spec.new do |s| + s.name = pubspec['name'] + s.version = library_version + s.summary = pubspec['description'] + s.description = pubspec['description'] + s.homepage = pubspec['homepage'] + s.license = { :file => '../LICENSE' } + s.authors = 'The Chromium Authors' + s.source = { :path => '.' } + + s.source_files = 'firebase_ai/Sources/firebase_ai/**/*.swift' + + s.ios.deployment_target = '15.0' + s.dependency 'Flutter' + + s.swift_version = '5.0' + + s.static_framework = true + s.pod_target_xcconfig = { + 'GCC_PREPROCESSOR_DEFINITIONS' => "LIBRARY_VERSION=\\\"#{library_version}\\\" LIBRARY_NAME=\\\"flutter-fire-ai\\\"", + 'DEFINES_MODULE' => 'YES' + } +end diff --git a/packages/firebase_ai/firebase_ai/ios/firebase_ai/Sources/firebase_ai/FirebaseAiPlugin.swift b/packages/firebase_ai/firebase_ai/ios/firebase_ai/Sources/firebase_ai/FirebaseAiPlugin.swift new file mode 100644 index 000000000000..7bc1588947bf --- /dev/null +++ b/packages/firebase_ai/firebase_ai/ios/firebase_ai/Sources/firebase_ai/FirebaseAiPlugin.swift @@ -0,0 +1,49 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(FlutterMacOS) + import FlutterMacOS +#else + import Flutter +#endif + +public class FirebaseAiPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + #if canImport(FlutterMacOS) + let messenger = registrar.messenger + #else + let messenger = registrar.messenger() + #endif + + let channel = FlutterMethodChannel( + name: "plugins.flutter.io/firebase_ai", + binaryMessenger: messenger + ) + let instance = FirebaseAiPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "getPlatformHeaders": + var headers: [String: String] = [:] + if let bundleId = Bundle.main.bundleIdentifier { + headers["x-ios-bundle-identifier"] = bundleId + } + result(headers) + default: + result(FlutterMethodNotImplemented) + } + } +} diff --git a/packages/firebase_ai/firebase_ai/ios/firebase_ai/Sources/firebase_ai/Resources/.gitkeep b/packages/firebase_ai/firebase_ai/ios/firebase_ai/Sources/firebase_ai/Resources/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/firebase_ai/firebase_ai/lib/src/base_model.dart b/packages/firebase_ai/firebase_ai/lib/src/base_model.dart index 01ac7eb834b3..22d896303bfb 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/base_model.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/base_model.dart @@ -30,6 +30,7 @@ import 'content.dart'; import 'developer/api.dart'; import 'error.dart'; import 'firebaseai_version.dart'; +import 'platform_header_helper.dart'; import 'imagen/imagen_api.dart'; import 'imagen/imagen_content.dart'; import 'imagen/imagen_edit.dart'; @@ -300,6 +301,10 @@ abstract class BaseModel { if (app != null && app.isAutomaticDataCollectionEnabled) { headers['X-Firebase-AppId'] = app.options.appId; } + // Add platform-specific headers for API key restrictions. + // Android: X-Android-Package + X-Android-Cert + // iOS/macOS: x-ios-bundle-identifier + headers.addAll(await getPlatformSecurityHeaders()); return headers; }; } diff --git a/packages/firebase_ai/firebase_ai/lib/src/platform_header_helper.dart b/packages/firebase_ai/firebase_ai/lib/src/platform_header_helper.dart new file mode 100644 index 000000000000..46c2159b02b3 --- /dev/null +++ b/packages/firebase_ai/firebase_ai/lib/src/platform_header_helper.dart @@ -0,0 +1,51 @@ +// Copyright 2025 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 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:meta/meta.dart'; + +/// Method channel for the native platform helper plugin. +@visibleForTesting +const platformHeaderChannel = MethodChannel('plugins.flutter.io/firebase_ai'); + +Map? _cachedHeaders; + +/// Clears the cached platform headers. Only for use in tests. +@visibleForTesting +void clearPlatformSecurityHeadersCache() { + _cachedHeaders = null; +} + +/// Returns platform-specific security headers for API key restrictions. +/// +/// Each platform's native plugin returns the appropriate headers: +/// - **Android**: `X-Android-Package` and `X-Android-Cert` +/// - **iOS/macOS**: `x-ios-bundle-identifier` +/// - **Web/other**: empty map (no plugin registered) +/// +/// Results are cached since platform identity does not change at runtime. +Future> getPlatformSecurityHeaders() async { + if (kIsWeb) return const {}; + if (_cachedHeaders != null) return _cachedHeaders!; + + try { + final result = await platformHeaderChannel + .invokeMapMethod('getPlatformHeaders'); + _cachedHeaders = result ?? const {}; + } catch (_) { + _cachedHeaders = const {}; + } + return _cachedHeaders!; +} diff --git a/packages/firebase_ai/firebase_ai/macos/firebase_ai.podspec b/packages/firebase_ai/firebase_ai/macos/firebase_ai.podspec new file mode 100644 index 000000000000..ebe84b6322df --- /dev/null +++ b/packages/firebase_ai/firebase_ai/macos/firebase_ai.podspec @@ -0,0 +1,28 @@ +require 'yaml' + +pubspec = YAML.load_file(File.join('..', 'pubspec.yaml')) +library_version = pubspec['version'].gsub('+', '-') + +Pod::Spec.new do |s| + s.name = pubspec['name'] + s.version = library_version + s.summary = pubspec['description'] + s.description = pubspec['description'] + s.homepage = pubspec['homepage'] + s.license = { :file => '../LICENSE' } + s.authors = 'The Chromium Authors' + s.source = { :path => '.' } + + s.source_files = 'firebase_ai/Sources/firebase_ai/**/*.swift' + + s.platform = :osx, '10.15' + s.swift_version = '5.0' + + s.dependency 'FlutterMacOS' + + s.static_framework = true + s.pod_target_xcconfig = { + 'GCC_PREPROCESSOR_DEFINITIONS' => "LIBRARY_VERSION=\\\"#{library_version}\\\" LIBRARY_NAME=\\\"flutter-fire-ai\\\"", + 'DEFINES_MODULE' => 'YES' + } +end diff --git a/packages/firebase_ai/firebase_ai/macos/firebase_ai/Sources/firebase_ai/FirebaseAiPlugin.swift b/packages/firebase_ai/firebase_ai/macos/firebase_ai/Sources/firebase_ai/FirebaseAiPlugin.swift new file mode 120000 index 000000000000..e93eb2a83545 --- /dev/null +++ b/packages/firebase_ai/firebase_ai/macos/firebase_ai/Sources/firebase_ai/FirebaseAiPlugin.swift @@ -0,0 +1 @@ +../../../../ios/firebase_ai/Sources/firebase_ai/FirebaseAiPlugin.swift \ No newline at end of file diff --git a/packages/firebase_ai/firebase_ai/macos/firebase_ai/Sources/firebase_ai/Resources/.gitkeep b/packages/firebase_ai/firebase_ai/macos/firebase_ai/Sources/firebase_ai/Resources/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/firebase_ai/firebase_ai/pubspec.yaml b/packages/firebase_ai/firebase_ai/pubspec.yaml index e545f84b43b2..ebf19451aff8 100644 --- a/packages/firebase_ai/firebase_ai/pubspec.yaml +++ b/packages/firebase_ai/firebase_ai/pubspec.yaml @@ -37,3 +37,14 @@ dev_dependencies: matcher: ^0.12.16 mockito: ^5.0.0 plugin_platform_interface: ^2.1.3 + +flutter: + plugin: + platforms: + android: + package: io.flutter.plugins.firebase.ai + pluginClass: FirebaseAiPlugin + ios: + pluginClass: FirebaseAiPlugin + macos: + pluginClass: FirebaseAiPlugin diff --git a/packages/firebase_ai/firebase_ai/test/base_model_test.dart b/packages/firebase_ai/firebase_ai/test/base_model_test.dart index 2cefadf4f39a..a4482bc7ddf3 100644 --- a/packages/firebase_ai/firebase_ai/test/base_model_test.dart +++ b/packages/firebase_ai/firebase_ai/test/base_model_test.dart @@ -14,9 +14,12 @@ import 'package:firebase_ai/src/base_model.dart'; import 'package:firebase_ai/src/client.dart'; +import 'package:firebase_ai/src/platform_header_helper.dart'; import 'package:firebase_app_check/firebase_app_check.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; @@ -74,7 +77,13 @@ class MockApiClient extends Mock implements ApiClient { } void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + group('BaseModel', () { + setUp(() { + clearPlatformSecurityHeadersCache(); + }); + test('firebaseTokens returns a function that generates headers', () async { final tokenFunction = BaseModel.firebaseTokens(null, null, null, false); final headers = await tokenFunction(); @@ -159,5 +168,58 @@ void main() { expect(headers['x-goog-api-client'], contains('fire')); expect(headers.length, 2); }); + + test('firebaseTokens includes Android platform headers when available', + () async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + addTearDown(() { + debugDefaultTargetPlatformOverride = null; + }); + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(platformHeaderChannel, + (MethodCall methodCall) async { + return { + 'X-Android-Package': 'com.example.test', + 'X-Android-Cert': 'AABBCCDD', + }; + }); + addTearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(platformHeaderChannel, null); + }); + + final tokenFunction = BaseModel.firebaseTokens(null, null, null, false); + final headers = await tokenFunction(); + expect(headers['X-Android-Package'], 'com.example.test'); + expect(headers['X-Android-Cert'], 'AABBCCDD'); + expect(headers['x-goog-api-client'], contains('gl-dart')); + expect(headers.length, 3); + }); + + test('firebaseTokens includes iOS bundle identifier when available', + () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(platformHeaderChannel, + (MethodCall methodCall) async { + return { + 'x-ios-bundle-identifier': 'com.example.iosapp', + }; + }); + addTearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(platformHeaderChannel, null); + }); + + final mockApp = MockFirebaseApp(); + + final tokenFunction = + BaseModel.firebaseTokens(null, null, mockApp, false); + final headers = await tokenFunction(); + expect(headers['x-ios-bundle-identifier'], 'com.example.iosapp'); + expect(headers['X-Firebase-AppId'], 'test-app-id'); + expect(headers['x-goog-api-client'], contains('gl-dart')); + expect(headers.length, 3); + }); }); } diff --git a/packages/firebase_ai/firebase_ai/test/platform_header_helper_test.dart b/packages/firebase_ai/firebase_ai/test/platform_header_helper_test.dart new file mode 100644 index 000000000000..d2c82e588796 --- /dev/null +++ b/packages/firebase_ai/firebase_ai/test/platform_header_helper_test.dart @@ -0,0 +1,113 @@ +// Copyright 2025 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 'package:firebase_ai/src/platform_header_helper.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + clearPlatformSecurityHeadersCache(); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(platformHeaderChannel, null); + }); + + group('getPlatformSecurityHeaders', () { + test('returns headers from native plugin', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(platformHeaderChannel, + (MethodCall methodCall) async { + if (methodCall.method == 'getPlatformHeaders') { + return { + 'X-Android-Package': 'com.example.test', + 'X-Android-Cert': 'AABBCCDD', + }; + } + return null; + }); + + final headers = await getPlatformSecurityHeaders(); + + expect(headers['X-Android-Package'], 'com.example.test'); + expect(headers['X-Android-Cert'], 'AABBCCDD'); + expect(headers.length, 2); + }); + + test('returns iOS bundle identifier from native plugin', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(platformHeaderChannel, + (MethodCall methodCall) async { + if (methodCall.method == 'getPlatformHeaders') { + return { + 'x-ios-bundle-identifier': 'com.example.iosapp', + }; + } + return null; + }); + + final headers = await getPlatformSecurityHeaders(); + + expect(headers['x-ios-bundle-identifier'], 'com.example.iosapp'); + expect(headers.length, 1); + }); + + test('caches result across calls', () async { + var callCount = 0; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(platformHeaderChannel, + (MethodCall methodCall) async { + callCount++; + return { + 'X-Android-Package': 'com.example.test', + 'X-Android-Cert': 'AABBCCDD', + }; + }); + + await getPlatformSecurityHeaders(); + await getPlatformSecurityHeaders(); + await getPlatformSecurityHeaders(); + + expect(callCount, 1); + }); + + test('returns empty map when native plugin is not available', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(platformHeaderChannel, + (MethodCall methodCall) async { + throw MissingPluginException(); + }); + + final headers = await getPlatformSecurityHeaders(); + + expect(headers, isEmpty); + }); + + test('returns empty map when native plugin returns null', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(platformHeaderChannel, + (MethodCall methodCall) async { + return null; + }); + + final headers = await getPlatformSecurityHeaders(); + + expect(headers, isEmpty); + }); + }); +} diff --git a/tests/integration_test/e2e_test.dart b/tests/integration_test/e2e_test.dart index 7f5d657b1b56..75818dc8f3a0 100644 --- a/tests/integration_test/e2e_test.dart +++ b/tests/integration_test/e2e_test.dart @@ -7,6 +7,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'cloud_functions/cloud_functions_e2e_test.dart' as cloud_functions; +import 'firebase_ai/firebase_ai_e2e_test.dart' as firebase_ai; import 'firebase_analytics/firebase_analytics_e2e_test.dart' as firebase_analytics; import 'firebase_app_check/firebase_app_check_e2e_test.dart' @@ -51,6 +52,7 @@ void main() { } if (kIsWeb) { firebase_core.main(); + firebase_ai.main(); firebase_auth.main(); firebase_database.main(); firebase_crashlytics.main(); @@ -87,6 +89,7 @@ void main() { void runAllTests() { firebase_core.main(); + firebase_ai.main(); firebase_auth.main(); firebase_database.main(); firebase_crashlytics.main(); diff --git a/tests/integration_test/firebase_ai/firebase_ai_e2e_test.dart b/tests/integration_test/firebase_ai/firebase_ai_e2e_test.dart new file mode 100644 index 000000000000..606479026969 --- /dev/null +++ b/tests/integration_test/firebase_ai/firebase_ai_e2e_test.dart @@ -0,0 +1,58 @@ +// Copyright 2025, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io' show Platform; + +import 'package:firebase_ai/src/platform_header_helper.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('firebase_ai', () { + group('platform security headers', () { + testWidgets('returns non-empty headers on mobile platforms', + (WidgetTester tester) async { + final headers = await getPlatformSecurityHeaders(); + + expect(headers, isNotEmpty, + reason: 'Native plugin should return platform headers'); + }); + + testWidgets('returns correct Android headers', + (WidgetTester tester) async { + if (!Platform.isAndroid) return; + + final headers = await getPlatformSecurityHeaders(); + + expect(headers, contains('X-Android-Package')); + expect(headers['X-Android-Package'], isNotEmpty, + reason: 'Package name should not be empty'); + // Cert may be empty in some emulator environments, but key must exist. + expect(headers, contains('X-Android-Cert')); + }); + + testWidgets('returns correct iOS/macOS headers', + (WidgetTester tester) async { + if (!Platform.isIOS && !Platform.isMacOS) return; + + final headers = await getPlatformSecurityHeaders(); + + expect(headers, contains('x-ios-bundle-identifier')); + expect(headers['x-ios-bundle-identifier'], isNotEmpty, + reason: 'Bundle identifier should not be empty'); + }); + + testWidgets('caches headers across calls', + (WidgetTester tester) async { + final headers1 = await getPlatformSecurityHeaders(); + final headers2 = await getPlatformSecurityHeaders(); + + expect(identical(headers1, headers2), isTrue, + reason: 'Headers should be cached after first call'); + }); + }); + }); +} diff --git a/tests/pubspec.yaml b/tests/pubspec.yaml index 0fb1e979b9cd..7fbc97e7a6bf 100644 --- a/tests/pubspec.yaml +++ b/tests/pubspec.yaml @@ -42,6 +42,7 @@ dependencies: firebase_remote_config: ^6.2.0 firebase_remote_config_platform_interface: ^2.1.0 firebase_remote_config_web: ^1.10.4 + firebase_ai: ^3.9.0 firebase_storage: ^13.1.0 firebase_storage_platform_interface: ^5.2.18 firebase_storage_web: ^3.11.3 From 9fe3dce4b1abb45bb652c88a94734a3835c6ad59 Mon Sep 17 00:00:00 2001 From: Guillaume Bernos Date: Wed, 4 Mar 2026 11:19:46 +0100 Subject: [PATCH 2/5] analyze fixes --- packages/firebase_ai/firebase_ai/lib/src/base_model.dart | 2 +- .../firebase_ai/lib/src/platform_header_helper.dart | 1 - packages/firebase_ai/firebase_ai/test/base_model_test.dart | 4 +--- .../firebase_ai/test/platform_header_helper_test.dart | 4 +--- 4 files changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/firebase_ai/firebase_ai/lib/src/base_model.dart b/packages/firebase_ai/firebase_ai/lib/src/base_model.dart index 22d896303bfb..cf1f98db8b1a 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/base_model.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/base_model.dart @@ -30,13 +30,13 @@ import 'content.dart'; import 'developer/api.dart'; import 'error.dart'; import 'firebaseai_version.dart'; -import 'platform_header_helper.dart'; import 'imagen/imagen_api.dart'; import 'imagen/imagen_content.dart'; import 'imagen/imagen_edit.dart'; import 'imagen/imagen_reference.dart'; import 'live_api.dart'; import 'live_session.dart'; +import 'platform_header_helper.dart'; import 'tool.dart'; part 'generative_model.dart'; diff --git a/packages/firebase_ai/firebase_ai/lib/src/platform_header_helper.dart b/packages/firebase_ai/firebase_ai/lib/src/platform_header_helper.dart index 46c2159b02b3..9a7b159ff416 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/platform_header_helper.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/platform_header_helper.dart @@ -14,7 +14,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import 'package:meta/meta.dart'; /// Method channel for the native platform helper plugin. @visibleForTesting diff --git a/packages/firebase_ai/firebase_ai/test/base_model_test.dart b/packages/firebase_ai/firebase_ai/test/base_model_test.dart index a4482bc7ddf3..089ef6fe2382 100644 --- a/packages/firebase_ai/firebase_ai/test/base_model_test.dart +++ b/packages/firebase_ai/firebase_ai/test/base_model_test.dart @@ -80,9 +80,7 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('BaseModel', () { - setUp(() { - clearPlatformSecurityHeadersCache(); - }); + setUp(clearPlatformSecurityHeadersCache); test('firebaseTokens returns a function that generates headers', () async { final tokenFunction = BaseModel.firebaseTokens(null, null, null, false); diff --git a/packages/firebase_ai/firebase_ai/test/platform_header_helper_test.dart b/packages/firebase_ai/firebase_ai/test/platform_header_helper_test.dart index d2c82e588796..23f5f27e8df9 100644 --- a/packages/firebase_ai/firebase_ai/test/platform_header_helper_test.dart +++ b/packages/firebase_ai/firebase_ai/test/platform_header_helper_test.dart @@ -19,9 +19,7 @@ import 'package:flutter_test/flutter_test.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - setUp(() { - clearPlatformSecurityHeadersCache(); - }); + setUp(clearPlatformSecurityHeadersCache); tearDown(() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger From 86cf6b6a6be137870f573751dd4d201f162bc71f Mon Sep 17 00:00:00 2001 From: Guillaume Bernos Date: Wed, 4 Mar 2026 11:33:43 +0100 Subject: [PATCH 3/5] clean --- .../firebase_ai/firebase_ai_e2e_test.dart | 120 +++++++++++------- tests/pubspec.yaml | 2 +- 2 files changed, 78 insertions(+), 44 deletions(-) diff --git a/tests/integration_test/firebase_ai/firebase_ai_e2e_test.dart b/tests/integration_test/firebase_ai/firebase_ai_e2e_test.dart index 606479026969..fdbf69c13716 100644 --- a/tests/integration_test/firebase_ai/firebase_ai_e2e_test.dart +++ b/tests/integration_test/firebase_ai/firebase_ai_e2e_test.dart @@ -2,57 +2,91 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -import 'dart:io' show Platform; - -import 'package:firebase_ai/src/platform_header_helper.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; +const _channel = MethodChannel('plugins.flutter.io/firebase_ai'); + void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('firebase_ai', () { group('platform security headers', () { - testWidgets('returns non-empty headers on mobile platforms', - (WidgetTester tester) async { - final headers = await getPlatformSecurityHeaders(); - - expect(headers, isNotEmpty, - reason: 'Native plugin should return platform headers'); - }); - - testWidgets('returns correct Android headers', - (WidgetTester tester) async { - if (!Platform.isAndroid) return; - - final headers = await getPlatformSecurityHeaders(); - - expect(headers, contains('X-Android-Package')); - expect(headers['X-Android-Package'], isNotEmpty, - reason: 'Package name should not be empty'); - // Cert may be empty in some emulator environments, but key must exist. - expect(headers, contains('X-Android-Cert')); - }); - - testWidgets('returns correct iOS/macOS headers', - (WidgetTester tester) async { - if (!Platform.isIOS && !Platform.isMacOS) return; - - final headers = await getPlatformSecurityHeaders(); - - expect(headers, contains('x-ios-bundle-identifier')); - expect(headers['x-ios-bundle-identifier'], isNotEmpty, - reason: 'Bundle identifier should not be empty'); - }); - - testWidgets('caches headers across calls', - (WidgetTester tester) async { - final headers1 = await getPlatformSecurityHeaders(); - final headers2 = await getPlatformSecurityHeaders(); - - expect(identical(headers1, headers2), isTrue, - reason: 'Headers should be cached after first call'); - }); + testWidgets( + 'returns non-empty headers on mobile platforms', + skip: kIsWeb, + (WidgetTester tester) async { + final headers = await _channel.invokeMapMethod( + 'getPlatformHeaders', + ); + + expect( + headers, + isNotNull, + reason: 'Native plugin should return platform headers', + ); + expect( + headers, + isNotEmpty, + reason: 'Native plugin should return non-empty platform headers', + ); + }, + ); + + testWidgets( + 'returns correct Android headers', + skip: kIsWeb || defaultTargetPlatform != TargetPlatform.android, + (WidgetTester tester) async { + final headers = await _channel.invokeMapMethod( + 'getPlatformHeaders', + ); + + expect(headers, contains('X-Android-Package')); + expect( + headers!['X-Android-Package'], + isNotEmpty, + reason: 'Package name should not be empty', + ); + // Cert may be empty in some emulator environments, but key must exist. + expect(headers, contains('X-Android-Cert')); + }, + ); + + testWidgets( + 'returns correct iOS/macOS headers', + skip: kIsWeb || + (defaultTargetPlatform != TargetPlatform.iOS && + defaultTargetPlatform != TargetPlatform.macOS), + (WidgetTester tester) async { + final headers = await _channel.invokeMapMethod( + 'getPlatformHeaders', + ); + + expect(headers, contains('x-ios-bundle-identifier')); + expect( + headers!['x-ios-bundle-identifier'], + isNotEmpty, + reason: 'Bundle identifier should not be empty', + ); + }, + ); + + testWidgets( + 'returns empty headers on web', + skip: !kIsWeb, + (WidgetTester tester) async { + // On web, no native plugin is registered, so the channel call + // should throw a MissingPluginException. + expect( + () => _channel.invokeMapMethod( + 'getPlatformHeaders', + ), + throwsA(isA()), + ); + }, + ); }); }); } diff --git a/tests/pubspec.yaml b/tests/pubspec.yaml index 7fbc97e7a6bf..677d7fecbbaf 100644 --- a/tests/pubspec.yaml +++ b/tests/pubspec.yaml @@ -13,6 +13,7 @@ dependencies: cloud_functions_platform_interface: ^5.8.10 cloud_functions_web: ^5.1.3 collection: ^1.15.0 + firebase_ai: ^3.9.0 firebase_analytics: ^12.1.3 firebase_analytics_platform_interface: ^5.0.7 firebase_analytics_web: ^0.6.1+3 @@ -42,7 +43,6 @@ dependencies: firebase_remote_config: ^6.2.0 firebase_remote_config_platform_interface: ^2.1.0 firebase_remote_config_web: ^1.10.4 - firebase_ai: ^3.9.0 firebase_storage: ^13.1.0 firebase_storage_platform_interface: ^5.2.18 firebase_storage_web: ^3.11.3 From 0d089436f9e8fc8fb452b08d7021e62398bfebae Mon Sep 17 00:00:00 2001 From: Guillaume Bernos Date: Wed, 4 Mar 2026 11:57:13 +0100 Subject: [PATCH 4/5] cleaning --- .../firebase_ai/android/build.gradle | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/firebase_ai/firebase_ai/android/build.gradle b/packages/firebase_ai/firebase_ai/android/build.gradle index b2cbd9a17adb..13affc9f6740 100644 --- a/packages/firebase_ai/firebase_ai/android/build.gradle +++ b/packages/firebase_ai/firebase_ai/android/build.gradle @@ -4,6 +4,12 @@ version '1.0-SNAPSHOT' apply plugin: 'com.android.library' apply from: file("local-config.gradle") +// AGP 9+ has built-in Kotlin support; older versions need the plugin explicitly. +def agpMajor = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')[0] as int +if (agpMajor < 9) { + apply plugin: 'kotlin-android' +} + buildscript { repositories { google() @@ -16,11 +22,10 @@ android { namespace 'io.flutter.plugins.firebase.ai' } - compileSdk project.ext.compileSdk + compileSdkVersion project.ext.compileSdk defaultConfig { minSdkVersion project.ext.minSdk - targetSdkVersion project.ext.targetSdk } compileOptions { @@ -28,8 +33,14 @@ android { targetCompatibility project.ext.javaVersion } - kotlinOptions { - jvmTarget = project.ext.javaVersion + if (agpMajor < 9) { + kotlinOptions { + jvmTarget = project.ext.javaVersion + } + } + + sourceSets { + main.java.srcDirs += "src/main/kotlin" } lintOptions { From 2159eaf2e30a2c31c107e92e8d5e6fc7d2eed7b8 Mon Sep 17 00:00:00 2001 From: Guillaume Bernos Date: Fri, 6 Mar 2026 08:32:33 +0100 Subject: [PATCH 5/5] fix casing and feedback --- .../ai/{FirebaseAiPlugin.kt => FirebaseAIPlugin.kt} | 10 +++++----- .../{FirebaseAiPlugin.swift => FirebaseAIPlugin.swift} | 6 +++--- .../firebase_ai/lib/src/platform_header_helper.dart | 2 +- .../Sources/firebase_ai/FirebaseAIPlugin.swift | 1 + .../Sources/firebase_ai/FirebaseAiPlugin.swift | 1 - packages/firebase_ai/firebase_ai/pubspec.yaml | 6 +++--- .../firebase_ai/test/platform_header_helper_test.dart | 2 +- .../firebase_ai/firebase_ai_e2e_test.dart | 4 +++- 8 files changed, 17 insertions(+), 15 deletions(-) rename packages/firebase_ai/firebase_ai/android/src/main/kotlin/io/flutter/plugins/firebase/ai/{FirebaseAiPlugin.kt => FirebaseAIPlugin.kt} (91%) rename packages/firebase_ai/firebase_ai/ios/firebase_ai/Sources/firebase_ai/{FirebaseAiPlugin.swift => FirebaseAIPlugin.swift} (92%) create mode 120000 packages/firebase_ai/firebase_ai/macos/firebase_ai/Sources/firebase_ai/FirebaseAIPlugin.swift delete mode 120000 packages/firebase_ai/firebase_ai/macos/firebase_ai/Sources/firebase_ai/FirebaseAiPlugin.swift diff --git a/packages/firebase_ai/firebase_ai/android/src/main/kotlin/io/flutter/plugins/firebase/ai/FirebaseAiPlugin.kt b/packages/firebase_ai/firebase_ai/android/src/main/kotlin/io/flutter/plugins/firebase/ai/FirebaseAIPlugin.kt similarity index 91% rename from packages/firebase_ai/firebase_ai/android/src/main/kotlin/io/flutter/plugins/firebase/ai/FirebaseAiPlugin.kt rename to packages/firebase_ai/firebase_ai/android/src/main/kotlin/io/flutter/plugins/firebase/ai/FirebaseAIPlugin.kt index 46282110bfd3..3377f693d3e5 100644 --- a/packages/firebase_ai/firebase_ai/android/src/main/kotlin/io/flutter/plugins/firebase/ai/FirebaseAiPlugin.kt +++ b/packages/firebase_ai/firebase_ai/android/src/main/kotlin/io/flutter/plugins/firebase/ai/FirebaseAIPlugin.kt @@ -1,4 +1,4 @@ -// Copyright 2025 Google LLC +// Copyright 2026 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ import io.flutter.plugin.common.MethodChannel import java.security.MessageDigest import java.security.NoSuchAlgorithmException -class FirebaseAiPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { +class FirebaseAIPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { private lateinit var channel: MethodChannel private lateinit var context: Context @@ -61,7 +61,7 @@ class FirebaseAiPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { PackageManager.GET_SIGNING_CERTIFICATES ) } catch (e: PackageManager.NameNotFoundException) { - Log.d(TAG, "PackageManager couldn't find the package \"$packageName\"") + Log.e(TAG, "PackageManager couldn't find the package \"$packageName\"", e) return null } val signingInfo = packageInfo?.signingInfo ?: return null @@ -78,7 +78,7 @@ class FirebaseAiPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { PackageManager.GET_SIGNATURES ) } catch (e: PackageManager.NameNotFoundException) { - Log.d(TAG, "PackageManager couldn't find the package \"$packageName\"") + Log.e(TAG, "PackageManager couldn't find the package \"$packageName\"", e) return null } @Suppress("DEPRECATION") @@ -96,6 +96,6 @@ class FirebaseAiPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { } companion object { - private const val TAG = "FirebaseAiPlugin" + private const val TAG = "FirebaseAIPlugin" } } diff --git a/packages/firebase_ai/firebase_ai/ios/firebase_ai/Sources/firebase_ai/FirebaseAiPlugin.swift b/packages/firebase_ai/firebase_ai/ios/firebase_ai/Sources/firebase_ai/FirebaseAIPlugin.swift similarity index 92% rename from packages/firebase_ai/firebase_ai/ios/firebase_ai/Sources/firebase_ai/FirebaseAiPlugin.swift rename to packages/firebase_ai/firebase_ai/ios/firebase_ai/Sources/firebase_ai/FirebaseAIPlugin.swift index 7bc1588947bf..a76212182bda 100644 --- a/packages/firebase_ai/firebase_ai/ios/firebase_ai/Sources/firebase_ai/FirebaseAiPlugin.swift +++ b/packages/firebase_ai/firebase_ai/ios/firebase_ai/Sources/firebase_ai/FirebaseAIPlugin.swift @@ -1,4 +1,4 @@ -// Copyright 2025 Google LLC +// Copyright 2026 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ import Flutter #endif -public class FirebaseAiPlugin: NSObject, FlutterPlugin { +public class FirebaseAIPlugin: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { #if canImport(FlutterMacOS) let messenger = registrar.messenger @@ -30,7 +30,7 @@ public class FirebaseAiPlugin: NSObject, FlutterPlugin { name: "plugins.flutter.io/firebase_ai", binaryMessenger: messenger ) - let instance = FirebaseAiPlugin() + let instance = FirebaseAIPlugin() registrar.addMethodCallDelegate(instance, channel: channel) } diff --git a/packages/firebase_ai/firebase_ai/lib/src/platform_header_helper.dart b/packages/firebase_ai/firebase_ai/lib/src/platform_header_helper.dart index 9a7b159ff416..9f0d115756de 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/platform_header_helper.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/platform_header_helper.dart @@ -1,4 +1,4 @@ -// Copyright 2025 Google LLC +// Copyright 2026 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/packages/firebase_ai/firebase_ai/macos/firebase_ai/Sources/firebase_ai/FirebaseAIPlugin.swift b/packages/firebase_ai/firebase_ai/macos/firebase_ai/Sources/firebase_ai/FirebaseAIPlugin.swift new file mode 120000 index 000000000000..d0761e47c393 --- /dev/null +++ b/packages/firebase_ai/firebase_ai/macos/firebase_ai/Sources/firebase_ai/FirebaseAIPlugin.swift @@ -0,0 +1 @@ +../../../../ios/firebase_ai/Sources/firebase_ai/FirebaseAIPlugin.swift \ No newline at end of file diff --git a/packages/firebase_ai/firebase_ai/macos/firebase_ai/Sources/firebase_ai/FirebaseAiPlugin.swift b/packages/firebase_ai/firebase_ai/macos/firebase_ai/Sources/firebase_ai/FirebaseAiPlugin.swift deleted file mode 120000 index e93eb2a83545..000000000000 --- a/packages/firebase_ai/firebase_ai/macos/firebase_ai/Sources/firebase_ai/FirebaseAiPlugin.swift +++ /dev/null @@ -1 +0,0 @@ -../../../../ios/firebase_ai/Sources/firebase_ai/FirebaseAiPlugin.swift \ No newline at end of file diff --git a/packages/firebase_ai/firebase_ai/pubspec.yaml b/packages/firebase_ai/firebase_ai/pubspec.yaml index ebf19451aff8..0304dac61456 100644 --- a/packages/firebase_ai/firebase_ai/pubspec.yaml +++ b/packages/firebase_ai/firebase_ai/pubspec.yaml @@ -43,8 +43,8 @@ flutter: platforms: android: package: io.flutter.plugins.firebase.ai - pluginClass: FirebaseAiPlugin + pluginClass: FirebaseAIPlugin ios: - pluginClass: FirebaseAiPlugin + pluginClass: FirebaseAIPlugin macos: - pluginClass: FirebaseAiPlugin + pluginClass: FirebaseAIPlugin diff --git a/packages/firebase_ai/firebase_ai/test/platform_header_helper_test.dart b/packages/firebase_ai/firebase_ai/test/platform_header_helper_test.dart index 23f5f27e8df9..442b56c95577 100644 --- a/packages/firebase_ai/firebase_ai/test/platform_header_helper_test.dart +++ b/packages/firebase_ai/firebase_ai/test/platform_header_helper_test.dart @@ -1,4 +1,4 @@ -// Copyright 2025 Google LLC +// Copyright 2026 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/tests/integration_test/firebase_ai/firebase_ai_e2e_test.dart b/tests/integration_test/firebase_ai/firebase_ai_e2e_test.dart index fdbf69c13716..75c7d67f7720 100644 --- a/tests/integration_test/firebase_ai/firebase_ai_e2e_test.dart +++ b/tests/integration_test/firebase_ai/firebase_ai_e2e_test.dart @@ -1,4 +1,4 @@ -// Copyright 2025, the Chromium project authors. Please see the AUTHORS file +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. @@ -43,6 +43,7 @@ void main() { 'getPlatformHeaders', ); + expect(headers, isNotNull); expect(headers, contains('X-Android-Package')); expect( headers!['X-Android-Package'], @@ -64,6 +65,7 @@ void main() { 'getPlatformHeaders', ); + expect(headers, isNotNull); expect(headers, contains('x-ios-bundle-identifier')); expect( headers!['x-ios-bundle-identifier'],