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