Skip to content
4 changes: 4 additions & 0 deletions firebase-ai/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
# 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

Expand Down
6 changes: 6 additions & 0 deletions firebase-ai/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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<? super kotlin.Unit>);
method public boolean isAudioConversationActive();
method public boolean isClosed();
method public kotlinx.coroutines.flow.Flow<com.google.firebase.ai.type.LiveServerMessage> receive();
method public suspend Object? send(com.google.firebase.ai.type.Content content, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public suspend Object? send(String text, kotlin.coroutines.Continuation<? super kotlin.Unit>);
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,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 = controller.firebaseApp
)
} else {
webSession.close()
throw ServiceConnectionHandshakeFailedException("Unable to connect to the server")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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].
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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.
*
Expand Down
2 changes: 1 addition & 1 deletion firebase-inappmessaging/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
version=22.0.1
version=22.1.0
latestReleasedVersion=22.0.0
Loading