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