diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/LiveGenerativeModel.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/LiveGenerativeModel.kt index 45e10114f72..624d1cc205c 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/LiveGenerativeModel.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/LiveGenerativeModel.kt @@ -113,13 +113,17 @@ internal constructor( val receivedJson = JSON.parseToJsonElement(receivedJsonStr) return if (receivedJson is JsonObject && "setupComplete" in receivedJson) { - LiveSession(session = webSession, blockingDispatcher = blockingDispatcher) + LiveSession( + session = webSession, + blockingDispatcher = blockingDispatcher, + firebaseApp = controller.firebaseApp + ) } else { webSession.close() throw ServiceConnectionHandshakeFailedException("Unable to connect to the server") } } catch (e: ClosedReceiveChannelException) { - throw ServiceConnectionHandshakeFailedException("Channel was closed by the server", e) + throw ServiceConnectionHandshakeFailedException("Error: Too many concurrent live requests", e) } } diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/APIController.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/APIController.kt index 7ba9b026817..a13c2233ee1 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/APIController.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/APIController.kt @@ -97,7 +97,7 @@ internal constructor( private val requestOptions: RequestOptions, httpEngine: HttpClientEngine, private val apiClient: String, - private val firebaseApp: FirebaseApp, + internal val firebaseApp: FirebaseApp, private val appVersion: Int = 0, private val googleAppId: String, private val headerProvider: HeaderProvider?, diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/LiveSessionFutures.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/LiveSessionFutures.kt index 1efa2dfedfc..20d688b8c46 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/LiveSessionFutures.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/LiveSessionFutures.kt @@ -29,6 +29,7 @@ import com.google.firebase.ai.type.MediaData import com.google.firebase.ai.type.PublicPreviewAPI import com.google.firebase.ai.type.SessionAlreadyReceivingException import io.ktor.websocket.close +import kotlinx.coroutines.isActive import kotlinx.coroutines.reactive.asPublisher import org.reactivestreams.Publisher @@ -51,6 +52,12 @@ public abstract class LiveSessionFutures internal constructor() { functionCallHandler: ((FunctionCallPart) -> FunctionResponsePart)? ): ListenableFuture + /** Indicates whether the underlying websocket connection is active. */ + public abstract fun isActive(): ListenableFuture + + /** Indicates whether an audio conversation is being used for this session object. */ + public abstract fun isAudioConversationActive(): ListenableFuture + /** * Starts an audio conversation with the model, which can only be stopped using * [stopAudioConversation]. @@ -169,6 +176,11 @@ public abstract class LiveSessionFutures internal constructor() { override fun startAudioConversation() = SuspendToFutureAdapter.launchFuture { session.startAudioConversation() } + override fun isActive() = SuspendToFutureAdapter.launchFuture { session.isActive() } + + override fun isAudioConversationActive() = + SuspendToFutureAdapter.launchFuture { session.isAudioConversationActive() } + override fun stopAudioConversation() = SuspendToFutureAdapter.launchFuture { session.stopAudioConversation() } diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Exceptions.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Exceptions.kt index 4118afd57c4..e4eb3e0d679 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Exceptions.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Exceptions.kt @@ -231,6 +231,10 @@ public class AudioRecordInitializationFailedException(message: String) : public class ServiceConnectionHandshakeFailedException(message: String, cause: Throwable? = null) : FirebaseAIException(message, cause) +/** The request is missing a permission that is required to perform the requested operation. */ +public class PermissionMissingException(message: String, cause: Throwable? = null) : + FirebaseAIException(message, cause) + /** Catch all case for exceptions not explicitly expected. */ public class UnknownException internal constructor(message: String, cause: Throwable? = null) : FirebaseAIException(message, cause) diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveSession.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveSession.kt index 6e584fe2a50..a3ae8fdf5ce 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveSession.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveSession.kt @@ -17,10 +17,13 @@ package com.google.firebase.ai.type import android.Manifest.permission.RECORD_AUDIO +import android.content.pm.PackageManager import android.media.AudioFormat import android.media.AudioTrack import android.util.Log import androidx.annotation.RequiresPermission +import androidx.core.content.ContextCompat +import com.google.firebase.FirebaseApp import com.google.firebase.ai.common.JSON import com.google.firebase.ai.common.util.CancelledCoroutineScope import com.google.firebase.ai.common.util.accumulateUntil @@ -58,7 +61,8 @@ public class LiveSession internal constructor( private val session: ClientWebSocketSession, @Blocking private val blockingDispatcher: CoroutineContext, - private var audioHelper: AudioHelper? = null + private var audioHelper: AudioHelper? = null, + private val firebaseApp: FirebaseApp, ) { /** * Coroutine scope that we batch data on for [startAudioConversation]. @@ -93,6 +97,14 @@ internal constructor( public suspend fun startAudioConversation( functionCallHandler: ((FunctionCallPart) -> FunctionResponsePart)? = null ) { + + val context = firebaseApp.applicationContext + if ( + ContextCompat.checkSelfPermission(context, RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED + ) { + throw PermissionMissingException("Audio access not provided by the user") + } + FirebaseAIException.catchAsync { if (scope.isActive) { Log.w( @@ -131,6 +143,12 @@ internal constructor( } } + /** Indicates whether the underlying websocket connection is active. */ + public fun isActive(): Boolean = session.isActive + + /** Indicates whether an audio conversation is being used for this session object. */ + public fun isAudioConversationActive(): Boolean = (audioHelper != null) + /** * Receives responses from the model for both streaming and standard requests. *