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.ComponentName
20 import android.content.Context
21 import android.content.Context.BIND_AUTO_CREATE
22 import android.content.Intent
23 import android.content.ServiceConnection
24 import android.os.IBinder
25 import android.util.Log
26 import kotlinx.coroutines.ExperimentalCoroutinesApi
27 import kotlinx.coroutines.flow.Flow
28 import kotlinx.coroutines.flow.MutableStateFlow
29 import kotlinx.coroutines.flow.emptyFlow
30 import kotlinx.coroutines.flow.flatMapConcat
31 import kotlinx.coroutines.flow.getAndUpdate
32 
33 data class LocalServiceConnection(
34     val isConnected: Boolean,
35     val context: Context? = null,
36     val serviceConnection: ServiceConnection? = null,
37     val connection: LocalIcsBinder? = null
38 )
39 
40 /**
41  * Manages the connection for the Provider of "remote" calls, which are calls from the app's
42  * [InCallServiceImpl].
43  */
44 @OptIn(ExperimentalCoroutinesApi::class)
45 class RemoteCallProvider {
46     private companion object {
47         const val LOG_TAG = "RemoteCallProvider"
48     }
49 
50     private val connectedService: MutableStateFlow<LocalServiceConnection> =
51         MutableStateFlow(LocalServiceConnection(false))
52 
53     /** Bind to the app's [LocalIcsBinder.Connector] Service implementation */
54     fun connectService(context: Context) {
55         if (connectedService.value.isConnected) return
56         val intent = Intent(context, InCallServiceImpl::class.java)
57         val serviceConnection =
58             object : ServiceConnection {
59                 override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
60                     if (service == null) return
61                     val localService = service as LocalIcsBinder.Connector
62                     connectedService.value =
63                         LocalServiceConnection(true, context, this, localService.getService())
64                 }
65 
66                 override fun onServiceDisconnected(name: ComponentName?) {
67                     // Unlikely since the Service is in the same process. Re-evaluate if the service
68                     // is moved to another process.
69                     Log.w(LOG_TAG, "onServiceDisconnected: Unexpected call")
70                 }
71             }
72         Log.i(LOG_TAG, "connectToIcs: Binding to ICS locally")
73         context.bindService(intent, serviceConnection, BIND_AUTO_CREATE)
74     }
75 
76     /** Disconnect from the app;s [LocalIcsBinder.Connector] Service implementation */
77     fun disconnectService() {
78         val localConnection = connectedService.getAndUpdate { LocalServiceConnection(false) }
79         localConnection.serviceConnection?.let { conn ->
80             Log.i(LOG_TAG, "connectToIcs: Unbinding to ICS locally")
81             localConnection.context?.unbindService(conn)
82         }
83     }
84 
85     /**
86      * Stream the [CallData] representing each active Call on the device. The Flow will be empty
87      * until the remote Service connects.
88      */
89     fun streamCallData(): Flow<List<CallData>> {
90         return connectedService.flatMapConcat { conn ->
91             if (!conn.isConnected) {
92                 emptyFlow()
93             } else {
94                 conn.connection?.callData ?: emptyFlow()
95             }
96         }
97     }
98 
99     /**
100      * Stream the global mute state of the device. The Flow will be empty until the remote Service
101      * connects.
102      */
103     fun streamMuteData(): Flow<Boolean> {
104         return connectedService.flatMapConcat { conn ->
105             if (!conn.isConnected) {
106                 emptyFlow()
107             } else {
108                 conn.connection?.isMuted ?: emptyFlow()
109             }
110         }
111     }
112 
113     /**
114      * Stream the [CallAudioEndpoint] representing the current endpoint of the active call. The Flow
115      * will be empty until the remote Service connects.
116      */
117     fun streamCurrentEndpointData(): Flow<CallAudioEndpoint?> {
118         return connectedService.flatMapConcat { conn ->
119             if (!conn.isConnected) {
120                 emptyFlow()
121             } else {
122                 conn.connection?.currentAudioEndpoint ?: emptyFlow()
123             }
124         }
125     }
126 
127     /**
128      * Stream the List of [CallAudioEndpoint]s representing the available endpoints of the active
129      * call. The Flow will be empty until the remote Service connects.
130      */
131     fun streamAvailableEndpointData(): Flow<List<CallAudioEndpoint>> {
132         return connectedService.flatMapConcat { conn ->
133             if (!conn.isConnected) {
134                 emptyFlow()
135             } else {
136                 conn.connection?.availableAudioEndpoints ?: emptyFlow()
137             }
138         }
139     }
140 
141     /** Request to change the global mute state of the device. */
142     fun onChangeMuteState(isMuted: Boolean) {
143         val service = connectedService.value
144         if (!service.isConnected) return
145         service.connection?.onChangeMuteState(isMuted)
146     }
147 
148     /** Request to change the current audio route of the active call. */
149     suspend fun onChangeAudioRoute(id: String) {
150         val service = connectedService.value
151         if (!service.isConnected) return
152         service.connection?.onChangeAudioRoute(id)
153     }
154 }
155