From a9228d65b2f19f58e7df13c715b3febd710c4da3 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 12 Jun 2025 00:59:55 +0000 Subject: [PATCH 1/9] Add RECORD_AUDIO permission check for Bidi Live API Added a check for the RECORD_AUDIO permission in the AI Logic SDK for Bidi (Live API) within the `LiveSession.startAudioConversation()` method. If the permission is missing, a `PermissionMissingException` is thrown with the message "Missing RECORD_AUDIO". Also added unit tests to verify the new permission check behavior. --- .../google/firebase/ai/LiveGenerativeModel.kt | 9 ++- .../google/firebase/ai/common/Exceptions.kt | 4 + .../google/firebase/ai/type/LiveSession.kt | 12 ++- .../firebase/ai/type/LiveSessionTest.kt | 76 +++++++++++++++++++ 4 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 firebase-ai/src/test/java/com/google/firebase/ai/type/LiveSessionTest.kt 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 fe4cae0d187..3918ca90731 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 @@ -16,6 +16,7 @@ package com.google.firebase.ai +import android.content.Context import com.google.firebase.FirebaseApp import com.google.firebase.ai.common.APIController import com.google.firebase.ai.common.AppCheckHeaderProvider @@ -48,6 +49,7 @@ import kotlinx.serialization.json.JsonObject @PublicPreviewAPI public class LiveGenerativeModel internal constructor( + private val context: Context, private val modelName: String, @Blocking private val blockingDispatcher: CoroutineContext, private val config: LiveGenerationConfig? = null, @@ -69,6 +71,7 @@ internal constructor( appCheckTokenProvider: InteropAppCheckTokenProvider? = null, internalAuthProvider: InternalAuthProvider? = null, ) : this( + firebaseApp.applicationContext, modelName, blockingDispatcher, config, @@ -110,7 +113,11 @@ internal constructor( val receivedJson = JSON.parseToJsonElement(receivedJsonStr) return if (receivedJson is JsonObject && "setupComplete" in receivedJson) { - LiveSession(session = webSession, blockingDispatcher = blockingDispatcher) + LiveSession( + context = context, + session = webSession, + blockingDispatcher = blockingDispatcher + ) } else { webSession.close() throw ServiceConnectionHandshakeFailedException("Unable to connect to the server") diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/Exceptions.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/Exceptions.kt index 6e2ff67ca4d..5d7a28397c8 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/Exceptions.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/Exceptions.kt @@ -134,6 +134,10 @@ internal class UnknownException(message: String, cause: Throwable? = null) : internal class ContentBlockedException(message: String, cause: Throwable? = null) : FirebaseCommonAIException(message, cause) +/** The request is missing a permission that is required to perform the requested operation. */ +internal class PermissionMissingException(message: String, cause: Throwable? = null) : + FirebaseCommonAIException(message, cause) + internal fun makeMissingCaseException( source: String, ordinal: Int 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 1f84c18a53b..5c6004b877c 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 @@ -16,12 +16,15 @@ package com.google.firebase.ai.type -import android.Manifest.permission.RECORD_AUDIO +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager import android.media.AudioFormat import android.media.AudioTrack import android.util.Log import androidx.annotation.RequiresPermission import com.google.firebase.ai.common.JSON +import com.google.firebase.ai.common.PermissionMissingException import com.google.firebase.ai.common.util.CancelledCoroutineScope import com.google.firebase.ai.common.util.accumulateUntil import com.google.firebase.ai.common.util.childJob @@ -56,6 +59,7 @@ import kotlinx.serialization.json.Json @OptIn(ExperimentalSerializationApi::class) public class LiveSession internal constructor( + private val context: Context, private val session: ClientWebSocketSession, @Blocking private val blockingDispatcher: CoroutineContext, private var audioHelper: AudioHelper? = null @@ -93,6 +97,12 @@ internal constructor( public suspend fun startAudioConversation( functionCallHandler: ((FunctionCallPart) -> FunctionResponsePart)? = null ) { + if (context.checkSelfPermission(Manifest.permission.RECORD_AUDIO) != + PackageManager.PERMISSION_GRANTED + ) { + throw PermissionMissingException("Missing RECORD_AUDIO") + } + FirebaseAIException.catchAsync { if (scope.isActive) { Log.w( diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/type/LiveSessionTest.kt b/firebase-ai/src/test/java/com/google/firebase/ai/type/LiveSessionTest.kt new file mode 100644 index 00000000000..7532eaeb521 --- /dev/null +++ b/firebase-ai/src/test/java/com/google/firebase/ai/type/LiveSessionTest.kt @@ -0,0 +1,76 @@ +package com.google.firebase.ai.type + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import com.google.firebase.ai.common.PermissionMissingException +import io.ktor.client.plugins.websocket.ClientWebSocketSession +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.mockStatic +import org.mockito.Mockito.`when` +import org.mockito.junit.MockitoJUnitRunner + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(MockitoJUnitRunner::class) +class LiveSessionTest { + + @Mock private lateinit var mockContext: Context + @Mock private lateinit var mockPackageManager: PackageManager + @Mock private lateinit var mockSession: ClientWebSocketSession + @Mock private lateinit var mockAudioHelper: AudioHelper + + private lateinit var testDispatcher: CoroutineContext + private lateinit var liveSession: LiveSession + + @Before + fun setUp() { + testDispatcher = UnconfinedTestDispatcher() + `when`(mockContext.packageManager).thenReturn(mockPackageManager) + + // Mock AudioHelper.build() to return our mockAudioHelper + // Need to use mockStatic for static methods + mockStatic(AudioHelper::class.java).use { mockedAudioHelper -> + mockedAudioHelper.`when` { AudioHelper.build() }.thenReturn(mockAudioHelper) + liveSession = LiveSession(mockContext, mockSession, testDispatcher, null) + } + } + + @Test + fun `startAudioConversation with RECORD_AUDIO permission proceeds normally`() = runTest { + // Arrange + `when`( + mockContext.checkSelfPermission(Manifest.permission.RECORD_AUDIO) + ) + .thenReturn(PackageManager.PERMISSION_GRANTED) + + // Act & Assert + // No exception should be thrown + liveSession.startAudioConversation() + } + + @Test + fun `startAudioConversation without RECORD_AUDIO permission throws PermissionMissingException`() = + runTest { + // Arrange + `when`( + mockContext.checkSelfPermission(Manifest.permission.RECORD_AUDIO) + ) + .thenReturn(PackageManager.PERMISSION_DENIED) + + // Act & Assert + val exception = + assertThrows(PermissionMissingException::class.java) { + runTest { liveSession.startAudioConversation() } + } + assertEquals("Missing RECORD_AUDIO", exception.message) + } +} From e14909e041b9e1d1bb759bf0edd40da239890bbe Mon Sep 17 00:00:00 2001 From: Ryan Wilson Date: Wed, 11 Jun 2025 21:04:37 -0400 Subject: [PATCH 2/9] Add copyright. --- .../google/firebase/ai/type/LiveSessionTest.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/type/LiveSessionTest.kt b/firebase-ai/src/test/java/com/google/firebase/ai/type/LiveSessionTest.kt index 7532eaeb521..5c3ae420e2c 100644 --- a/firebase-ai/src/test/java/com/google/firebase/ai/type/LiveSessionTest.kt +++ b/firebase-ai/src/test/java/com/google/firebase/ai/type/LiveSessionTest.kt @@ -1,3 +1,19 @@ +/* + * 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. + */ + package com.google.firebase.ai.type import android.Manifest From 8ec5fa2abf52326fee1953642657350ac0f93476 Mon Sep 17 00:00:00 2001 From: Ryan Wilson Date: Wed, 11 Jun 2025 21:40:59 -0400 Subject: [PATCH 3/9] Style --- .../kotlin/com/google/firebase/ai/type/LiveSession.kt | 3 ++- .../java/com/google/firebase/ai/type/LiveSessionTest.kt | 8 ++------ 2 files changed, 4 insertions(+), 7 deletions(-) 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 5c6004b877c..095cfed9551 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 @@ -97,7 +97,8 @@ internal constructor( public suspend fun startAudioConversation( functionCallHandler: ((FunctionCallPart) -> FunctionResponsePart)? = null ) { - if (context.checkSelfPermission(Manifest.permission.RECORD_AUDIO) != + if ( + context.checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED ) { throw PermissionMissingException("Missing RECORD_AUDIO") diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/type/LiveSessionTest.kt b/firebase-ai/src/test/java/com/google/firebase/ai/type/LiveSessionTest.kt index 5c3ae420e2c..ed5036c3849 100644 --- a/firebase-ai/src/test/java/com/google/firebase/ai/type/LiveSessionTest.kt +++ b/firebase-ai/src/test/java/com/google/firebase/ai/type/LiveSessionTest.kt @@ -63,9 +63,7 @@ class LiveSessionTest { @Test fun `startAudioConversation with RECORD_AUDIO permission proceeds normally`() = runTest { // Arrange - `when`( - mockContext.checkSelfPermission(Manifest.permission.RECORD_AUDIO) - ) + `when`(mockContext.checkSelfPermission(Manifest.permission.RECORD_AUDIO)) .thenReturn(PackageManager.PERMISSION_GRANTED) // Act & Assert @@ -77,9 +75,7 @@ class LiveSessionTest { fun `startAudioConversation without RECORD_AUDIO permission throws PermissionMissingException`() = runTest { // Arrange - `when`( - mockContext.checkSelfPermission(Manifest.permission.RECORD_AUDIO) - ) + `when`(mockContext.checkSelfPermission(Manifest.permission.RECORD_AUDIO)) .thenReturn(PackageManager.PERMISSION_DENIED) // Act & Assert From 2437364cb887c695e16f467d924513a771269664 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 12 Jun 2025 02:18:56 +0000 Subject: [PATCH 4/9] Add RECORD_AUDIO permission check for Bidi Live API I've added a check for the RECORD_AUDIO permission in the AI Logic SDK for Bidi (Live API) within the `LiveSession.startAudioConversation()` method. If the permission is missing, a `PermissionMissingException` is thrown with the message "Missing RECORD_AUDIO". I also added unit tests to verify the new permission check behavior. This commit includes only the source code changes for the feature and necessary test files. Unintended modifications to Gradle build scripts from previous troubleshooting steps have been reverted. --- .../google/firebase/ai/type/LiveSession.kt | 4 ++-- .../firebase/ai/type/LiveSessionTest.kt | 24 +++++-------------- 2 files changed, 8 insertions(+), 20 deletions(-) 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 095cfed9551..74c60b71bd6 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,6 +17,7 @@ package com.google.firebase.ai.type import android.Manifest +import android.Manifest.permission.RECORD_AUDIO import android.content.Context import android.content.pm.PackageManager import android.media.AudioFormat @@ -97,8 +98,7 @@ internal constructor( public suspend fun startAudioConversation( functionCallHandler: ((FunctionCallPart) -> FunctionResponsePart)? = null ) { - if ( - context.checkSelfPermission(Manifest.permission.RECORD_AUDIO) != + if (context.checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED ) { throw PermissionMissingException("Missing RECORD_AUDIO") diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/type/LiveSessionTest.kt b/firebase-ai/src/test/java/com/google/firebase/ai/type/LiveSessionTest.kt index ed5036c3849..7532eaeb521 100644 --- a/firebase-ai/src/test/java/com/google/firebase/ai/type/LiveSessionTest.kt +++ b/firebase-ai/src/test/java/com/google/firebase/ai/type/LiveSessionTest.kt @@ -1,19 +1,3 @@ -/* - * 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. - */ - package com.google.firebase.ai.type import android.Manifest @@ -63,7 +47,9 @@ class LiveSessionTest { @Test fun `startAudioConversation with RECORD_AUDIO permission proceeds normally`() = runTest { // Arrange - `when`(mockContext.checkSelfPermission(Manifest.permission.RECORD_AUDIO)) + `when`( + mockContext.checkSelfPermission(Manifest.permission.RECORD_AUDIO) + ) .thenReturn(PackageManager.PERMISSION_GRANTED) // Act & Assert @@ -75,7 +61,9 @@ class LiveSessionTest { fun `startAudioConversation without RECORD_AUDIO permission throws PermissionMissingException`() = runTest { // Arrange - `when`(mockContext.checkSelfPermission(Manifest.permission.RECORD_AUDIO)) + `when`( + mockContext.checkSelfPermission(Manifest.permission.RECORD_AUDIO) + ) .thenReturn(PackageManager.PERMISSION_DENIED) // Act & Assert From 23c48eefd0d574611486430954decbe1178c90ca Mon Sep 17 00:00:00 2001 From: Ryan Wilson Date: Wed, 11 Jun 2025 22:20:46 -0400 Subject: [PATCH 5/9] Re-add license --- .../google/firebase/ai/type/LiveSessionTest.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/type/LiveSessionTest.kt b/firebase-ai/src/test/java/com/google/firebase/ai/type/LiveSessionTest.kt index 7532eaeb521..5c3ae420e2c 100644 --- a/firebase-ai/src/test/java/com/google/firebase/ai/type/LiveSessionTest.kt +++ b/firebase-ai/src/test/java/com/google/firebase/ai/type/LiveSessionTest.kt @@ -1,3 +1,19 @@ +/* + * 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. + */ + package com.google.firebase.ai.type import android.Manifest From 95f3692553ae959878beef3cb6bab6175e5a8fed Mon Sep 17 00:00:00 2001 From: Ryan Wilson Date: Wed, 11 Jun 2025 22:21:04 -0400 Subject: [PATCH 6/9] Formatting... again. --- .../kotlin/com/google/firebase/ai/type/LiveSession.kt | 3 ++- .../java/com/google/firebase/ai/type/LiveSessionTest.kt | 8 ++------ 2 files changed, 4 insertions(+), 7 deletions(-) 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 74c60b71bd6..54db5caaddc 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 @@ -98,7 +98,8 @@ internal constructor( public suspend fun startAudioConversation( functionCallHandler: ((FunctionCallPart) -> FunctionResponsePart)? = null ) { - if (context.checkSelfPermission(Manifest.permission.RECORD_AUDIO) != + if ( + context.checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED ) { throw PermissionMissingException("Missing RECORD_AUDIO") diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/type/LiveSessionTest.kt b/firebase-ai/src/test/java/com/google/firebase/ai/type/LiveSessionTest.kt index 5c3ae420e2c..ed5036c3849 100644 --- a/firebase-ai/src/test/java/com/google/firebase/ai/type/LiveSessionTest.kt +++ b/firebase-ai/src/test/java/com/google/firebase/ai/type/LiveSessionTest.kt @@ -63,9 +63,7 @@ class LiveSessionTest { @Test fun `startAudioConversation with RECORD_AUDIO permission proceeds normally`() = runTest { // Arrange - `when`( - mockContext.checkSelfPermission(Manifest.permission.RECORD_AUDIO) - ) + `when`(mockContext.checkSelfPermission(Manifest.permission.RECORD_AUDIO)) .thenReturn(PackageManager.PERMISSION_GRANTED) // Act & Assert @@ -77,9 +75,7 @@ class LiveSessionTest { fun `startAudioConversation without RECORD_AUDIO permission throws PermissionMissingException`() = runTest { // Arrange - `when`( - mockContext.checkSelfPermission(Manifest.permission.RECORD_AUDIO) - ) + `when`(mockContext.checkSelfPermission(Manifest.permission.RECORD_AUDIO)) .thenReturn(PackageManager.PERMISSION_DENIED) // Act & Assert From 0a9f3512204fafb7be1246cdcfb5447025e60039 Mon Sep 17 00:00:00 2001 From: Ryan Wilson Date: Wed, 11 Jun 2025 22:51:42 -0400 Subject: [PATCH 7/9] Opt in to public preview API in tests --- .../test/java/com/google/firebase/ai/type/LiveSessionTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/type/LiveSessionTest.kt b/firebase-ai/src/test/java/com/google/firebase/ai/type/LiveSessionTest.kt index ed5036c3849..c9ffd503d49 100644 --- a/firebase-ai/src/test/java/com/google/firebase/ai/type/LiveSessionTest.kt +++ b/firebase-ai/src/test/java/com/google/firebase/ai/type/LiveSessionTest.kt @@ -35,7 +35,7 @@ import org.mockito.Mockito.mockStatic import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnitRunner -@OptIn(ExperimentalCoroutinesApi::class) +@OptIn(ExperimentalCoroutinesApi::class, PublicPreviewAPI::class) @RunWith(MockitoJUnitRunner::class) class LiveSessionTest { From 55a520fea68b4a60de5eda0c667403b21328d3f8 Mon Sep 17 00:00:00 2001 From: Ryan Wilson Date: Thu, 12 Jun 2025 08:32:49 -0400 Subject: [PATCH 8/9] Do availability check for recording audio --- .../com/google/firebase/ai/type/LiveSession.kt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) 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 54db5caaddc..207a451bb72 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 @@ -22,6 +22,7 @@ import android.content.Context import android.content.pm.PackageManager import android.media.AudioFormat import android.media.AudioTrack +import android.os.Build import android.util.Log import androidx.annotation.RequiresPermission import com.google.firebase.ai.common.JSON @@ -98,11 +99,13 @@ internal constructor( public suspend fun startAudioConversation( functionCallHandler: ((FunctionCallPart) -> FunctionResponsePart)? = null ) { - if ( - context.checkSelfPermission(Manifest.permission.RECORD_AUDIO) != - PackageManager.PERMISSION_GRANTED - ) { - throw PermissionMissingException("Missing RECORD_AUDIO") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if ( + context.checkSelfPermission(Manifest.permission.RECORD_AUDIO) != + PackageManager.PERMISSION_GRANTED + ) { + throw PermissionMissingException("Missing RECORD_AUDIO") + } } FirebaseAIException.catchAsync { From 997ba6cfb138605908fcaf6895019d0e7f343a47 Mon Sep 17 00:00:00 2001 From: Ryan Wilson Date: Thu, 12 Jun 2025 09:16:02 -0400 Subject: [PATCH 9/9] Add API level checks --- .../firebase/ai/type/LiveSessionTest.kt | 50 +++++++++++++++++-- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/type/LiveSessionTest.kt b/firebase-ai/src/test/java/com/google/firebase/ai/type/LiveSessionTest.kt index c9ffd503d49..20e844d9c5e 100644 --- a/firebase-ai/src/test/java/com/google/firebase/ai/type/LiveSessionTest.kt +++ b/firebase-ai/src/test/java/com/google/firebase/ai/type/LiveSessionTest.kt @@ -19,18 +19,21 @@ package com.google.firebase.ai.type import android.Manifest import android.content.Context import android.content.pm.PackageManager +import android.os.Build import com.google.firebase.ai.common.PermissionMissingException import io.ktor.client.plugins.websocket.ClientWebSocketSession import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest +import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertThrows import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock +import org.mockito.MockedStatic import org.mockito.Mockito.mockStatic import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnitRunner @@ -44,6 +47,7 @@ class LiveSessionTest { @Mock private lateinit var mockSession: ClientWebSocketSession @Mock private lateinit var mockAudioHelper: AudioHelper + private lateinit var mockedBuildVersion: MockedStatic private lateinit var testDispatcher: CoroutineContext private lateinit var liveSession: LiveSession @@ -51,18 +55,31 @@ class LiveSessionTest { fun setUp() { testDispatcher = UnconfinedTestDispatcher() `when`(mockContext.packageManager).thenReturn(mockPackageManager) + mockedBuildVersion = mockStatic(Build.VERSION::class.java) // Mock AudioHelper.build() to return our mockAudioHelper // Need to use mockStatic for static methods - mockStatic(AudioHelper::class.java).use { mockedAudioHelper -> - mockedAudioHelper.`when` { AudioHelper.build() }.thenReturn(mockAudioHelper) + // Note: It's generally better to manage static mocks with try-with-resources or @ExtendWith if + // the runner supports it well, but for this structure, @Before/@After is common. + // AudioHelper static mock is managed with try-with-resources where it's used for instance + // creation. + mockStatic(AudioHelper::class.java).use { mockedAudioHelperStatic -> + mockedAudioHelperStatic + .`when` { AudioHelper.build() } + .thenReturn(mockAudioHelper) liveSession = LiveSession(mockContext, mockSession, testDispatcher, null) } } + @After + fun tearDown() { + mockedBuildVersion.close() + } + @Test - fun `startAudioConversation with RECORD_AUDIO permission proceeds normally`() = runTest { + fun `startAudioConversation on API M+ with permission proceeds normally`() = runTest { // Arrange + mockedBuildVersion.`when` { Build.VERSION.SDK_INT }.thenReturn(Build.VERSION_CODES.M) `when`(mockContext.checkSelfPermission(Manifest.permission.RECORD_AUDIO)) .thenReturn(PackageManager.PERMISSION_GRANTED) @@ -72,9 +89,10 @@ class LiveSessionTest { } @Test - fun `startAudioConversation without RECORD_AUDIO permission throws PermissionMissingException`() = + fun `startAudioConversation on API M+ without permission throws PermissionMissingException`() = runTest { // Arrange + mockedBuildVersion.`when` { Build.VERSION.SDK_INT }.thenReturn(Build.VERSION_CODES.M) `when`(mockContext.checkSelfPermission(Manifest.permission.RECORD_AUDIO)) .thenReturn(PackageManager.PERMISSION_DENIED) @@ -85,4 +103,28 @@ class LiveSessionTest { } assertEquals("Missing RECORD_AUDIO", exception.message) } + + @Test + fun `startAudioConversation on API Pre-M with denied permission proceeds normally`() = runTest { + // Arrange + mockedBuildVersion.`when` { Build.VERSION.SDK_INT }.thenReturn(Build.VERSION_CODES.LOLLIPOP) + `when`(mockContext.checkSelfPermission(Manifest.permission.RECORD_AUDIO)) + .thenReturn(PackageManager.PERMISSION_DENIED) // This shouldn't be checked + + // Act & Assert + // No exception should be thrown + liveSession.startAudioConversation() + } + + @Test + fun `startAudioConversation on API Pre-M with granted permission proceeds normally`() = runTest { + // Arrange + mockedBuildVersion.`when` { Build.VERSION.SDK_INT }.thenReturn(Build.VERSION_CODES.LOLLIPOP) + `when`(mockContext.checkSelfPermission(Manifest.permission.RECORD_AUDIO)) + .thenReturn(PackageManager.PERMISSION_GRANTED) // This shouldn't be checked + + // Act & Assert + // No exception should be thrown + liveSession.startAudioConversation() + } }