From 04ad427b94871ed28eac6457374838d69a59a7db Mon Sep 17 00:00:00 2001 From: rahullohra Date: Wed, 22 Jul 2026 13:29:59 +0530 Subject: [PATCH 1/7] Chore: make incoming call android 17 compatible --- .../video/android/core/StreamVideoClient.kt | 2 +- .../ForegroundServicePermissionManager.kt | 19 ++++++++++ .../ForegroundServicePermissionManagerTest.kt | 35 +++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt index ee5e89befb7..736e04d5801 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt @@ -778,7 +778,7 @@ internal class StreamVideoClient internal constructor( logger.d { "[getOrCreateCall] type: $type, id: $id, members: $members" } return apiCall { - coordinatorConnectionModule.api.getOrCreateCall( + coordinatorConnectionModule.api.getOrCreateCall( //noob type = type, id = id, getOrCreateCallRequest = GetOrCreateCallRequest( diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/permissions/ForegroundServicePermissionManager.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/permissions/ForegroundServicePermissionManager.kt index 209a5e8ad04..cc693689e29 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/permissions/ForegroundServicePermissionManager.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/permissions/ForegroundServicePermissionManager.kt @@ -56,6 +56,7 @@ internal open class ForegroundServicePermissionManager { CallService.Companion.TRIGGER_ONGOING_CALL, CallService.Companion.TRIGGER_OUTGOING_CALL, -> calculateServiceType(context) + CallService.Companion.TRIGGER_INCOMING_CALL -> ringingServiceType() else -> noPermissionServiceType() } } @@ -91,6 +92,24 @@ 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 ringingServiceType(): Int = + // TODO: replace the hardcoded 37 with Build.VERSION_CODES. once compileSdk / + // targetSdk are raised to 37; the constant does not exist at the current compileSdk. + if (requiredForegroundTypes.contains(ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL) && + Build.VERSION.SDK_INT >= 37 + ) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL + } else { + noPermissionServiceType() + } + @SuppressLint("InlinedApi") internal open fun androidQServiceType(): Int { return if (requiredForegroundTypes.contains( diff --git a/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/notifications/internal/service/permissions/ForegroundServicePermissionManagerTest.kt b/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/notifications/internal/service/permissions/ForegroundServicePermissionManagerTest.kt index 30671f36a08..6dcd3b68379 100644 --- a/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/notifications/internal/service/permissions/ForegroundServicePermissionManagerTest.kt +++ b/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/notifications/internal/service/permissions/ForegroundServicePermissionManagerTest.kt @@ -27,6 +27,7 @@ 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 @@ -99,6 +100,40 @@ class ForegroundServicePermissionManagerTest { assertEquals(0, type) } + @Test + @Config(sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE]) + fun `incoming call keeps short service below android 17`() { + // 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, + ) + } + + @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", 37) + + val type = manager.getServiceType( + context, + CallService.TRIGGER_INCOMING_CALL, + ) + + assertEquals( + ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL, + type, + ) + } + @Test @Config(sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE]) fun `no permission uses short service on android 14`() { From 67a92f4c4b90e992e7b7443dabef278789041256 Mon Sep 17 00:00:00 2001 From: rahullohra Date: Thu, 23 Jul 2026 12:04:09 +0530 Subject: [PATCH 2/7] Chore: revert --- .../kotlin/io/getstream/video/android/core/StreamVideoClient.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt index 736e04d5801..ee5e89befb7 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt @@ -778,7 +778,7 @@ internal class StreamVideoClient internal constructor( logger.d { "[getOrCreateCall] type: $type, id: $id, members: $members" } return apiCall { - coordinatorConnectionModule.api.getOrCreateCall( //noob + coordinatorConnectionModule.api.getOrCreateCall( type = type, id = id, getOrCreateCallRequest = GetOrCreateCallRequest( From 0b0bf1de63e9fc7995b4d359117ac2a7a842f4f8 Mon Sep 17 00:00:00 2001 From: rahullohra Date: Thu, 23 Jul 2026 12:26:00 +0530 Subject: [PATCH 3/7] Chore: refactor name --- .../permissions/ForegroundServicePermissionManager.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/permissions/ForegroundServicePermissionManager.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/permissions/ForegroundServicePermissionManager.kt index cc693689e29..f6432356ad8 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/permissions/ForegroundServicePermissionManager.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/permissions/ForegroundServicePermissionManager.kt @@ -56,7 +56,7 @@ internal open class ForegroundServicePermissionManager { CallService.Companion.TRIGGER_ONGOING_CALL, CallService.Companion.TRIGGER_OUTGOING_CALL, -> calculateServiceType(context) - CallService.Companion.TRIGGER_INCOMING_CALL -> ringingServiceType() + CallService.Companion.TRIGGER_INCOMING_CALL -> incomingRingingServiceType() else -> noPermissionServiceType() } } @@ -93,13 +93,13 @@ internal open class ForegroundServicePermissionManager { } /** - * Foreground-service type for the incoming/ringing window. + * 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 ringingServiceType(): Int = + internal open fun incomingRingingServiceType(): Int = // TODO: replace the hardcoded 37 with Build.VERSION_CODES. once compileSdk / // targetSdk are raised to 37; the constant does not exist at the current compileSdk. if (requiredForegroundTypes.contains(ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL) && From 4dfbc0af9cd89eed5f624b7886e3ad3c8c3efce5 Mon Sep 17 00:00:00 2001 From: rahullohra Date: Tue, 28 Jul 2026 13:56:19 +0530 Subject: [PATCH 4/7] refactor: remove maxSize named argument for AGP 9 compatibility --- .../kotlin/io/getstream/video/android/core/StreamVideoClient.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt index ee5e89befb7..f1cec98224c 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt @@ -229,7 +229,7 @@ internal class StreamVideoClient internal constructor( private val logger by taggedLogger("Call:StreamVideo") private var subscriptions = mutableSetOf() private var calls = mutableMapOf() - private val destroyedCalls = LruCache(maxSize = 100) + private val destroyedCalls = LruCache(100) internal val callSoundAndVibrationPlayer = CallSoundAndVibrationPlayer(context) internal val audioExecutionContext = AudioExecutionContext() From c7c7e306b4564eb679f3f8f2f9473b80cbc1193f Mon Sep 17 00:00:00 2001 From: rahullohra Date: Tue, 28 Jul 2026 14:04:16 +0530 Subject: [PATCH 5/7] refactor: remove unnecessary inline arg --- .../video/android/ui/common/permission/PermissionManager.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/permission/PermissionManager.kt b/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/permission/PermissionManager.kt index e38639c2b14..4a6bc2d2f44 100644 --- a/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/permission/PermissionManager.kt +++ b/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/permission/PermissionManager.kt @@ -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 { /** From dabfbd1854a8077083e58ce7c0f037a0b17db879 Mon Sep 17 00:00:00 2001 From: rahullohra Date: Tue, 4 Aug 2026 11:49:41 +0530 Subject: [PATCH 6/7] update: Update streamLog to 1.3.4 which fixes namespace issue which is returned as error in AGP 9 --- gradle/libs.versions.toml | 2 +- .../internal/receivers/GenericCallActionBroadcastReceiver.kt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6cf1978277b..3248dd3ff4e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/receivers/GenericCallActionBroadcastReceiver.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/receivers/GenericCallActionBroadcastReceiver.kt index edbf99c4c6e..ca30adb8287 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/receivers/GenericCallActionBroadcastReceiver.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/receivers/GenericCallActionBroadcastReceiver.kt @@ -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().", From 8984c3bc0a92b21fdafa9d3fa38e9375864cc03a Mon Sep 17 00:00:00 2001 From: rahullohra Date: Wed, 5 Aug 2026 14:06:40 +0530 Subject: [PATCH 7/7] test: cover incoming-ring service type on Android 15/16/17 Move VERSION_CODES polyfills to shared AndroidVersionCodes.kt (fix BALAKLAVA typo). Force SDK_INT for API 35/36/37 tests and restore it via @After teardown. --- .../ForegroundServicePermissionManager.kt | 5 +- .../android/core/utils/AndroidVersionCodes.kt | 22 +++++++ .../ForegroundServicePermissionManagerTest.kt | 65 ++++++++++++++++++- 3 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/utils/AndroidVersionCodes.kt diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/permissions/ForegroundServicePermissionManager.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/permissions/ForegroundServicePermissionManager.kt index f6432356ad8..0727d59be82 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/permissions/ForegroundServicePermissionManager.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/permissions/ForegroundServicePermissionManager.kt @@ -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") @@ -100,10 +101,8 @@ internal open class ForegroundServicePermissionManager { */ @SuppressLint("InlinedApi") internal open fun incomingRingingServiceType(): Int = - // TODO: replace the hardcoded 37 with Build.VERSION_CODES. once compileSdk / - // targetSdk are raised to 37; the constant does not exist at the current compileSdk. if (requiredForegroundTypes.contains(ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL) && - Build.VERSION.SDK_INT >= 37 + Build.VERSION.SDK_INT >= BUILD_VERSION_CODES_CINNAMON_BUN ) { ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL } else { diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/utils/AndroidVersionCodes.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/utils/AndroidVersionCodes.kt new file mode 100644 index 00000000000..bed1757ef12 --- /dev/null +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/utils/AndroidVersionCodes.kt @@ -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. +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 diff --git a/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/notifications/internal/service/permissions/ForegroundServicePermissionManagerTest.kt b/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/notifications/internal/service/permissions/ForegroundServicePermissionManagerTest.kt index 6dcd3b68379..0a0056a39cc 100644 --- a/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/notifications/internal/service/permissions/ForegroundServicePermissionManagerTest.kt +++ b/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/notifications/internal/service/permissions/ForegroundServicePermissionManagerTest.kt @@ -22,6 +22,9 @@ 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 @@ -38,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 @@ -116,12 +127,44 @@ class ForegroundServicePermissionManagerTest { ) } + // 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", 37) + ReflectionHelpers.setStaticField( + Build.VERSION::class.java, + "SDK_INT", + BUILD_VERSION_CODES_CINNAMON_BUN, + ) val type = manager.getServiceType( context, @@ -134,6 +177,26 @@ class ForegroundServicePermissionManagerTest { ) } + // 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`() {