Skip to content

Commit e35a424

Browse files
authored
BIDI mini refactoring and new additions (#7267)
The PR does the following: - Introduce a new MissingPermissionsException which would be thrown incase the audio permission is not given by the end user. - Add helper functions to LiveSession to help the developer track if the audio session is running or not and if the underlying websocket connection is active or not..
1 parent 546cbcb commit e35a424

File tree

6 files changed

+43
-4
lines changed

6 files changed

+43
-4
lines changed

firebase-ai/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Unreleased
22

3+
- [feature] Introduced `MissingPermissionsException`, which is thrown when the necessary permissions
4+
have not been granted by the user.
5+
- [feature] Added helper functions to `LiveSession` to allow developers to track the status of the
6+
audio session and the underlying websocket connection.
7+
38
# 17.2.0
49

510
- [feature] Added support for returning thought summaries, which are synthesized versions of a

firebase-ai/api.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -879,6 +879,8 @@ package com.google.firebase.ai.type {
879879

880880
@com.google.firebase.ai.type.PublicPreviewAPI public final class LiveSession {
881881
method public suspend Object? close(kotlin.coroutines.Continuation<? super kotlin.Unit>);
882+
method public boolean isAudioConversationActive();
883+
method public boolean isClosed();
882884
method public kotlinx.coroutines.flow.Flow<com.google.firebase.ai.type.LiveServerMessage> receive();
883885
method public suspend Object? send(com.google.firebase.ai.type.Content content, kotlin.coroutines.Continuation<? super kotlin.Unit>);
884886
method public suspend Object? send(String text, kotlin.coroutines.Continuation<? super kotlin.Unit>);
@@ -918,6 +920,10 @@ package com.google.firebase.ai.type {
918920
method public static String? asTextOrNull(com.google.firebase.ai.type.Part);
919921
}
920922

923+
public final class PermissionMissingException extends com.google.firebase.ai.type.FirebaseAIException {
924+
ctor public PermissionMissingException(String message, Throwable? cause = null);
925+
}
926+
921927
public final class PromptBlockedException extends com.google.firebase.ai.type.FirebaseAIException {
922928
method public com.google.firebase.ai.type.GenerateContentResponse? getResponse();
923929
property public final com.google.firebase.ai.type.GenerateContentResponse? response;

firebase-ai/gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
version=17.2.1
15+
version=17.3.0
1616
latestReleasedVersion=17.2.0

firebase-ai/src/main/kotlin/com/google/firebase/ai/LiveGenerativeModel.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ internal constructor(
5555
private val tools: List<Tool>? = null,
5656
private val systemInstruction: Content? = null,
5757
private val location: String,
58-
private val controller: APIController,
58+
private val firebaseApp: FirebaseApp,
59+
private val controller: APIController
5960
) {
6061
internal constructor(
6162
modelName: String,
@@ -78,6 +79,7 @@ internal constructor(
7879
tools,
7980
systemInstruction,
8081
location,
82+
firebaseApp,
8183
APIController(
8284
apiKey,
8385
modelName,
@@ -119,7 +121,11 @@ internal constructor(
119121
val receivedJson = JSON.parseToJsonElement(receivedJsonStr)
120122

121123
return if (receivedJson is JsonObject && "setupComplete" in receivedJson) {
122-
LiveSession(session = webSession, blockingDispatcher = blockingDispatcher)
124+
LiveSession(
125+
session = webSession,
126+
blockingDispatcher = blockingDispatcher,
127+
firebaseApp = firebaseApp
128+
)
123129
} else {
124130
webSession.close()
125131
throw ServiceConnectionHandshakeFailedException("Unable to connect to the server")

firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Exceptions.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,10 @@ public class AudioRecordInitializationFailedException(message: String) :
231231
public class ServiceConnectionHandshakeFailedException(message: String, cause: Throwable? = null) :
232232
FirebaseAIException(message, cause)
233233

234+
/** The request is missing a permission that is required to perform the requested operation. */
235+
public class PermissionMissingException(message: String, cause: Throwable? = null) :
236+
FirebaseAIException(message, cause)
237+
234238
/** Catch all case for exceptions not explicitly expected. */
235239
public class UnknownException internal constructor(message: String, cause: Throwable? = null) :
236240
FirebaseAIException(message, cause)

firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveSession.kt

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@
1717
package com.google.firebase.ai.type
1818

1919
import android.Manifest.permission.RECORD_AUDIO
20+
import android.content.pm.PackageManager
2021
import android.media.AudioFormat
2122
import android.media.AudioTrack
2223
import android.util.Log
2324
import androidx.annotation.RequiresPermission
25+
import androidx.core.content.ContextCompat
26+
import com.google.firebase.FirebaseApp
2427
import com.google.firebase.ai.common.JSON
2528
import com.google.firebase.ai.common.util.CancelledCoroutineScope
2629
import com.google.firebase.ai.common.util.accumulateUntil
@@ -58,7 +61,8 @@ public class LiveSession
5861
internal constructor(
5962
private val session: ClientWebSocketSession,
6063
@Blocking private val blockingDispatcher: CoroutineContext,
61-
private var audioHelper: AudioHelper? = null
64+
private var audioHelper: AudioHelper? = null,
65+
private val firebaseApp: FirebaseApp,
6266
) {
6367
/**
6468
* Coroutine scope that we batch data on for [startAudioConversation].
@@ -93,6 +97,14 @@ internal constructor(
9397
public suspend fun startAudioConversation(
9498
functionCallHandler: ((FunctionCallPart) -> FunctionResponsePart)? = null
9599
) {
100+
101+
val context = firebaseApp.applicationContext
102+
if (
103+
ContextCompat.checkSelfPermission(context, RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED
104+
) {
105+
throw PermissionMissingException("Audio access not provided by the user")
106+
}
107+
96108
FirebaseAIException.catchAsync {
97109
if (scope.isActive) {
98110
Log.w(
@@ -131,6 +143,12 @@ internal constructor(
131143
}
132144
}
133145

146+
/** Indicates whether the underlying websocket connection is active. */
147+
public fun isClosed(): Boolean = !(session.isActive && !session.incoming.tryReceive().isClosed)
148+
149+
/** Indicates whether an audio conversation is being used for this session object. */
150+
public fun isAudioConversationActive(): Boolean = (audioHelper != null)
151+
134152
/**
135153
* Receives responses from the model for both streaming and standard requests.
136154
*

0 commit comments

Comments
 (0)