1 /* 2 * Copyright 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.core.telecom.extensions 18 19 import android.content.Context 20 import android.media.AudioManager 21 import android.util.Log 22 import androidx.core.telecom.internal.CallStateEvent 23 import androidx.core.telecom.internal.CapabilityExchangeRepository 24 import androidx.core.telecom.internal.LocalCallSilenceCallbackRepository 25 import androidx.core.telecom.internal.LocalCallSilenceStateListenerRemote 26 import androidx.core.telecom.util.ExperimentalAppActions 27 import kotlin.coroutines.CoroutineContext 28 import kotlinx.coroutines.CoroutineScope 29 import kotlinx.coroutines.flow.MutableSharedFlow 30 import kotlinx.coroutines.flow.MutableStateFlow 31 import kotlinx.coroutines.flow.drop 32 import kotlinx.coroutines.flow.launchIn 33 import kotlinx.coroutines.flow.onEach 34 import kotlinx.coroutines.launch 35 36 @OptIn(ExperimentalAppActions::class) 37 internal class LocalCallSilenceExtensionImpl( 38 context: Context, 39 coroutineContext: CoroutineContext, 40 private val callStateFlow: MutableSharedFlow<CallStateEvent>, 41 private val initialSilenceState: Boolean, 42 private val onLocalSilenceUpdate: suspend (Boolean) -> Unit 43 ) : LocalCallSilenceExtension { 44 private val mAudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager 45 private var mIsGloballyMuted: Boolean = false 46 private var mCallState: CallStateEvent = CallStateEvent.NEW 47 private val TAG = LocalCallSilenceExtensionImpl::class.java.simpleName 48 49 init { 50 var shouldRemute = false <lambda>null51 CoroutineScope(coroutineContext).launch { 52 callStateFlow.collect { 53 maybeUpdateCallControlState(state = it) 54 maybeUpdateGlobalMuteState(state = it) 55 if (isFocus() && isGloballyMuted()) { 56 Log.i(TAG, "UNMUTING the mic globally in favor of a local call silence") 57 mAudioManager.setMicrophoneMute(false) 58 shouldRemute = true 59 } else if (isInactive() && shouldRemute) { 60 Log.i( 61 TAG, 62 "MUTING the mic globally to put the device back in its original state" 63 ) 64 mAudioManager.setMicrophoneMute(true) 65 shouldRemute = false 66 } 67 } 68 } 69 } 70 isFocusnull71 private fun isFocus(): Boolean { 72 return mCallState.isFocusState() 73 } 74 isInactivenull75 private fun isInactive(): Boolean { 76 return mCallState.isInactiveState() 77 } 78 isGloballyMutednull79 private fun isGloballyMuted(): Boolean { 80 return mIsGloballyMuted 81 } 82 maybeUpdateGlobalMuteStatenull83 private fun maybeUpdateGlobalMuteState(state: CallStateEvent) { 84 if (state.isGlobalMuteState()) { 85 mIsGloballyMuted = state.isMuted() 86 } 87 } 88 maybeUpdateCallControlStatenull89 private fun maybeUpdateCallControlState(state: CallStateEvent) { 90 if (state.isCallControlState()) { 91 mCallState = state 92 } 93 } 94 95 companion object { 96 internal const val VERSION = 1 97 val TAG: String = LocalCallSilenceExtensionImpl::class.java.simpleName 98 } 99 100 internal val isLocallySilenced: MutableStateFlow<Boolean> = 101 MutableStateFlow(initialSilenceState) 102 103 /** 104 * This method is called by the VoIP application whenever the VoIP application wants to update 105 * all the remote surfaces 106 */ updateIsLocallySilencednull107 override suspend fun updateIsLocallySilenced(isSilenced: Boolean) { 108 Log.i(TAG, "updateIsLocallySilenced: isSilenced=[$isSilenced]") 109 isLocallySilenced.emit(isSilenced) 110 } 111 onExchangeStartednull112 internal fun onExchangeStarted(callbacks: CapabilityExchangeRepository): Capability { 113 callbacks.onCreateLocalCallSilenceExtension = ::onCreateLocalSilenceExtension 114 return Capability().apply { 115 featureId = Extensions.LOCAL_CALL_SILENCE 116 featureVersion = VERSION 117 supportedActions = IntArray(0) 118 } 119 } 120 onCreateLocalSilenceExtensionnull121 private fun onCreateLocalSilenceExtension( 122 coroutineScope: CoroutineScope, 123 remoteActions: Set<Int>, 124 binder: LocalCallSilenceStateListenerRemote 125 ) { 126 Log.d(TAG, "onCreateLocalSilenceExtension: actions=$remoteActions") 127 // Synchronize initial state with remote 128 binder.updateIsLocallySilenced(initialSilenceState) 129 // Setup listeners for changes to state 130 isLocallySilenced 131 .drop(1) // drop the first value since the sync was already sent out 132 .onEach { 133 // send all updates to the remote surfaces 134 // VoIP --> ICS 135 binder.updateIsLocallySilenced(it) 136 } 137 .launchIn(coroutineScope) 138 // hook up the callbacks so the remote ICS can update this impl 139 val callbackRepository = LocalCallSilenceCallbackRepository(coroutineScope) 140 callbackRepository.localCallSilenceCallback = ::localCallSilenceStateChanged 141 binder.finishSync(callbackRepository.eventListener) 142 } 143 144 /** 145 * This method is the entry point when the remote surface wants to update this impl. This 146 * updates the block in the VoIP app where the extension was added. 147 */ localCallSilenceStateChangednull148 private suspend fun localCallSilenceStateChanged(isSilenced: Boolean) { 149 Log.i(TAG, "localCallSilenceStateChanged: isSilenced=[$isSilenced]") 150 onLocalSilenceUpdate(isSilenced) 151 } 152 } 153