diff --git a/firebase-ai/CHANGELOG.md b/firebase-ai/CHANGELOG.md index ea6bb088f63..bf278b1106c 100644 --- a/firebase-ai/CHANGELOG.md +++ b/firebase-ai/CHANGELOG.md @@ -1,5 +1,10 @@ # Unreleased +- [feature] Introduced `MissingPermissionsException`, which is thrown when the necessary permissions + have not been granted by the user. +- [feature] Added helper functions to `LiveSession` to allow developers to track the status of the + audio session and the underlying websocket connection. + # 17.2.0 - [feature] Added support for returning thought summaries, which are synthesized versions of a diff --git a/firebase-ai/api.txt b/firebase-ai/api.txt index 2a49552828a..a80e9efcaf7 100644 --- a/firebase-ai/api.txt +++ b/firebase-ai/api.txt @@ -879,6 +879,8 @@ package com.google.firebase.ai.type { @com.google.firebase.ai.type.PublicPreviewAPI public final class LiveSession { method public suspend Object? close(kotlin.coroutines.Continuation); + method public boolean isAudioConversationActive(); + method public boolean isClosed(); method public kotlinx.coroutines.flow.Flow receive(); method public suspend Object? send(com.google.firebase.ai.type.Content content, kotlin.coroutines.Continuation); method public suspend Object? send(String text, kotlin.coroutines.Continuation); @@ -918,6 +920,10 @@ package com.google.firebase.ai.type { method public static String? asTextOrNull(com.google.firebase.ai.type.Part); } + public final class PermissionMissingException extends com.google.firebase.ai.type.FirebaseAIException { + ctor public PermissionMissingException(String message, Throwable? cause = null); + } + public final class PromptBlockedException extends com.google.firebase.ai.type.FirebaseAIException { method public com.google.firebase.ai.type.GenerateContentResponse? getResponse(); property public final com.google.firebase.ai.type.GenerateContentResponse? response; diff --git a/firebase-ai/gradle.properties b/firebase-ai/gradle.properties index a7c8098777d..2a62002eb65 100644 --- a/firebase-ai/gradle.properties +++ b/firebase-ai/gradle.properties @@ -12,5 +12,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -version=17.2.1 +version=17.3.0 latestReleasedVersion=17.2.0 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 8a8646ce93f..a696ddd5f73 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 @@ -55,7 +55,8 @@ internal constructor( private val tools: List? = null, private val systemInstruction: Content? = null, private val location: String, - private val controller: APIController, + private val firebaseApp: FirebaseApp, + private val controller: APIController ) { internal constructor( modelName: String, @@ -78,6 +79,7 @@ internal constructor( tools, systemInstruction, location, + firebaseApp, APIController( apiKey, modelName, @@ -119,7 +121,11 @@ 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 = firebaseApp + ) } else { webSession.close() throw ServiceConnectionHandshakeFailedException("Unable to connect to the server") 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 fb23ca26e95..64bfe43ac85 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..a91d7e4aedf 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 isClosed(): Boolean = !(session.isActive && !session.incoming.tryReceive().isClosed) + + /** 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. *