1 /*
<lambda>null2  * 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.test.services
18 
19 import android.content.Intent
20 import android.database.ContentObserver
21 import android.graphics.Bitmap
22 import android.graphics.BitmapFactory
23 import android.net.Uri
24 import android.os.Binder
25 import android.os.Handler
26 import android.os.IBinder
27 import android.os.Looper
28 import android.os.ParcelFileDescriptor
29 import android.telecom.Call
30 import android.telecom.CallAudioState
31 import android.telecom.CallEndpoint
32 import android.util.Log
33 import androidx.core.telecom.InCallServiceCompat
34 import androidx.core.telecom.test.Compatibility
35 import androidx.core.telecom.util.ExperimentalAppActions
36 import androidx.lifecycle.lifecycleScope
37 import java.io.IOException
38 import java.util.concurrent.atomic.AtomicInteger
39 import kotlinx.coroutines.Job
40 import kotlinx.coroutines.cancel
41 import kotlinx.coroutines.flow.StateFlow
42 import kotlinx.coroutines.flow.combine
43 import kotlinx.coroutines.launch
44 
45 /**
46  * Implements the InCallService for this application as well as a local ICS binder for activities to
47  * bind to this service locally and receive state changes.
48  */
49 class InCallServiceImpl : LocalIcsBinder, InCallServiceCompat() {
50     private companion object {
51         const val LOG_TAG = "InCallServiceImpl"
52     }
53 
54     private val localBinder =
55         object : LocalIcsBinder.Connector, Binder() {
56             override fun getService(): LocalIcsBinder {
57                 return this@InCallServiceImpl
58             }
59         }
60 
61     private val currId = AtomicInteger(1)
62     private val mCallDataAggregator = CallDataAggregator()
63     override val callData: StateFlow<List<CallData>> = mCallDataAggregator.callDataState
64     private val mMuteStateResolver = MuteStateResolver()
65 
66     @Suppress("DEPRECATION")
67     private val mCallAudioRouteResolver =
68         CallAudioRouteResolver(
69             lifecycleScope,
70             callData,
71             ::setAudioRoute,
72             ::requestBluetoothAudio,
73             onRequestEndpointChange = { ep, e, or ->
74                 Compatibility.requestCallEndpointChange(this@InCallServiceImpl, ep, e, or)
75             }
76         )
77     override val isMuted: StateFlow<Boolean> = mMuteStateResolver.muteState
78     override val currentAudioEndpoint: StateFlow<CallAudioEndpoint?> =
79         mCallAudioRouteResolver.currentEndpoint
80     override val availableAudioEndpoints: StateFlow<List<CallAudioEndpoint>> =
81         mCallAudioRouteResolver.availableEndpoints
82 
83     override fun onBind(intent: Intent?): IBinder? {
84         if (intent == null) {
85             Log.w(LOG_TAG, "onBind: null intent, returning")
86             return null
87         }
88         if (SERVICE_INTERFACE == intent.action) {
89             Log.d(LOG_TAG, "onBind: Received telecom interface.")
90             return super.onBind(intent)
91         }
92         Log.d(LOG_TAG, "onBind: Received bind request from ${intent.`package`}")
93         return localBinder
94     }
95 
96     override fun onUnbind(intent: Intent?): Boolean {
97         Log.d(LOG_TAG, "onUnbind: Received unbind request from $intent")
98         // work around a stupid bug where InCallService assumes that the unbind request can only
99         // come from telecom
100         if (intent?.action != null) {
101             return super.onUnbind(intent)
102         }
103         return false
104     }
105 
106     override fun onChangeMuteState(isMuted: Boolean) {
107         setMuted(isMuted)
108     }
109 
110     override suspend fun onChangeAudioRoute(id: String) {
111         mCallAudioRouteResolver.onChangeAudioRoute(id)
112     }
113 
114     private fun readCallIconUriFromFile(uri: Uri): Bitmap? {
115         var parcelFileDescriptor: ParcelFileDescriptor? = null
116         return try {
117             parcelFileDescriptor = contentResolver.openFileDescriptor(uri, "r") // "r" for read mode
118             parcelFileDescriptor?.let {
119                 val bitmap = BitmapFactory.decodeFileDescriptor(it.fileDescriptor)
120                 bitmap // Return the bitmap (Kotlin's last expression is the return value)
121             }
122         } catch (e: Exception) {
123             e.printStackTrace()
124             null
125         } finally {
126             try {
127                 parcelFileDescriptor?.close() // ALWAYS close the ParcelFileDescriptor
128             } catch (e: IOException) {
129                 e.printStackTrace()
130             }
131         }
132     }
133 
134     inner class MyContentObserver(
135         handler: Handler,
136         private val mUri: Uri,
137         private val mCallIconDataEmitter: CallIconExtensionDataEmitter
138     ) : ContentObserver(handler) {
139 
140         override fun onChange(selfChange: Boolean) {
141             onChange(selfChange, null)
142         }
143 
144         override fun onChange(selfChange: Boolean, uri: Uri?) {
145             Log.d(LOG_TAG, "Content changed for URI: $mUri")
146             if (uri != null) {
147                 val bitMap = readCallIconUriFromFile(uri)
148                 mCallIconDataEmitter.onVoipAppUpdate(bitMap!!)
149             }
150         }
151     }
152 
153     @OptIn(ExperimentalAppActions::class)
154     override fun onCallAdded(call: Call?) {
155         if (call == null) return
156         var callJob: Job? = null
157         callJob =
158             lifecycleScope.launch {
159                 connectExtensions(call) {
160                     val participantsEmitter = ParticipantExtensionDataEmitter()
161                     val participantExtension =
162                         addParticipantExtension(
163                             onActiveParticipantChanged =
164                                 participantsEmitter::onActiveParticipantChanged,
165                             onParticipantsUpdated = participantsEmitter::onParticipantsChanged
166                         )
167 
168                     val meetingSummaryEmitter = MeetingSummaryExtensionDataEmitter()
169                     addMeetingSummaryExtension(
170                         onCurrentSpeakerChanged = meetingSummaryEmitter::onCurrentSpeakerChanged,
171                         onParticipantCountChanged = meetingSummaryEmitter::onParticipantCountChanged
172                     )
173 
174                     val kickParticipantDataEmitter = KickParticipantDataEmitter()
175                     val kickParticipantAction = participantExtension.addKickParticipantAction()
176 
177                     val raiseHandDataEmitter = RaiseHandDataEmitter()
178                     val raiseHandAction =
179                         participantExtension.addRaiseHandAction(
180                             raiseHandDataEmitter::onRaisedHandsChanged
181                         )
182 
183                     val localCallSilenceDataEmitter = LocalCallSilenceExtensionDataEmitter()
184                     val localCallSilenceExtension =
185                         addLocalCallSilenceExtension(
186                             onIsLocallySilencedUpdated =
187                                 localCallSilenceDataEmitter::onVoipAppUpdate
188                         )
189 
190                     val callIconDataEmitter = CallIconExtensionDataEmitter()
191 
192                     var contentObserver: MyContentObserver? = null
193                     var observedUri: Uri? = null
194                     addCallIconSupport { newUri ->
195                         // Check if the URI has changed.  No need to do anything if it's the same.
196                         if (newUri != observedUri) {
197                             // Unregister the previous observer if it exists.  Use safe call ?. and
198                             // let
199                             // the platform handle nulls gracefully. No need for !!.
200                             contentObserver?.let { contentResolver.unregisterContentObserver(it) }
201                             // Create a new observer.  Use a local variable for clarity.
202                             val newObserver =
203                                 MyContentObserver(
204                                     Handler(Looper.getMainLooper()),
205                                     newUri,
206                                     callIconDataEmitter
207                                 )
208                             // Register the new observer.
209                             contentResolver.registerContentObserver(newUri, false, newObserver)
210                             // Update the tracked observer and URI.
211                             contentObserver = newObserver
212                             observedUri = newUri
213                             // Read the call icon and emit the update.  Use let for concise null
214                             // check.
215                             readCallIconUriFromFile(newUri)?.let { bitmap ->
216                                 callIconDataEmitter.onVoipAppUpdate(bitmap)
217                             }
218                         }
219                     }
220 
221                     onConnected {
222                         val callData = CallDataEmitter(IcsCall(currId.getAndAdd(1), call)).collect()
223 
224                         val meetingSummaryData = meetingSummaryEmitter.collect()
225 
226                         val participantData =
227                             participantsEmitter.collect(
228                                 participantExtension.isSupported,
229                                 raiseHandDataEmitter.collect(raiseHandAction),
230                                 kickParticipantDataEmitter.collect(kickParticipantAction)
231                             )
232 
233                         val localCallSilenceData =
234                             localCallSilenceDataEmitter.collect(localCallSilenceExtension)
235 
236                         val callIconData = callIconDataEmitter.collect()
237 
238                         val fullData =
239                             combine(
240                                 callData,
241                                 meetingSummaryData,
242                                 participantData,
243                                 localCallSilenceData,
244                                 callIconData
245                             ) { cd, summary, partData, silenceData, iconData ->
246                                 CallData(cd, summary, partData, silenceData, iconData)
247                             }
248                         mCallDataAggregator.watch(this@launch, fullData)
249                     }
250                 }
251                 callJob?.cancel("Call Disconnected")
252                 Log.d(LOG_TAG, "onCallAdded: connectedExtensions complete")
253             }
254     }
255 
256     @Deprecated("Deprecated in API 34")
257     override fun onCallAudioStateChanged(audioState: CallAudioState?) {
258         mMuteStateResolver.onCallAudioStateChanged(audioState)
259         mCallAudioRouteResolver.onCallAudioStateChanged(audioState)
260     }
261 
262     override fun onMuteStateChanged(isMuted: Boolean) {
263         mMuteStateResolver.onMuteStateChanged(isMuted)
264     }
265 
266     override fun onCallEndpointChanged(callEndpoint: CallEndpoint) {
267         mCallAudioRouteResolver.onCallEndpointChanged(callEndpoint)
268     }
269 
270     override fun onAvailableCallEndpointsChanged(availableEndpoints: MutableList<CallEndpoint>) {
271         mCallAudioRouteResolver.onAvailableCallEndpointsChanged(availableEndpoints)
272     }
273 }
274