Skip to content
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ streamWebRTC = "145.6.0"
streamNoiseCancellation = "3.0.0"
streamResult = "1.3.0"
streamChat = "6.10.0"
streamLog = "1.3.2"
streamLog = "1.3.4"
streamPush = "1.3.4"
streamRenderscript = "0.0.1"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@
private val logger by taggedLogger("Call:StreamVideo")
private var subscriptions = mutableSetOf<EventSubscription>()
private var calls = mutableMapOf<String, Call>()
private val destroyedCalls = LruCache<Int, Call>(maxSize = 100)
private val destroyedCalls = LruCache<Int, Call>(100)
internal val callSoundAndVibrationPlayer = CallSoundAndVibrationPlayer(context)

internal val audioExecutionContext = AudioExecutionContext()
Expand Down Expand Up @@ -1222,7 +1222,7 @@
),
level = DeprecationLevel.WARNING,
)
override fun logOut() {

Check warning on line 1225 in stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-video-android&issues=AZ-n5WjFJh9jyqenAwEL&open=AZ-n5WjFJh9jyqenAwEL&pullRequest=1752
scope.launch(
CoroutineName("logOut"),
) { streamNotificationManager.deviceTokenStorage.clear() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ internal abstract class GenericCallActionBroadcastReceiver : BroadcastReceiver()
if (streamVideo == null) {
// Stream not initialized, action not handled
logger.e(
message =
createMessage(
intentAction,
"StreamVideo is not initialised. To handle notifications to initialise StreamVideo in Application.onCreate().",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import android.os.Build
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import io.getstream.video.android.core.notifications.internal.service.CallService
import io.getstream.video.android.core.utils.BUILD_VERSION_CODES_CINNAMON_BUN

internal open class ForegroundServicePermissionManager {
@SuppressLint("InlinedApi")
Expand Down Expand Up @@ -56,6 +57,7 @@ internal open class ForegroundServicePermissionManager {
CallService.Companion.TRIGGER_ONGOING_CALL,
CallService.Companion.TRIGGER_OUTGOING_CALL,
-> calculateServiceType(context)
CallService.Companion.TRIGGER_INCOMING_CALL -> incomingRingingServiceType()
else -> noPermissionServiceType()
}
}
Expand Down Expand Up @@ -91,6 +93,22 @@ internal open class ForegroundServicePermissionManager {
ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
}

/**
* Foreground-service type for the incoming-ringing window.
*
* On Android 17+, background audio (the ringtone) is muted under [ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE],
* so phoneCall-capable VoIP services use phoneCall instead. Other services keep [noPermissionServiceType].
*/
@SuppressLint("InlinedApi")
internal open fun incomingRingingServiceType(): Int =
Comment thread
aleksandar-apostolov marked this conversation as resolved.
if (requiredForegroundTypes.contains(ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL) &&
Build.VERSION.SDK_INT >= BUILD_VERSION_CODES_CINNAMON_BUN
) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
} else {
noPermissionServiceType()
}

@SuppressLint("InlinedApi")
internal open fun androidQServiceType(): Int {
return if (requiredForegroundTypes.contains(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright (c) 2014-2026 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-video-android/blob/main/LICENSE
*
* 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 io.getstream.video.android.core.utils

// Polyfills for Build.VERSION_CODES values not yet available at the current compileSdk.
// TODO: delete each once compileSdk covers it, replacing usages with the real constant.

Check warning on line 20 in stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/utils/AndroidVersionCodes.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this TODO comment.

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-video-android&issues=AZ_RJoHlSyMJk0Zj7QB3&open=AZ_RJoHlSyMJk0Zj7QB3&pullRequest=1752
internal const val BUILD_VERSION_CODES_BAKLAVA = 36 // Build.VERSION_CODES.BAKLAVA — needs compileSdk 36
internal const val BUILD_VERSION_CODES_CINNAMON_BUN = 37 // Build.VERSION_CODES.CINNAMON_BUN — needs compileSdk 37
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,15 @@ import android.content.pm.ServiceInfo
import android.os.Build
import androidx.test.core.app.ApplicationProvider
import io.getstream.video.android.core.notifications.internal.service.CallService
import io.getstream.video.android.core.utils.BUILD_VERSION_CODES_BAKLAVA
import io.getstream.video.android.core.utils.BUILD_VERSION_CODES_CINNAMON_BUN
import org.junit.After
import org.junit.Before
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.shadows.ShadowApplication
import org.robolectric.util.ReflectionHelpers
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
Expand All @@ -37,11 +41,19 @@ class ForegroundServicePermissionManagerTest {

private lateinit var context: Context
private lateinit var manager: ForegroundServicePermissionManager
private var originalSdkInt = 0

@Before
fun setup() {
context = ApplicationProvider.getApplicationContext()
manager = ForegroundServicePermissionManager()
originalSdkInt = Build.VERSION.SDK_INT
}

// Restore SDK_INT for tests that force it via ReflectionHelpers, independent of sandbox isolation.
@After
fun tearDown() {
ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", originalSdkInt)
}

@Test
Expand Down Expand Up @@ -99,6 +111,92 @@ class ForegroundServicePermissionManagerTest {
assertEquals(0, type)
}

@Test
@Config(sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE])
fun `incoming call keeps short service below android 17`() {
Comment thread
aleksandar-apostolov marked this conversation as resolved.
// ringingServiceType only switches to phoneCall from Android 17 up, where the
// background-audio hardening applies. Android 14/15/16 keep their existing behavior.
val type = manager.getServiceType(
context,
CallService.TRIGGER_INCOMING_CALL,
)

assertEquals(
ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE,
type,
)
}

// Forces SDK_INT to 35 since Robolectric caps @Config at API 34; TODO: use @Config once API 35+ is supported.
@Test
fun `incoming call keeps short service on android 15`() {
ReflectionHelpers.setStaticField(
Build.VERSION::class.java,
"SDK_INT",
Build.VERSION_CODES.VANILLA_ICE_CREAM,
)

val type = manager.getServiceType(context, CallService.TRIGGER_INCOMING_CALL)

assertEquals(ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE, type)
}

// Forces SDK_INT to 36 since Robolectric caps @Config at API 34; TODO: use @Config once API 36+ is supported.
@Test
fun `incoming call keeps short service on android 16`() {
ReflectionHelpers.setStaticField(
Build.VERSION::class.java,
"SDK_INT",
BUILD_VERSION_CODES_BAKLAVA,
)

val type = manager.getServiceType(context, CallService.TRIGGER_INCOMING_CALL)

assertEquals(ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE, type)
}

@Test
fun `incoming call uses phone call service type on android 17 and above`() {
// Android 17 background-audio hardening mutes the ringtone under SHORT_SERVICE; use the
// while-in-use phoneCall type instead. Robolectric 4.11.1 caps @Config at API 34, so the
// >= 37 branch is exercised by forcing SDK_INT.
ReflectionHelpers.setStaticField(
Build.VERSION::class.java,
"SDK_INT",
BUILD_VERSION_CODES_CINNAMON_BUN,
)

val type = manager.getServiceType(
context,
CallService.TRIGGER_INCOMING_CALL,
)

assertEquals(
ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL,
type,
)
}

// A service that validly omits PHONE_CALL (e.g. camera + mic, no Telecom) keeps SHORT_SERVICE on 17+.
@Test
fun `incoming call keeps short service on android 17 when service does not declare phone call`() {
ReflectionHelpers.setStaticField(
Build.VERSION::class.java,
"SDK_INT",
BUILD_VERSION_CODES_CINNAMON_BUN,
)
val cameraMicManager = object : ForegroundServicePermissionManager() {
override val requiredForegroundTypes = setOf(
ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA,
ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE,
)
}

val type = cameraMicManager.getServiceType(context, CallService.TRIGGER_INCOMING_CALL)

assertEquals(ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE, type)
}

@Test
@Config(sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE])
fun `no permission uses short service on android 14`() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,8 @@ public interface PermissionManager {
*/
private class StreamPermissionManagerImpl(
private val activity: ComponentActivity,
private inline val onPermissionResult: (String, Boolean) -> Unit,
private inline val onShowRequestPermissionRationale: (String) -> Unit,
private val onPermissionResult: (String, Boolean) -> Unit,
private val onShowRequestPermissionRationale: (String) -> Unit,
) : PermissionManager {

/**
Expand Down
Loading