diff --git a/melos.yaml b/melos.yaml index a04e3758f5e5..8fe40bdfc22d 100644 --- a/melos.yaml +++ b/melos.yaml @@ -290,6 +290,10 @@ scripts: --ignore "**/generated/**" \ --ignore "**/flutter/generated_plugin_registrant.h" \ --ignore "**/flutter/generated_plugin_registrant.cc" \ + --ignore "**/android/app/build.gradle.kts" \ + --ignore "**/android/build.gradle.kts" \ + --ignore "**/android/settings.gradle.kts" \ + --ignore "**/RunnerTests/RunnerTests.swift" \ . description: Add a license header to all necessary files. @@ -326,6 +330,10 @@ scripts: --ignore "**/generated/**" \ --ignore "**/flutter/generated_plugin_registrant.h" \ --ignore "**/flutter/generated_plugin_registrant.cc" \ + --ignore "**/android/app/build.gradle.kts" \ + --ignore "**/android/build.gradle.kts" \ + --ignore "**/android/settings.gradle.kts" \ + --ignore "**/RunnerTests/RunnerTests.swift" \ . description: Add a license header to all necessary files. diff --git a/packages/firebase_ai/firebase_ai/example/lib/pages/bidi_page.dart b/packages/firebase_ai/firebase_ai/example/lib/pages/bidi_page.dart index acd936a7e827..cb221329d28f 100644 --- a/packages/firebase_ai/firebase_ai/example/lib/pages/bidi_page.dart +++ b/packages/firebase_ai/firebase_ai/example/lib/pages/bidi_page.dart @@ -11,15 +11,15 @@ // 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 'dart:typed_data'; import 'dart:async'; -import 'dart:developer'; +import 'dart:developer' as developer; import 'package:flutter/material.dart'; import 'package:firebase_ai/firebase_ai.dart'; + +import '../utils/audio_input.dart'; +import '../utils/audio_output.dart'; import '../widgets/message_widget.dart'; -import '../utils/audio_player.dart'; -import '../utils/audio_recorder.dart'; class BidiPage extends StatefulWidget { const BidiPage({super.key, required this.title, required this.model}); @@ -48,11 +48,9 @@ class _BidiPageState extends State { bool _recording = false; late LiveGenerativeModel _liveModel; late LiveSession _session; - final _audioManager = AudioStreamManager(); - final _audioRecorder = InMemoryAudioRecorder(); - var _chunkBuilder = BytesBuilder(); - var _audioIndex = 0; StreamController _stopController = StreamController(); + final AudioOutput _audioOutput = AudioOutput(); + final AudioInput _audioInput = AudioInput(); @override void initState() { @@ -65,6 +63,7 @@ class _BidiPageState extends State { ], ); + // ignore: deprecated_member_use _liveModel = FirebaseAI.vertexAI().liveGenerativeModel( model: 'gemini-2.0-flash-exp', liveGenerationConfig: config, @@ -72,6 +71,12 @@ class _BidiPageState extends State { Tool.functionDeclarations([lightControlTool]), ], ); + _initAudio(); + } + + Future _initAudio() async { + await _audioOutput.init(); + await _audioInput.init(); } void _scrollDown() { @@ -89,13 +94,7 @@ class _BidiPageState extends State { @override void dispose() { if (_sessionOpening) { - _audioManager.stopAudioPlayer(); - _audioManager.disposeAudioPlayer(); - - _audioRecorder.stopRecording(); - _stopController.close(); - _sessionOpening = false; _session.close(); } @@ -234,7 +233,7 @@ class _BidiPageState extends State { _sessionOpening = true; _stopController = StreamController(); unawaited( - processMessagesContinuously( + _processMessagesContinuously( stopSignal: _stopController, ), ); @@ -243,8 +242,6 @@ class _BidiPageState extends State { await _stopController.close(); await _session.close(); - await _audioManager.stopAudioPlayer(); - await _audioManager.disposeAudioPlayer(); _sessionOpening = false; } @@ -258,21 +255,25 @@ class _BidiPageState extends State { _recording = true; }); try { - await _audioRecorder.checkPermission(); - final audioRecordStream = _audioRecorder.startRecordingStream(); + var inputStream = await _audioInput.startRecordingStream(); + await _audioOutput.playStream(); // Map the Uint8List stream to InlineDataPart stream - final mediaChunkStream = audioRecordStream.map((data) { - return InlineDataPart('audio/pcm', data); - }); - await _session.sendMediaStream(mediaChunkStream); + if (inputStream != null) { + final inlineDataStream = inputStream.map((data) { + return InlineDataPart('audio/pcm', data); + }); + + await _session.sendMediaStream(inlineDataStream); + } } catch (e) { + developer.log(e.toString()); _showError(e.toString()); } } Future _stopRecording() async { try { - await _audioRecorder.stopRecording(); + await _audioInput.stopRecording(); } catch (e) { _showError(e.toString()); } @@ -298,7 +299,7 @@ class _BidiPageState extends State { }); } - Future processMessagesContinuously({ + Future _processMessagesContinuously({ required StreamController stopSignal, }) async { bool shouldContinue = true; @@ -335,11 +336,8 @@ class _BidiPageState extends State { if (message.modelTurn != null) { await _handleLiveServerContent(message); } - if (message.turnComplete != null && message.turnComplete!) { - await _handleTurnComplete(); - } if (message.interrupted != null && message.interrupted!) { - log('Interrupted: $response'); + developer.log('Interrupted: $response'); } } else if (message is LiveServerToolCall && message.functionCalls != null) { await _handleLiveServerToolCall(message); @@ -355,7 +353,7 @@ class _BidiPageState extends State { } else if (part is InlineDataPart) { await _handleInlineDataPart(part); } else { - log('receive part with type ${part.runtimeType}'); + developer.log('receive part with type ${part.runtimeType}'); } } } @@ -376,29 +374,7 @@ class _BidiPageState extends State { Future _handleInlineDataPart(InlineDataPart part) async { if (part.mimeType.startsWith('audio')) { - _chunkBuilder.add(part.bytes); - _audioIndex++; - if (_audioIndex == 15) { - Uint8List chunk = await audioChunkWithHeader( - _chunkBuilder.toBytes(), - 24000, - ); - _audioManager.addAudio(chunk); - _chunkBuilder.clear(); - _audioIndex = 0; - } - } - } - - Future _handleTurnComplete() async { - if (_chunkBuilder.isNotEmpty) { - Uint8List chunk = await audioChunkWithHeader( - _chunkBuilder.toBytes(), - 24000, - ); - _audioManager.addAudio(chunk); - _audioIndex = 0; - _chunkBuilder.clear(); + _audioOutput.addAudioStream(part.bytes); } } diff --git a/packages/firebase_ai/firebase_ai/example/lib/utils/audio_input.dart b/packages/firebase_ai/firebase_ai/example/lib/utils/audio_input.dart new file mode 100644 index 000000000000..869d4ee32781 --- /dev/null +++ b/packages/firebase_ai/firebase_ai/example/lib/utils/audio_input.dart @@ -0,0 +1,95 @@ +// 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/material.dart'; +import 'package:record/record.dart'; +import 'dart:typed_data'; + +class AudioInput extends ChangeNotifier { + final _recorder = AudioRecorder(); + final AudioEncoder _encoder = AudioEncoder.pcm16bits; + bool isRecording = false; + bool isPaused = false; + Stream? audioStream; + + Future init() async { + await _checkPermission(); + } + + @override + void dispose() { + _recorder.dispose(); + super.dispose(); + } + + Future _checkPermission() async { + final hasPermission = await _recorder.hasPermission(); + if (!hasPermission) { + throw MicrophonePermissionDeniedException( + 'App does not have mic permissions', + ); + } + } + + Future?> startRecordingStream() async { + var recordConfig = RecordConfig( + encoder: _encoder, + sampleRate: 24000, + numChannels: 1, + echoCancel: true, + noiseSuppress: true, + androidConfig: const AndroidRecordConfig( + audioSource: AndroidAudioSource.voiceCommunication, + ), + iosConfig: const IosRecordConfig(categoryOptions: []), + ); + await _recorder.listInputDevices(); + audioStream = await _recorder.startStream(recordConfig); + isRecording = true; + notifyListeners(); + return audioStream; + } + + Future stopRecording() async { + await _recorder.stop(); + isRecording = false; + notifyListeners(); + } + + Future togglePause() async { + if (isPaused) { + await _recorder.resume(); + isPaused = false; + } else { + await _recorder.pause(); + isPaused = true; + } + notifyListeners(); + return; + } +} + +/// An exception thrown when microphone permission is denied or not granted. +class MicrophonePermissionDeniedException implements Exception { + /// The optional message associated with the permission denial. + final String? message; + + /// Creates a new [MicrophonePermissionDeniedException] with an optional [message]. + MicrophonePermissionDeniedException([this.message]); + + @override + String toString() { + return 'MicrophonePermissionDeniedException: $message'; + } +} diff --git a/packages/firebase_ai/firebase_ai/example/lib/utils/audio_output.dart b/packages/firebase_ai/firebase_ai/example/lib/utils/audio_output.dart new file mode 100644 index 000000000000..b97ad3478f5b --- /dev/null +++ b/packages/firebase_ai/firebase_ai/example/lib/utils/audio_output.dart @@ -0,0 +1,66 @@ +// 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 'dart:typed_data'; + +import 'package:flutter_soloud/flutter_soloud.dart'; + +class AudioOutput { + AudioSource? stream; + SoundHandle? handle; + + Future init() async { + // Initialize the player. + await SoLoud.instance.init(sampleRate: 24000, channels: Channels.mono); + await setupNewStream(); + } + + Future setupNewStream() async { + if (SoLoud.instance.isInitialized) { + // Stop and clear any previous playback handle if it's still valid + await stopStream(); // Ensure previous sound is stopped + + stream = SoLoud.instance.setBufferStream( + maxBufferSizeBytes: + 1024 * 1024 * 10, // 10MB of max buffer (not allocated) + bufferingType: BufferingType.released, + bufferingTimeNeeds: 0, + onBuffering: (isBuffering, handle, time) {}, + ); + // Reset handle to null until the stream is played again + handle = null; + } + } + + Future playStream() async { + handle = await SoLoud.instance.play(stream!); + return stream; + } + + Future stopStream() async { + if (stream != null && + handle != null && + SoLoud.instance.getIsValidVoiceHandle(handle!)) { + SoLoud.instance.setDataIsEnded(stream!); + await SoLoud.instance.stop(handle!); + + // Clear old stream, set up new session for next time. + await setupNewStream(); + } + } + + void addAudioStream(Uint8List audioChunk) { + SoLoud.instance.addAudioDataStream(stream!, audioChunk); + } +} diff --git a/packages/firebase_ai/firebase_ai/example/lib/utils/audio_player.dart b/packages/firebase_ai/firebase_ai/example/lib/utils/audio_player.dart deleted file mode 100644 index 3c5559481ed7..000000000000 --- a/packages/firebase_ai/firebase_ai/example/lib/utils/audio_player.dart +++ /dev/null @@ -1,143 +0,0 @@ -// 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 'dart:typed_data'; -import 'dart:async'; - -import 'package:just_audio/just_audio.dart'; - -/// Creates a WAV audio chunk with a properly formatted header. -Future audioChunkWithHeader( - List data, - int sampleRate, -) async { - var channels = 1; - - int byteRate = ((16 * sampleRate * channels) / 8).round(); - - var size = data.length; - var fileSize = size + 36; - - Uint8List header = Uint8List.fromList([ - // "RIFF" - 82, 73, 70, 70, - fileSize & 0xff, - (fileSize >> 8) & 0xff, - (fileSize >> 16) & 0xff, - (fileSize >> 24) & 0xff, - // WAVE - 87, 65, 86, 69, - // fmt - 102, 109, 116, 32, - // fmt chunk size 16 - 16, 0, 0, 0, - // Type of format - 1, 0, - // One channel - channels, 0, - // Sample rate - sampleRate & 0xff, - (sampleRate >> 8) & 0xff, - (sampleRate >> 16) & 0xff, - (sampleRate >> 24) & 0xff, - // Byte rate - byteRate & 0xff, - (byteRate >> 8) & 0xff, - (byteRate >> 16) & 0xff, - (byteRate >> 24) & 0xff, - // Uhm - ((16 * channels) / 8).round(), 0, - // bitsize - 16, 0, - // "data" - 100, 97, 116, 97, - size & 0xff, - (size >> 8) & 0xff, - (size >> 16) & 0xff, - (size >> 24) & 0xff, - // incoming data - ...data, - ]); - return header; -} - -class ByteStreamAudioSource extends StreamAudioSource { - ByteStreamAudioSource(this.bytes) : super(tag: 'Byte Stream Audio'); - - final Uint8List bytes; - - @override - Future request([int? start, int? end]) async { - start ??= 0; - end ??= bytes.length; - return StreamAudioResponse( - sourceLength: bytes.length, - contentLength: end - start, - offset: start, - stream: Stream.value(bytes.sublist(start, end)), - contentType: 'audio/wav', // Or the appropriate content type - ); - } -} - -class AudioStreamManager { - final _audioPlayer = AudioPlayer(); - final _audioChunkController = StreamController(); - var _audioSource = ConcatenatingAudioSource( - children: [], - ); - - AudioStreamManager() { - _initAudioPlayer(); - } - - Future _initAudioPlayer() async { - // 1. Create a ConcatenatingAudioSource to handle the stream - await _audioPlayer.setAudioSource(_audioSource); - - // 2. Listen to the stream of audio chunks - _audioChunkController.stream.listen(_addAudioChunk); - - await _audioPlayer.play(); // Start playing (even if initially empty) - - _audioPlayer.processingStateStream.listen((state) async { - if (state == ProcessingState.completed) { - await _audioPlayer - .pause(); // Or player.stop() if you want to release resources - await _audioPlayer.seek(Duration.zero, index: 0); - await _audioSource.clear(); - await _audioPlayer.play(); - } - }); - } - - Future _addAudioChunk(Uint8List chunk) async { - var buffer = ByteStreamAudioSource(chunk); - - await _audioSource.add(buffer); - } - - void addAudio(Uint8List chunk) { - _audioChunkController.add(chunk); - } - - Future stopAudioPlayer() async { - await _audioPlayer.stop(); - } - - Future disposeAudioPlayer() async { - await _audioPlayer.dispose(); - await _audioChunkController.close(); - } -} diff --git a/packages/firebase_ai/firebase_ai/example/lib/utils/audio_recorder.dart b/packages/firebase_ai/firebase_ai/example/lib/utils/audio_recorder.dart deleted file mode 100644 index 1f3710cd0c8f..000000000000 --- a/packages/firebase_ai/firebase_ai/example/lib/utils/audio_recorder.dart +++ /dev/null @@ -1,245 +0,0 @@ -// 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 'dart:async'; -import 'dart:io'; -import 'dart:typed_data'; - -import 'package:flutter/material.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:record/record.dart'; - -/// An exception thrown when microphone permission is denied or not granted. -class MicrophonePermissionDeniedException implements Exception { - /// The optional message associated with the permission denial. - final String? message; - - /// Creates a new [MicrophonePermissionDeniedException] with an optional [message]. - MicrophonePermissionDeniedException([this.message]); - - @override - String toString() { - if (message == null) { - return 'MicrophonePermissionDeniedException'; - } - return 'MicrophonePermissionDeniedException: $message'; - } -} - -class Resampler { - /// Resamples 16-bit integer PCM audio data from a source sample rate to a - /// target sample rate using linear interpolation. - /// - /// [sourceRate]: The sample rate of the input audio data. - /// [targetRate]: The desired sample rate of the output audio data. - /// [input]: The input audio data as a Uint8List containing 16-bit PCM samples. - /// - /// Returns a new Uint8List containing 16-bit PCM samples resampled to the - /// target rate. - static Uint8List resampleLinear16( - int sourceRate, - int targetRate, - Uint8List input, - ) { - if (sourceRate == targetRate) return input; // No resampling needed - - final outputLength = (input.length * targetRate / sourceRate).round(); - final output = Uint8List(outputLength); - final inputData = Int16List.view(input.buffer); - final outputData = Int16List.view(output.buffer); - - for (int i = 0; i < outputLength ~/ 2; i++) { - final sourcePosition = i * sourceRate / targetRate; - final index1 = sourcePosition.floor(); - final index2 = index1 + 1; - final weight2 = sourcePosition - index1; - final weight1 = 1.0 - weight2; - - // Ensure indices are within the valid range - final sample1 = inputData[index1.clamp(0, inputData.length - 1)]; - final sample2 = inputData[index2.clamp(0, inputData.length - 1)]; - - // Interpolate and convert back to 16-bit integer - final interpolatedSample = - (sample1 * weight1 + sample2 * weight2).toInt(); - - outputData[i] = interpolatedSample; - } - - return output; - } -} - -class InMemoryAudioRecorder { - final _audioChunks = []; - final _recorder = AudioRecorder(); - StreamSubscription? _recordSubscription; - late String? _lastAudioPath; - AudioEncoder _encoder = AudioEncoder.pcm16bits; - - Future _getPath() async { - String suffix; - if (_encoder == AudioEncoder.pcm16bits) { - suffix = 'pcm'; - } else if (_encoder == AudioEncoder.aacLc) { - suffix = 'm4a'; - } else { - suffix = 'wav'; - } - final dir = await getDownloadsDirectory(); - final path = - '${dir!.path}/audio_${DateTime.now().millisecondsSinceEpoch}.$suffix'; - return path; - } - - Future checkPermission() async { - final hasPermission = await _recorder.hasPermission(); - if (!hasPermission) { - throw MicrophonePermissionDeniedException('Not having mic permission'); - } - } - - Future _isEncoderSupported(AudioEncoder encoder) async { - final isSupported = await _recorder.isEncoderSupported( - encoder, - ); - - if (!isSupported) { - debugPrint('${encoder.name} is not supported on this platform.'); - debugPrint('Supported encoders are:'); - - for (final e in AudioEncoder.values) { - if (await _recorder.isEncoderSupported(e)) { - debugPrint('- ${e.name}'); - } - } - } - - return isSupported; - } - - Future startRecording({bool fromFile = false}) async { - if (!await _isEncoderSupported(_encoder)) { - return; - } - var recordConfig = RecordConfig( - encoder: _encoder, - sampleRate: 16000, - numChannels: 1, - androidConfig: const AndroidRecordConfig( - muteAudio: true, - audioSource: AndroidAudioSource.mic, - ), - ); - final devs = await _recorder.listInputDevices(); - debugPrint(devs.toString()); - _lastAudioPath = await _getPath(); - if (fromFile) { - await _recorder.start(recordConfig, path: _lastAudioPath!); - } else { - final stream = await _recorder.startStream(recordConfig); - _recordSubscription = stream.listen(_audioChunks.add); - } - } - - Future startRecordingFile() async { - if (!await _isEncoderSupported(_encoder)) { - return; - } - var recordConfig = RecordConfig( - encoder: _encoder, - sampleRate: 16000, - numChannels: 1, - ); - final devs = await _recorder.listInputDevices(); - debugPrint(devs.toString()); - _lastAudioPath = await _getPath(); - await _recorder.start(recordConfig, path: _lastAudioPath!); - } - - Stream startRecordingStream() async* { - if (!await _isEncoderSupported(_encoder)) { - return; - } - var recordConfig = RecordConfig( - encoder: _encoder, - sampleRate: 16000, - numChannels: 1, - ); - final devices = await _recorder.listInputDevices(); - debugPrint(devices.toString()); - final stream = await _recorder.startStream(recordConfig); - - await for (final data in stream) { - yield data; - } - } - - Future stopRecording() async { - await _recordSubscription?.cancel(); - _recordSubscription = null; - - await _recorder.stop(); - } - - Future fetchAudioBytes({ - bool fromFile = false, - bool removeHeader = false, - }) async { - Uint8List resultBytes; - if (fromFile) { - resultBytes = await _getAudioBytesFromFile(_lastAudioPath!); - } else { - final builder = BytesBuilder(); - _audioChunks.forEach(builder.add); - resultBytes = builder.toBytes(); - } - - // resample - resultBytes = Resampler.resampleLinear16(44100, 16000, resultBytes); - final dir = await getDownloadsDirectory(); - final path = '${dir!.path}/audio_resampled.pcm'; - final file = File(path); - final sink = file.openWrite(); - - sink.add(resultBytes); - - await sink.close(); - return resultBytes; - } - - Future _removeWavHeader(Uint8List audio) async { - // Assuming a standard WAV header size of 44 bytes - const wavHeaderSize = 44; - final audioData = audio.sublist(wavHeaderSize); - return audioData; - } - - Future _getAudioBytesFromFile( - String filePath, { - bool removeHeader = false, - }) async { - final file = File(_lastAudioPath!); - - if (!file.existsSync()) { - throw Exception('Audio file not found: ${file.path}'); - } - - var pcmBytes = await file.readAsBytes(); - if (removeHeader) { - pcmBytes = await _removeWavHeader(pcmBytes); - } - return pcmBytes; - } -} diff --git a/packages/firebase_ai/firebase_ai/example/pubspec.yaml b/packages/firebase_ai/firebase_ai/example/pubspec.yaml index 21b1fa04272d..4868f106d648 100644 --- a/packages/firebase_ai/firebase_ai/example/pubspec.yaml +++ b/packages/firebase_ai/firebase_ai/example/pubspec.yaml @@ -26,7 +26,7 @@ dependencies: flutter: sdk: flutter flutter_markdown: ^0.6.20 - just_audio: ^0.9.43 + flutter_soloud: ^3.1.6 path_provider: ^2.1.5 record: ^5.2.1 diff --git a/packages/firebase_vertexai/firebase_vertexai/example/.metadata b/packages/firebase_vertexai/firebase_vertexai/example/.metadata index 784ce1298249..e8f7bf911cbc 100644 --- a/packages/firebase_vertexai/firebase_vertexai/example/.metadata +++ b/packages/firebase_vertexai/firebase_vertexai/example/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "a14f74ff3a1cbd521163c5f03d68113d50af93d3" + revision: "ea121f8859e4b13e47a8f845e4586164519588bc" channel: "stable" project_type: app @@ -13,11 +13,26 @@ project_type: app migration: platforms: - platform: root - create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 - base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 + create_revision: ea121f8859e4b13e47a8f845e4586164519588bc + base_revision: ea121f8859e4b13e47a8f845e4586164519588bc + - platform: android + create_revision: ea121f8859e4b13e47a8f845e4586164519588bc + base_revision: ea121f8859e4b13e47a8f845e4586164519588bc + - platform: ios + create_revision: ea121f8859e4b13e47a8f845e4586164519588bc + base_revision: ea121f8859e4b13e47a8f845e4586164519588bc + - platform: linux + create_revision: ea121f8859e4b13e47a8f845e4586164519588bc + base_revision: ea121f8859e4b13e47a8f845e4586164519588bc + - platform: macos + create_revision: ea121f8859e4b13e47a8f845e4586164519588bc + base_revision: ea121f8859e4b13e47a8f845e4586164519588bc - platform: web - create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 - base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 + create_revision: ea121f8859e4b13e47a8f845e4586164519588bc + base_revision: ea121f8859e4b13e47a8f845e4586164519588bc + - platform: windows + create_revision: ea121f8859e4b13e47a8f845e4586164519588bc + base_revision: ea121f8859e4b13e47a8f845e4586164519588bc # User provided section diff --git a/packages/firebase_vertexai/firebase_vertexai/example/android/.gitignore b/packages/firebase_vertexai/firebase_vertexai/example/android/.gitignore index 6f568019d3c6..be3943c96d8e 100644 --- a/packages/firebase_vertexai/firebase_vertexai/example/android/.gitignore +++ b/packages/firebase_vertexai/firebase_vertexai/example/android/.gitignore @@ -5,9 +5,10 @@ gradle-wrapper.jar /gradlew.bat /local.properties GeneratedPluginRegistrant.java +.cxx/ # Remember to never publicly share your keystore. -# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +# See https://flutter.dev/to/reference-keystore key.properties **/*.keystore **/*.jks diff --git a/packages/firebase_vertexai/firebase_vertexai/example/android/app/build.gradle.kts b/packages/firebase_vertexai/firebase_vertexai/example/android/app/build.gradle.kts new file mode 100644 index 000000000000..3415989fde7e --- /dev/null +++ b/packages/firebase_vertexai/firebase_vertexai/example/android/app/build.gradle.kts @@ -0,0 +1,47 @@ +plugins { + id("com.android.application") + // START: FlutterFire Configuration + id("com.google.gms.google-services") + // END: FlutterFire Configuration + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.vertex_ai_example" + compileSdk = flutter.compileSdkVersion + ndkVersion = "27.0.12077973" + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.vertex_ai_example" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = 23 + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/packages/firebase_vertexai/firebase_vertexai/example/android/app/src/debug/AndroidManifest.xml b/packages/firebase_vertexai/firebase_vertexai/example/android/app/src/debug/AndroidManifest.xml index 399f6981d5d3..d30de11e3410 100644 --- a/packages/firebase_vertexai/firebase_vertexai/example/android/app/src/debug/AndroidManifest.xml +++ b/packages/firebase_vertexai/firebase_vertexai/example/android/app/src/debug/AndroidManifest.xml @@ -4,4 +4,5 @@ to allow setting breakpoints, to provide hot reload, etc. --> + diff --git a/packages/firebase_vertexai/firebase_vertexai/example/android/app/src/main/AndroidManifest.xml b/packages/firebase_vertexai/firebase_vertexai/example/android/app/src/main/AndroidManifest.xml index 3401fcfb42b9..48622205141f 100644 --- a/packages/firebase_vertexai/firebase_vertexai/example/android/app/src/main/AndroidManifest.xml +++ b/packages/firebase_vertexai/firebase_vertexai/example/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ @@ -8,6 +8,7 @@ android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" + android:taskAffinity="" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" @@ -32,7 +33,7 @@ android:value="2" /> diff --git a/packages/firebase_vertexai/firebase_vertexai/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt b/packages/firebase_vertexai/firebase_vertexai/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt deleted file mode 100644 index 70f8f08f2479..000000000000 --- a/packages/firebase_vertexai/firebase_vertexai/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.example - -import io.flutter.embedding.android.FlutterActivity - -class MainActivity: FlutterActivity() diff --git a/packages/firebase_vertexai/firebase_vertexai/example/android/app/src/main/kotlin/com/example/vertex_ai_example/MainActivity.kt b/packages/firebase_vertexai/firebase_vertexai/example/android/app/src/main/kotlin/com/example/vertex_ai_example/MainActivity.kt new file mode 100644 index 000000000000..a09c414f7bc6 --- /dev/null +++ b/packages/firebase_vertexai/firebase_vertexai/example/android/app/src/main/kotlin/com/example/vertex_ai_example/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.vertex_ai_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/packages/firebase_vertexai/firebase_vertexai/example/android/build.gradle.kts b/packages/firebase_vertexai/firebase_vertexai/example/android/build.gradle.kts new file mode 100644 index 000000000000..89176ef44e8c --- /dev/null +++ b/packages/firebase_vertexai/firebase_vertexai/example/android/build.gradle.kts @@ -0,0 +1,21 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/packages/firebase_vertexai/firebase_vertexai/example/android/gradle.properties b/packages/firebase_vertexai/firebase_vertexai/example/android/gradle.properties index 598d13fee446..f018a61817f5 100644 --- a/packages/firebase_vertexai/firebase_vertexai/example/android/gradle.properties +++ b/packages/firebase_vertexai/firebase_vertexai/example/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx4G +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true diff --git a/packages/firebase_vertexai/firebase_vertexai/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/firebase_vertexai/firebase_vertexai/example/android/gradle/wrapper/gradle-wrapper.properties index aa49780cd59e..afa1e8eb0a83 100644 --- a/packages/firebase_vertexai/firebase_vertexai/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/firebase_vertexai/firebase_vertexai/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip diff --git a/packages/firebase_vertexai/firebase_vertexai/example/android/settings.gradle.kts b/packages/firebase_vertexai/firebase_vertexai/example/android/settings.gradle.kts new file mode 100644 index 000000000000..9e2d35ccf5e0 --- /dev/null +++ b/packages/firebase_vertexai/firebase_vertexai/example/android/settings.gradle.kts @@ -0,0 +1,28 @@ +pluginManagement { + val flutterSdkPath = run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.7.0" apply false + // START: FlutterFire Configuration + id("com.google.gms.google-services") version("4.3.15") apply false + // END: FlutterFire Configuration + id("org.jetbrains.kotlin.android") version "1.8.22" apply false +} + +include(":app") diff --git a/packages/firebase_vertexai/firebase_vertexai/example/ios/Podfile b/packages/firebase_vertexai/firebase_vertexai/example/ios/Podfile index e51a31d9ca9d..2dbf7d728d81 100644 --- a/packages/firebase_vertexai/firebase_vertexai/example/ios/Podfile +++ b/packages/firebase_vertexai/firebase_vertexai/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '13.0' +platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -29,7 +29,6 @@ flutter_ios_podfile_setup target 'Runner' do use_frameworks! - use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do diff --git a/packages/firebase_vertexai/firebase_vertexai/example/ios/Runner.xcodeproj/project.pbxproj b/packages/firebase_vertexai/firebase_vertexai/example/ios/Runner.xcodeproj/project.pbxproj index 98cd0c1ded4a..37f29d6208ac 100644 --- a/packages/firebase_vertexai/firebase_vertexai/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/firebase_vertexai/firebase_vertexai/example/ios/Runner.xcodeproj/project.pbxproj @@ -7,17 +7,15 @@ objects = { /* Begin PBXBuildFile section */ + 12DD27C70B6F1A3A29F606CA /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5EC8278BABD88B76D174C9B3 /* Pods_RunnerTests.framework */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; - 3414F5B6C6F086F6373F1948 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5F1FA05866A2D0FCA3287B20 /* GoogleService-Info.plist */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; - 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; - 901FEC83A38129064032C578 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 94CE5BFCDF90764354BB6740 /* Pods_Runner.framework */; }; + 7B483211B8F8447551559CD8 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A7451CAF0BF9B58B1FC94AC /* Pods_Runner.framework */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - B7B3CA2D70F15615E1B8E5D8 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 154D9627A1C14A5ACE0B7B0D /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -46,18 +44,18 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 154D9627A1C14A5ACE0B7B0D /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 232D95ECCEC6F04B9CEC8925 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 1B003FC08370C067F6112BA3 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 1E4EFC92E26DC42959308596 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 2DF9D5C450661BB71EE1CA4A /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 560CA017EC76D8AAE2E21549 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - 5F1FA05866A2D0FCA3287B20 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; + 5A7451CAF0BF9B58B1FC94AC /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5EC8278BABD88B76D174C9B3 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 8ACDC47C7E9AF1A1B9595598 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; - 94CE5BFCDF90764354BB6740 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8E74AA7A780E6AD093F2280C /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -65,26 +63,24 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - A85D07EF8959748E1D3E564B /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; - B0B22A9E291076BD22BA9F10 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; - E1D0571EA0792087F8F27457 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + B2BD865801978D1293EC9548 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + B5411DA55636D994211B15CD /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 0F5F3CD1ED7DB09B81C92173 /* Frameworks */ = { + 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - B7B3CA2D70F15615E1B8E5D8 /* Pods_RunnerTests.framework in Frameworks */, + 7B483211B8F8447551559CD8 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; - 97C146EB1CF9000F007C117D /* Frameworks */ = { + E0117E231D8F6E331F0AF95D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, - 901FEC83A38129064032C578 /* Pods_Runner.framework in Frameworks */, + 12DD27C70B6F1A3A29F606CA /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -99,19 +95,29 @@ path = RunnerTests; sourceTree = ""; }; - 3C3B3E8596675CC144D1BD5B /* Pods */ = { + 51AC52FF58548C49E2FD13CA /* Pods */ = { isa = PBXGroup; children = ( - E1D0571EA0792087F8F27457 /* Pods-Runner.debug.xcconfig */, - 232D95ECCEC6F04B9CEC8925 /* Pods-Runner.release.xcconfig */, - 560CA017EC76D8AAE2E21549 /* Pods-Runner.profile.xcconfig */, - A85D07EF8959748E1D3E564B /* Pods-RunnerTests.debug.xcconfig */, - 8ACDC47C7E9AF1A1B9595598 /* Pods-RunnerTests.release.xcconfig */, - B0B22A9E291076BD22BA9F10 /* Pods-RunnerTests.profile.xcconfig */, - ); + 2DF9D5C450661BB71EE1CA4A /* Pods-Runner.debug.xcconfig */, + B2BD865801978D1293EC9548 /* Pods-Runner.release.xcconfig */, + 1E4EFC92E26DC42959308596 /* Pods-Runner.profile.xcconfig */, + 1B003FC08370C067F6112BA3 /* Pods-RunnerTests.debug.xcconfig */, + 8E74AA7A780E6AD093F2280C /* Pods-RunnerTests.release.xcconfig */, + B5411DA55636D994211B15CD /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; path = Pods; sourceTree = ""; }; + 67A1388587063912C673254D /* Frameworks */ = { + isa = PBXGroup; + children = ( + 5A7451CAF0BF9B58B1FC94AC /* Pods_Runner.framework */, + 5EC8278BABD88B76D174C9B3 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -130,9 +136,8 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, - 5F1FA05866A2D0FCA3287B20 /* GoogleService-Info.plist */, - 3C3B3E8596675CC144D1BD5B /* Pods */, - A50BECFB61A452F592070BAA /* Frameworks */, + 51AC52FF58548C49E2FD13CA /* Pods */, + 67A1388587063912C673254D /* Frameworks */, ); sourceTree = ""; }; @@ -160,15 +165,6 @@ path = Runner; sourceTree = ""; }; - A50BECFB61A452F592070BAA /* Frameworks */ = { - isa = PBXGroup; - children = ( - 94CE5BFCDF90764354BB6740 /* Pods_Runner.framework */, - 154D9627A1C14A5ACE0B7B0D /* Pods_RunnerTests.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -176,10 +172,10 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - F5C7CFE0E232B64D613F0623 /* [CP] Check Pods Manifest.lock */, + 8580F0CE7F25830B9BDEF0A0 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, - 0F5F3CD1ED7DB09B81C92173 /* Frameworks */, + E0117E231D8F6E331F0AF95D /* Frameworks */, ); buildRules = ( ); @@ -195,23 +191,20 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - F51794D56D63ACA383D5C2E4 /* [CP] Check Pods Manifest.lock */, + 0C5779354B72E692610FCBAC /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 34F21DFC67109DEAFD936E80 /* [CP] Embed Pods Frameworks */, + 8F2729CA72CB997339394B4E /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( ); name = Runner; - packageProductDependencies = ( - 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, - ); productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; @@ -245,9 +238,6 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; - packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, - ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; @@ -274,28 +264,32 @@ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - 3414F5B6C6F086F6373F1948 /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 34F21DFC67109DEAFD936E80 /* [CP] Embed Pods Frameworks */ = { + 0C5779354B72E692610FCBAC /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Embed Pods Frameworks"; + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + 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; }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { @@ -314,64 +308,59 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 9740EEB61CF901F6004384FC /* Run Script */ = { + 8580F0CE7F25830B9BDEF0A0 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( ); - name = "Run Script"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + 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; }; - F51794D56D63ACA383D5C2E4 /* [CP] Check Pods Manifest.lock */ = { + 8F2729CA72CB997339394B4E /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; + name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); 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"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - F5C7CFE0E232B64D613F0623 /* [CP] Check Pods Manifest.lock */ = { + 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( ); + name = "Run Script"; outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-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; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; /* End PBXShellScriptBuildPhase section */ @@ -466,7 +455,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -482,14 +471,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = YYX2P3XVJ7; + DEVELOPMENT_TEAM = S8QB4VV633; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_BUNDLE_IDENTIFIER = com.example.vertexAiExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -499,14 +488,14 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = A85D07EF8959748E1D3E564B /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = 1B003FC08370C067F6112BA3 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.example.vertexAiExample.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -517,14 +506,14 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 8ACDC47C7E9AF1A1B9595598 /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = 8E74AA7A780E6AD093F2280C /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.example.vertexAiExample.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; @@ -533,14 +522,14 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = B0B22A9E291076BD22BA9F10 /* Pods-RunnerTests.profile.xcconfig */; + baseConfigurationReference = B5411DA55636D994211B15CD /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.example.vertexAiExample.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; @@ -596,7 +585,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -647,7 +636,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -665,14 +654,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = YYX2P3XVJ7; + DEVELOPMENT_TEAM = S8QB4VV633; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_BUNDLE_IDENTIFIER = com.example.vertexAiExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -688,14 +677,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = YYX2P3XVJ7; + DEVELOPMENT_TEAM = S8QB4VV633; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_BUNDLE_IDENTIFIER = com.example.vertexAiExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -737,20 +726,6 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ - -/* Begin XCLocalSwiftPackageReference section */ - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { - isa = XCLocalSwiftPackageReference; - relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; - }; -/* End XCLocalSwiftPackageReference section */ - -/* Begin XCSwiftPackageProductDependency section */ - 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { - isa = XCSwiftPackageProductDependency; - productName = FlutterGeneratedPluginSwiftPackage; - }; -/* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/packages/firebase_vertexai/firebase_vertexai/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/firebase_vertexai/firebase_vertexai/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 8178cd1c619c..15cada4838e2 100644 --- a/packages/firebase_vertexai/firebase_vertexai/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/firebase_vertexai/firebase_vertexai/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,24 +5,6 @@ - - - - - - - - - - - - - - CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - Example + Vertex Ai Example CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -13,7 +13,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - example + vertex_ai_example CFBundlePackageType APPL CFBundleShortVersionString @@ -46,6 +46,6 @@ UIApplicationSupportsIndirectInputEvents NSMicrophoneUsageDescription - We need access to the microphone to record audio. + Need microphone to talk with Gemini diff --git a/packages/firebase_vertexai/firebase_vertexai/example/ios/firebase_app_id_file.json b/packages/firebase_vertexai/firebase_vertexai/example/ios/firebase_app_id_file.json deleted file mode 100644 index 59a23a1a01cc..000000000000 --- a/packages/firebase_vertexai/firebase_vertexai/example/ios/firebase_app_id_file.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "file_generated_by": "FlutterFire CLI", - "purpose": "FirebaseAppID & ProjectID for this Firebase app in this directory", - "GOOGLE_APP_ID": "1:651313571784:ios:2f1472905da3e8e9b1c2fd", - "FIREBASE_PROJECT_ID": "vertex-ai-example-ef5a2", - "GCM_SENDER_ID": "651313571784" -} \ No newline at end of file diff --git a/packages/firebase_vertexai/firebase_vertexai/example/lib/pages/bidi_page.dart b/packages/firebase_vertexai/firebase_vertexai/example/lib/pages/bidi_page.dart index 4cd509d1257f..4d6cd0077c41 100644 --- a/packages/firebase_vertexai/firebase_vertexai/example/lib/pages/bidi_page.dart +++ b/packages/firebase_vertexai/firebase_vertexai/example/lib/pages/bidi_page.dart @@ -11,15 +11,15 @@ // 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 'dart:typed_data'; import 'dart:async'; -import 'dart:developer'; +import 'dart:developer' as developer; import 'package:flutter/material.dart'; import 'package:firebase_vertexai/firebase_vertexai.dart'; + +import '../utils/audio_input.dart'; +import '../utils/audio_output.dart'; import '../widgets/message_widget.dart'; -import '../utils/audio_player.dart'; -import '../utils/audio_recorder.dart'; class BidiPage extends StatefulWidget { const BidiPage({super.key, required this.title, required this.model}); @@ -48,11 +48,9 @@ class _BidiPageState extends State { bool _recording = false; late LiveGenerativeModel _liveModel; late LiveSession _session; - final _audioManager = AudioStreamManager(); - final _audioRecorder = InMemoryAudioRecorder(); - var _chunkBuilder = BytesBuilder(); - var _audioIndex = 0; StreamController _stopController = StreamController(); + final _audioOutput = AudioOutput(); + final _audioInput = AudioInput(); @override void initState() { @@ -73,6 +71,12 @@ class _BidiPageState extends State { Tool.functionDeclarations([lightControlTool]), ], ); + _initAudio(); + } + + Future _initAudio() async { + await _audioOutput.init(); + await _audioInput.init(); } void _scrollDown() { @@ -90,13 +94,7 @@ class _BidiPageState extends State { @override void dispose() { if (_sessionOpening) { - _audioManager.stopAudioPlayer(); - _audioManager.disposeAudioPlayer(); - - _audioRecorder.stopRecording(); - _stopController.close(); - _sessionOpening = false; _session.close(); } @@ -235,7 +233,7 @@ class _BidiPageState extends State { _sessionOpening = true; _stopController = StreamController(); unawaited( - processMessagesContinuously( + _processMessagesContinuously( stopSignal: _stopController, ), ); @@ -244,8 +242,6 @@ class _BidiPageState extends State { await _stopController.close(); await _session.close(); - await _audioManager.stopAudioPlayer(); - await _audioManager.disposeAudioPlayer(); _sessionOpening = false; } @@ -259,21 +255,25 @@ class _BidiPageState extends State { _recording = true; }); try { - await _audioRecorder.checkPermission(); - final audioRecordStream = _audioRecorder.startRecordingStream(); + var inputStream = await _audioInput.startRecordingStream(); + await _audioOutput.playStream(); // Map the Uint8List stream to InlineDataPart stream - final mediaChunkStream = audioRecordStream.map((data) { - return InlineDataPart('audio/pcm', data); - }); - await _session.sendMediaStream(mediaChunkStream); + if (inputStream != null) { + final inlineDataStream = inputStream.map((data) { + return InlineDataPart('audio/pcm', data); + }); + + await _session.sendMediaStream(inlineDataStream); + } } catch (e) { + developer.log(e.toString()); _showError(e.toString()); } } Future _stopRecording() async { try { - await _audioRecorder.stopRecording(); + await _audioInput.stopRecording(); } catch (e) { _showError(e.toString()); } @@ -299,7 +299,7 @@ class _BidiPageState extends State { }); } - Future processMessagesContinuously({ + Future _processMessagesContinuously({ required StreamController stopSignal, }) async { bool shouldContinue = true; @@ -336,11 +336,8 @@ class _BidiPageState extends State { if (message.modelTurn != null) { await _handleLiveServerContent(message); } - if (message.turnComplete != null && message.turnComplete!) { - await _handleTurnComplete(); - } if (message.interrupted != null && message.interrupted!) { - log('Interrupted: $response'); + developer.log('Interrupted: $response'); } } else if (message is LiveServerToolCall && message.functionCalls != null) { await _handleLiveServerToolCall(message); @@ -356,7 +353,7 @@ class _BidiPageState extends State { } else if (part is InlineDataPart) { await _handleInlineDataPart(part); } else { - log('receive part with type ${part.runtimeType}'); + developer.log('receive part with type ${part.runtimeType}'); } } } @@ -377,29 +374,7 @@ class _BidiPageState extends State { Future _handleInlineDataPart(InlineDataPart part) async { if (part.mimeType.startsWith('audio')) { - _chunkBuilder.add(part.bytes); - _audioIndex++; - if (_audioIndex == 15) { - Uint8List chunk = await audioChunkWithHeader( - _chunkBuilder.toBytes(), - 24000, - ); - _audioManager.addAudio(chunk); - _chunkBuilder.clear(); - _audioIndex = 0; - } - } - } - - Future _handleTurnComplete() async { - if (_chunkBuilder.isNotEmpty) { - Uint8List chunk = await audioChunkWithHeader( - _chunkBuilder.toBytes(), - 24000, - ); - _audioManager.addAudio(chunk); - _audioIndex = 0; - _chunkBuilder.clear(); + _audioOutput.addAudioStream(part.bytes); } } diff --git a/packages/firebase_vertexai/firebase_vertexai/example/lib/utils/audio_input.dart b/packages/firebase_vertexai/firebase_vertexai/example/lib/utils/audio_input.dart new file mode 100644 index 000000000000..869d4ee32781 --- /dev/null +++ b/packages/firebase_vertexai/firebase_vertexai/example/lib/utils/audio_input.dart @@ -0,0 +1,95 @@ +// 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/material.dart'; +import 'package:record/record.dart'; +import 'dart:typed_data'; + +class AudioInput extends ChangeNotifier { + final _recorder = AudioRecorder(); + final AudioEncoder _encoder = AudioEncoder.pcm16bits; + bool isRecording = false; + bool isPaused = false; + Stream? audioStream; + + Future init() async { + await _checkPermission(); + } + + @override + void dispose() { + _recorder.dispose(); + super.dispose(); + } + + Future _checkPermission() async { + final hasPermission = await _recorder.hasPermission(); + if (!hasPermission) { + throw MicrophonePermissionDeniedException( + 'App does not have mic permissions', + ); + } + } + + Future?> startRecordingStream() async { + var recordConfig = RecordConfig( + encoder: _encoder, + sampleRate: 24000, + numChannels: 1, + echoCancel: true, + noiseSuppress: true, + androidConfig: const AndroidRecordConfig( + audioSource: AndroidAudioSource.voiceCommunication, + ), + iosConfig: const IosRecordConfig(categoryOptions: []), + ); + await _recorder.listInputDevices(); + audioStream = await _recorder.startStream(recordConfig); + isRecording = true; + notifyListeners(); + return audioStream; + } + + Future stopRecording() async { + await _recorder.stop(); + isRecording = false; + notifyListeners(); + } + + Future togglePause() async { + if (isPaused) { + await _recorder.resume(); + isPaused = false; + } else { + await _recorder.pause(); + isPaused = true; + } + notifyListeners(); + return; + } +} + +/// An exception thrown when microphone permission is denied or not granted. +class MicrophonePermissionDeniedException implements Exception { + /// The optional message associated with the permission denial. + final String? message; + + /// Creates a new [MicrophonePermissionDeniedException] with an optional [message]. + MicrophonePermissionDeniedException([this.message]); + + @override + String toString() { + return 'MicrophonePermissionDeniedException: $message'; + } +} diff --git a/packages/firebase_vertexai/firebase_vertexai/example/lib/utils/audio_output.dart b/packages/firebase_vertexai/firebase_vertexai/example/lib/utils/audio_output.dart new file mode 100644 index 000000000000..b97ad3478f5b --- /dev/null +++ b/packages/firebase_vertexai/firebase_vertexai/example/lib/utils/audio_output.dart @@ -0,0 +1,66 @@ +// 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 'dart:typed_data'; + +import 'package:flutter_soloud/flutter_soloud.dart'; + +class AudioOutput { + AudioSource? stream; + SoundHandle? handle; + + Future init() async { + // Initialize the player. + await SoLoud.instance.init(sampleRate: 24000, channels: Channels.mono); + await setupNewStream(); + } + + Future setupNewStream() async { + if (SoLoud.instance.isInitialized) { + // Stop and clear any previous playback handle if it's still valid + await stopStream(); // Ensure previous sound is stopped + + stream = SoLoud.instance.setBufferStream( + maxBufferSizeBytes: + 1024 * 1024 * 10, // 10MB of max buffer (not allocated) + bufferingType: BufferingType.released, + bufferingTimeNeeds: 0, + onBuffering: (isBuffering, handle, time) {}, + ); + // Reset handle to null until the stream is played again + handle = null; + } + } + + Future playStream() async { + handle = await SoLoud.instance.play(stream!); + return stream; + } + + Future stopStream() async { + if (stream != null && + handle != null && + SoLoud.instance.getIsValidVoiceHandle(handle!)) { + SoLoud.instance.setDataIsEnded(stream!); + await SoLoud.instance.stop(handle!); + + // Clear old stream, set up new session for next time. + await setupNewStream(); + } + } + + void addAudioStream(Uint8List audioChunk) { + SoLoud.instance.addAudioDataStream(stream!, audioChunk); + } +} diff --git a/packages/firebase_vertexai/firebase_vertexai/example/lib/utils/audio_player.dart b/packages/firebase_vertexai/firebase_vertexai/example/lib/utils/audio_player.dart deleted file mode 100644 index 3c5559481ed7..000000000000 --- a/packages/firebase_vertexai/firebase_vertexai/example/lib/utils/audio_player.dart +++ /dev/null @@ -1,143 +0,0 @@ -// 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 'dart:typed_data'; -import 'dart:async'; - -import 'package:just_audio/just_audio.dart'; - -/// Creates a WAV audio chunk with a properly formatted header. -Future audioChunkWithHeader( - List data, - int sampleRate, -) async { - var channels = 1; - - int byteRate = ((16 * sampleRate * channels) / 8).round(); - - var size = data.length; - var fileSize = size + 36; - - Uint8List header = Uint8List.fromList([ - // "RIFF" - 82, 73, 70, 70, - fileSize & 0xff, - (fileSize >> 8) & 0xff, - (fileSize >> 16) & 0xff, - (fileSize >> 24) & 0xff, - // WAVE - 87, 65, 86, 69, - // fmt - 102, 109, 116, 32, - // fmt chunk size 16 - 16, 0, 0, 0, - // Type of format - 1, 0, - // One channel - channels, 0, - // Sample rate - sampleRate & 0xff, - (sampleRate >> 8) & 0xff, - (sampleRate >> 16) & 0xff, - (sampleRate >> 24) & 0xff, - // Byte rate - byteRate & 0xff, - (byteRate >> 8) & 0xff, - (byteRate >> 16) & 0xff, - (byteRate >> 24) & 0xff, - // Uhm - ((16 * channels) / 8).round(), 0, - // bitsize - 16, 0, - // "data" - 100, 97, 116, 97, - size & 0xff, - (size >> 8) & 0xff, - (size >> 16) & 0xff, - (size >> 24) & 0xff, - // incoming data - ...data, - ]); - return header; -} - -class ByteStreamAudioSource extends StreamAudioSource { - ByteStreamAudioSource(this.bytes) : super(tag: 'Byte Stream Audio'); - - final Uint8List bytes; - - @override - Future request([int? start, int? end]) async { - start ??= 0; - end ??= bytes.length; - return StreamAudioResponse( - sourceLength: bytes.length, - contentLength: end - start, - offset: start, - stream: Stream.value(bytes.sublist(start, end)), - contentType: 'audio/wav', // Or the appropriate content type - ); - } -} - -class AudioStreamManager { - final _audioPlayer = AudioPlayer(); - final _audioChunkController = StreamController(); - var _audioSource = ConcatenatingAudioSource( - children: [], - ); - - AudioStreamManager() { - _initAudioPlayer(); - } - - Future _initAudioPlayer() async { - // 1. Create a ConcatenatingAudioSource to handle the stream - await _audioPlayer.setAudioSource(_audioSource); - - // 2. Listen to the stream of audio chunks - _audioChunkController.stream.listen(_addAudioChunk); - - await _audioPlayer.play(); // Start playing (even if initially empty) - - _audioPlayer.processingStateStream.listen((state) async { - if (state == ProcessingState.completed) { - await _audioPlayer - .pause(); // Or player.stop() if you want to release resources - await _audioPlayer.seek(Duration.zero, index: 0); - await _audioSource.clear(); - await _audioPlayer.play(); - } - }); - } - - Future _addAudioChunk(Uint8List chunk) async { - var buffer = ByteStreamAudioSource(chunk); - - await _audioSource.add(buffer); - } - - void addAudio(Uint8List chunk) { - _audioChunkController.add(chunk); - } - - Future stopAudioPlayer() async { - await _audioPlayer.stop(); - } - - Future disposeAudioPlayer() async { - await _audioPlayer.dispose(); - await _audioChunkController.close(); - } -} diff --git a/packages/firebase_vertexai/firebase_vertexai/example/lib/utils/audio_recorder.dart b/packages/firebase_vertexai/firebase_vertexai/example/lib/utils/audio_recorder.dart deleted file mode 100644 index 1f3710cd0c8f..000000000000 --- a/packages/firebase_vertexai/firebase_vertexai/example/lib/utils/audio_recorder.dart +++ /dev/null @@ -1,245 +0,0 @@ -// 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 'dart:async'; -import 'dart:io'; -import 'dart:typed_data'; - -import 'package:flutter/material.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:record/record.dart'; - -/// An exception thrown when microphone permission is denied or not granted. -class MicrophonePermissionDeniedException implements Exception { - /// The optional message associated with the permission denial. - final String? message; - - /// Creates a new [MicrophonePermissionDeniedException] with an optional [message]. - MicrophonePermissionDeniedException([this.message]); - - @override - String toString() { - if (message == null) { - return 'MicrophonePermissionDeniedException'; - } - return 'MicrophonePermissionDeniedException: $message'; - } -} - -class Resampler { - /// Resamples 16-bit integer PCM audio data from a source sample rate to a - /// target sample rate using linear interpolation. - /// - /// [sourceRate]: The sample rate of the input audio data. - /// [targetRate]: The desired sample rate of the output audio data. - /// [input]: The input audio data as a Uint8List containing 16-bit PCM samples. - /// - /// Returns a new Uint8List containing 16-bit PCM samples resampled to the - /// target rate. - static Uint8List resampleLinear16( - int sourceRate, - int targetRate, - Uint8List input, - ) { - if (sourceRate == targetRate) return input; // No resampling needed - - final outputLength = (input.length * targetRate / sourceRate).round(); - final output = Uint8List(outputLength); - final inputData = Int16List.view(input.buffer); - final outputData = Int16List.view(output.buffer); - - for (int i = 0; i < outputLength ~/ 2; i++) { - final sourcePosition = i * sourceRate / targetRate; - final index1 = sourcePosition.floor(); - final index2 = index1 + 1; - final weight2 = sourcePosition - index1; - final weight1 = 1.0 - weight2; - - // Ensure indices are within the valid range - final sample1 = inputData[index1.clamp(0, inputData.length - 1)]; - final sample2 = inputData[index2.clamp(0, inputData.length - 1)]; - - // Interpolate and convert back to 16-bit integer - final interpolatedSample = - (sample1 * weight1 + sample2 * weight2).toInt(); - - outputData[i] = interpolatedSample; - } - - return output; - } -} - -class InMemoryAudioRecorder { - final _audioChunks = []; - final _recorder = AudioRecorder(); - StreamSubscription? _recordSubscription; - late String? _lastAudioPath; - AudioEncoder _encoder = AudioEncoder.pcm16bits; - - Future _getPath() async { - String suffix; - if (_encoder == AudioEncoder.pcm16bits) { - suffix = 'pcm'; - } else if (_encoder == AudioEncoder.aacLc) { - suffix = 'm4a'; - } else { - suffix = 'wav'; - } - final dir = await getDownloadsDirectory(); - final path = - '${dir!.path}/audio_${DateTime.now().millisecondsSinceEpoch}.$suffix'; - return path; - } - - Future checkPermission() async { - final hasPermission = await _recorder.hasPermission(); - if (!hasPermission) { - throw MicrophonePermissionDeniedException('Not having mic permission'); - } - } - - Future _isEncoderSupported(AudioEncoder encoder) async { - final isSupported = await _recorder.isEncoderSupported( - encoder, - ); - - if (!isSupported) { - debugPrint('${encoder.name} is not supported on this platform.'); - debugPrint('Supported encoders are:'); - - for (final e in AudioEncoder.values) { - if (await _recorder.isEncoderSupported(e)) { - debugPrint('- ${e.name}'); - } - } - } - - return isSupported; - } - - Future startRecording({bool fromFile = false}) async { - if (!await _isEncoderSupported(_encoder)) { - return; - } - var recordConfig = RecordConfig( - encoder: _encoder, - sampleRate: 16000, - numChannels: 1, - androidConfig: const AndroidRecordConfig( - muteAudio: true, - audioSource: AndroidAudioSource.mic, - ), - ); - final devs = await _recorder.listInputDevices(); - debugPrint(devs.toString()); - _lastAudioPath = await _getPath(); - if (fromFile) { - await _recorder.start(recordConfig, path: _lastAudioPath!); - } else { - final stream = await _recorder.startStream(recordConfig); - _recordSubscription = stream.listen(_audioChunks.add); - } - } - - Future startRecordingFile() async { - if (!await _isEncoderSupported(_encoder)) { - return; - } - var recordConfig = RecordConfig( - encoder: _encoder, - sampleRate: 16000, - numChannels: 1, - ); - final devs = await _recorder.listInputDevices(); - debugPrint(devs.toString()); - _lastAudioPath = await _getPath(); - await _recorder.start(recordConfig, path: _lastAudioPath!); - } - - Stream startRecordingStream() async* { - if (!await _isEncoderSupported(_encoder)) { - return; - } - var recordConfig = RecordConfig( - encoder: _encoder, - sampleRate: 16000, - numChannels: 1, - ); - final devices = await _recorder.listInputDevices(); - debugPrint(devices.toString()); - final stream = await _recorder.startStream(recordConfig); - - await for (final data in stream) { - yield data; - } - } - - Future stopRecording() async { - await _recordSubscription?.cancel(); - _recordSubscription = null; - - await _recorder.stop(); - } - - Future fetchAudioBytes({ - bool fromFile = false, - bool removeHeader = false, - }) async { - Uint8List resultBytes; - if (fromFile) { - resultBytes = await _getAudioBytesFromFile(_lastAudioPath!); - } else { - final builder = BytesBuilder(); - _audioChunks.forEach(builder.add); - resultBytes = builder.toBytes(); - } - - // resample - resultBytes = Resampler.resampleLinear16(44100, 16000, resultBytes); - final dir = await getDownloadsDirectory(); - final path = '${dir!.path}/audio_resampled.pcm'; - final file = File(path); - final sink = file.openWrite(); - - sink.add(resultBytes); - - await sink.close(); - return resultBytes; - } - - Future _removeWavHeader(Uint8List audio) async { - // Assuming a standard WAV header size of 44 bytes - const wavHeaderSize = 44; - final audioData = audio.sublist(wavHeaderSize); - return audioData; - } - - Future _getAudioBytesFromFile( - String filePath, { - bool removeHeader = false, - }) async { - final file = File(_lastAudioPath!); - - if (!file.existsSync()) { - throw Exception('Audio file not found: ${file.path}'); - } - - var pcmBytes = await file.readAsBytes(); - if (removeHeader) { - pcmBytes = await _removeWavHeader(pcmBytes); - } - return pcmBytes; - } -} diff --git a/packages/firebase_vertexai/firebase_vertexai/example/macos/Runner.xcodeproj/project.pbxproj b/packages/firebase_vertexai/firebase_vertexai/example/macos/Runner.xcodeproj/project.pbxproj index 4bc66a519ca5..47f1397e22f9 100644 --- a/packages/firebase_vertexai/firebase_vertexai/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/firebase_vertexai/firebase_vertexai/example/macos/Runner.xcodeproj/project.pbxproj @@ -198,7 +198,6 @@ 587C61AFC0E2B0BF5340F8E8 /* Pods-RunnerTests.release.xcconfig */, 5C2B5E4F1CE100E1FA5D9DC5 /* Pods-RunnerTests.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -584,6 +583,7 @@ "@executable_path/../Frameworks", ); PROVISIONING_PROFILE_SPECIFIER = ""; + STRIP_STYLE = "non-global"; SWIFT_VERSION = 5.0; }; name = Profile; @@ -716,6 +716,7 @@ "@executable_path/../Frameworks", ); PROVISIONING_PROFILE_SPECIFIER = ""; + STRIP_STYLE = "non-global"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; @@ -736,6 +737,7 @@ "@executable_path/../Frameworks", ); PROVISIONING_PROFILE_SPECIFIER = ""; + STRIP_STYLE = "non-global"; SWIFT_VERSION = 5.0; }; name = Release; diff --git a/packages/firebase_vertexai/firebase_vertexai/example/macos/Runner/DebugProfile.entitlements b/packages/firebase_vertexai/firebase_vertexai/example/macos/Runner/DebugProfile.entitlements index b4bd9ee174a1..d3eb4e3f2a11 100644 --- a/packages/firebase_vertexai/firebase_vertexai/example/macos/Runner/DebugProfile.entitlements +++ b/packages/firebase_vertexai/firebase_vertexai/example/macos/Runner/DebugProfile.entitlements @@ -14,5 +14,7 @@ com.apple.security.device.audio-input + com.apple.security.network.client + diff --git a/packages/firebase_vertexai/firebase_vertexai/example/macos/Runner/Release.entitlements b/packages/firebase_vertexai/firebase_vertexai/example/macos/Runner/Release.entitlements index 2f9659c917fb..f18debee72ff 100644 --- a/packages/firebase_vertexai/firebase_vertexai/example/macos/Runner/Release.entitlements +++ b/packages/firebase_vertexai/firebase_vertexai/example/macos/Runner/Release.entitlements @@ -6,5 +6,7 @@ com.apple.security.files.downloads.read-write + com.apple.security.network.client + diff --git a/packages/firebase_vertexai/firebase_vertexai/example/pubspec.yaml b/packages/firebase_vertexai/firebase_vertexai/example/pubspec.yaml index a043982716f2..7f62a9dd4a3b 100644 --- a/packages/firebase_vertexai/firebase_vertexai/example/pubspec.yaml +++ b/packages/firebase_vertexai/firebase_vertexai/example/pubspec.yaml @@ -26,7 +26,7 @@ dependencies: flutter: sdk: flutter flutter_markdown: ^0.6.20 - just_audio: ^0.9.43 + flutter_soloud: ^3.1.6 path_provider: ^2.1.5 record: ^5.2.1 diff --git a/packages/firebase_vertexai/firebase_vertexai/example/web/index.html b/packages/firebase_vertexai/firebase_vertexai/example/web/index.html index adc47a626031..dcd929827260 100644 --- a/packages/firebase_vertexai/firebase_vertexai/example/web/index.html +++ b/packages/firebase_vertexai/firebase_vertexai/example/web/index.html @@ -1,5 +1,6 @@ + - + flutterfire_vertexai + + + - + + \ No newline at end of file diff --git a/packages/firebase_vertexai/firebase_vertexai/example/windows/.gitignore b/packages/firebase_vertexai/firebase_vertexai/example/windows/.gitignore new file mode 100644 index 000000000000..d492d0d98c8f --- /dev/null +++ b/packages/firebase_vertexai/firebase_vertexai/example/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/