• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2022, 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 com.android.server.telecom;
18 
19 import android.content.Context;
20 import android.bluetooth.BluetoothDevice;
21 import android.os.Bundle;
22 import android.os.ParcelUuid;
23 import android.os.ResultReceiver;
24 import android.telecom.CallAudioState;
25 import android.telecom.CallEndpoint;
26 import android.telecom.CallEndpointException;
27 import android.telecom.Log;
28 
29 import com.android.internal.annotations.VisibleForTesting;
30 
31 import java.util.HashMap;
32 import java.util.Map;
33 import java.util.HashSet;
34 import java.util.Set;
35 import java.util.concurrent.CompletableFuture;
36 import java.util.concurrent.TimeUnit;
37 
38 /**
39  * Provides to {@link CallsManager} the service that can request change of CallEndpoint to the
40  * {@link CallAudioManager}. And notify change of CallEndpoint status to {@link CallsManager}
41  */
42 public class CallEndpointController extends CallsManagerListenerBase {
43     public static final int CHANGE_TIMEOUT_SEC = 2;
44     public static final int RESULT_REQUEST_SUCCESS = 0;
45     public static final int RESULT_ENDPOINT_DOES_NOT_EXIST = 1;
46     public static final int RESULT_REQUEST_TIME_OUT = 2;
47     public static final int RESULT_ANOTHER_REQUEST = 3;
48     public static final int RESULT_UNSPECIFIED_ERROR = 4;
49 
50     private final Context mContext;
51     private final CallsManager mCallsManager;
52     private final HashMap<Integer, Integer> mRouteToTypeMap;
53     private final HashMap<Integer, Integer> mTypeToRouteMap;
54     private final Map<ParcelUuid, String> mBluetoothAddressMap = new HashMap<>();
55     private final Set<CallEndpoint> mAvailableCallEndpoints = new HashSet<>();
56     private CallEndpoint mActiveCallEndpoint;
57     private ParcelUuid mRequestedEndpointId;
58     private CompletableFuture<Integer> mPendingChangeRequest;
59 
CallEndpointController(Context context, CallsManager callsManager)60     public CallEndpointController(Context context, CallsManager callsManager) {
61         mContext = context;
62         mCallsManager = callsManager;
63 
64         mRouteToTypeMap = new HashMap<>(5);
65         mRouteToTypeMap.put(CallAudioState.ROUTE_EARPIECE, CallEndpoint.TYPE_EARPIECE);
66         mRouteToTypeMap.put(CallAudioState.ROUTE_BLUETOOTH, CallEndpoint.TYPE_BLUETOOTH);
67         mRouteToTypeMap.put(CallAudioState.ROUTE_WIRED_HEADSET, CallEndpoint.TYPE_WIRED_HEADSET);
68         mRouteToTypeMap.put(CallAudioState.ROUTE_SPEAKER, CallEndpoint.TYPE_SPEAKER);
69         mRouteToTypeMap.put(CallAudioState.ROUTE_STREAMING, CallEndpoint.TYPE_STREAMING);
70 
71         mTypeToRouteMap = new HashMap<>(5);
72         mTypeToRouteMap.put(CallEndpoint.TYPE_EARPIECE, CallAudioState.ROUTE_EARPIECE);
73         mTypeToRouteMap.put(CallEndpoint.TYPE_BLUETOOTH, CallAudioState.ROUTE_BLUETOOTH);
74         mTypeToRouteMap.put(CallEndpoint.TYPE_WIRED_HEADSET, CallAudioState.ROUTE_WIRED_HEADSET);
75         mTypeToRouteMap.put(CallEndpoint.TYPE_SPEAKER, CallAudioState.ROUTE_SPEAKER);
76         mTypeToRouteMap.put(CallEndpoint.TYPE_STREAMING, CallAudioState.ROUTE_STREAMING);
77     }
78 
79     @VisibleForTesting
getCurrentCallEndpoint()80     public CallEndpoint getCurrentCallEndpoint() {
81         return mActiveCallEndpoint;
82     }
83 
84     @VisibleForTesting
getAvailableEndpoints()85     public Set<CallEndpoint> getAvailableEndpoints() {
86         return mAvailableCallEndpoints;
87     }
88 
requestCallEndpointChange(CallEndpoint endpoint, ResultReceiver callback)89     public void requestCallEndpointChange(CallEndpoint endpoint, ResultReceiver callback) {
90         Log.d(this, "requestCallEndpointChange %s", endpoint);
91         int route = mTypeToRouteMap.get(endpoint.getEndpointType());
92         String bluetoothAddress = getBluetoothAddress(endpoint);
93 
94         if (findMatchingTypeEndpoint(endpoint.getEndpointType()) == null ||
95                 (route == CallAudioState.ROUTE_BLUETOOTH && bluetoothAddress == null)) {
96             callback.send(CallEndpoint.ENDPOINT_OPERATION_FAILED,
97                     getErrorResult(RESULT_ENDPOINT_DOES_NOT_EXIST));
98             return;
99         }
100 
101         if (isCurrentEndpointRequestedEndpoint(route, bluetoothAddress)) {
102             Log.d(this, "requestCallEndpointChange: requested endpoint is already active");
103             callback.send(CallEndpoint.ENDPOINT_OPERATION_SUCCESS, new Bundle());
104             return;
105         }
106 
107         if (mPendingChangeRequest != null && !mPendingChangeRequest.isDone()) {
108             mPendingChangeRequest.complete(RESULT_ANOTHER_REQUEST);
109             mPendingChangeRequest = null;
110             mRequestedEndpointId = null;
111         }
112 
113         mPendingChangeRequest = new CompletableFuture<Integer>()
114                 .completeOnTimeout(RESULT_REQUEST_TIME_OUT, CHANGE_TIMEOUT_SEC, TimeUnit.SECONDS);
115 
116         mPendingChangeRequest.thenAcceptAsync((result) -> {
117             if (result == RESULT_REQUEST_SUCCESS) {
118                 callback.send(CallEndpoint.ENDPOINT_OPERATION_SUCCESS, new Bundle());
119             } else {
120                 callback.send(CallEndpoint.ENDPOINT_OPERATION_FAILED, getErrorResult(result));
121             }
122         });
123         mRequestedEndpointId = endpoint.getIdentifier();
124         mCallsManager.getCallAudioManager().setAudioRoute(route, bluetoothAddress);
125     }
126 
isCurrentEndpointRequestedEndpoint(int requestedRoute, String requestedAddress)127     public boolean isCurrentEndpointRequestedEndpoint(int requestedRoute, String requestedAddress) {
128         if (mCallsManager.getCallAudioManager() == null
129                 || mCallsManager.getCallAudioManager().getCallAudioState() == null) {
130             return false;
131         }
132         CallAudioState currentAudioState = mCallsManager.getCallAudioManager().getCallAudioState();
133         // requested non-bt endpoint is already active
134         if (requestedRoute != CallAudioState.ROUTE_BLUETOOTH &&
135                 requestedRoute == currentAudioState.getRoute()) {
136             return true;
137         }
138         // requested bt endpoint is already active
139         if (requestedRoute == CallAudioState.ROUTE_BLUETOOTH &&
140                 currentAudioState.getActiveBluetoothDevice() != null &&
141                 requestedAddress.equals(
142                         currentAudioState.getActiveBluetoothDevice().getAddress())) {
143             return true;
144         }
145         return false;
146     }
147 
getErrorResult(int result)148     private Bundle getErrorResult(int result) {
149         String message;
150         int resultCode;
151         switch (result) {
152             case RESULT_ENDPOINT_DOES_NOT_EXIST:
153                 message = "Requested CallEndpoint does not exist";
154                 resultCode = CallEndpointException.ERROR_ENDPOINT_DOES_NOT_EXIST;
155                 break;
156             case RESULT_REQUEST_TIME_OUT:
157                 message = "The operation was not completed on time";
158                 resultCode = CallEndpointException.ERROR_REQUEST_TIME_OUT;
159                 break;
160             case RESULT_ANOTHER_REQUEST:
161                 message = "The operation was canceled by another request";
162                 resultCode = CallEndpointException.ERROR_ANOTHER_REQUEST;
163                 break;
164             default:
165                 message = "The operation has failed due to an unknown or unspecified error";
166                 resultCode = CallEndpointException.ERROR_UNSPECIFIED;
167         }
168         CallEndpointException exception = new CallEndpointException(message, resultCode);
169         Bundle extras = new Bundle();
170         extras.putParcelable(CallEndpointException.CHANGE_ERROR, exception);
171         return extras;
172     }
173 
174     @VisibleForTesting
getBluetoothAddress(CallEndpoint endpoint)175     public String getBluetoothAddress(CallEndpoint endpoint) {
176         return mBluetoothAddressMap.get(endpoint.getIdentifier());
177     }
178 
notifyCallEndpointChange()179     private void notifyCallEndpointChange() {
180         if (mActiveCallEndpoint == null) {
181             Log.i(this, "notifyCallEndpointChange, invalid CallEndpoint");
182             return;
183         }
184 
185         if (mRequestedEndpointId != null && mPendingChangeRequest != null &&
186                 mRequestedEndpointId.equals(mActiveCallEndpoint.getIdentifier())) {
187             mPendingChangeRequest.complete(RESULT_REQUEST_SUCCESS);
188             mPendingChangeRequest = null;
189             mRequestedEndpointId = null;
190         }
191         mCallsManager.updateCallEndpoint(mActiveCallEndpoint);
192 
193         Set<Call> calls = mCallsManager.getTrackedCalls();
194         for (Call call : calls) {
195             if (call != null && call.getConnectionService() != null) {
196                 call.getConnectionService().onCallEndpointChanged(call, mActiveCallEndpoint);
197             } else if (call != null && call.getTransactionServiceWrapper() != null) {
198                 call.getTransactionServiceWrapper()
199                         .onCallEndpointChanged(call, mActiveCallEndpoint);
200             }
201         }
202     }
203 
notifyAvailableCallEndpointsChange()204     private void notifyAvailableCallEndpointsChange() {
205         mCallsManager.updateAvailableCallEndpoints(mAvailableCallEndpoints);
206 
207         Set<Call> calls = mCallsManager.getTrackedCalls();
208         for (Call call : calls) {
209             if (call != null && call.getConnectionService() != null) {
210                 call.getConnectionService().onAvailableCallEndpointsChanged(call,
211                         mAvailableCallEndpoints);
212             } else if (call != null && call.getTransactionServiceWrapper() != null) {
213                 call.getTransactionServiceWrapper()
214                         .onAvailableCallEndpointsChanged(call, mAvailableCallEndpoints);
215             }
216         }
217     }
218 
notifyMuteStateChange(boolean isMuted)219     private void notifyMuteStateChange(boolean isMuted) {
220         mCallsManager.updateMuteState(isMuted);
221 
222         Set<Call> calls = mCallsManager.getTrackedCalls();
223         for (Call call : calls) {
224             if (call != null && call.getConnectionService() != null) {
225                 call.getConnectionService().onMuteStateChanged(call, isMuted);
226             } else if (call != null && call.getTransactionServiceWrapper() != null) {
227                 call.getTransactionServiceWrapper().onMuteStateChanged(call, isMuted);
228             }
229         }
230     }
231 
createAvailableCallEndpoints(CallAudioState state)232     private void createAvailableCallEndpoints(CallAudioState state) {
233         Set<CallEndpoint> newAvailableEndpoints = new HashSet<>();
234         Map<ParcelUuid, String> newBluetoothDevices = new HashMap<>();
235 
236         mRouteToTypeMap.forEach((route, type) -> {
237             if ((state.getSupportedRouteMask() & route) != 0) {
238                 if (type == CallEndpoint.TYPE_STREAMING) {
239                     if (state.getRoute() == CallAudioState.ROUTE_STREAMING) {
240                         if (mActiveCallEndpoint == null
241                                 || mActiveCallEndpoint.getEndpointType() != type) {
242                             mActiveCallEndpoint = new CallEndpoint(getEndpointName(type) != null
243                                     ? getEndpointName(type) : "", type);
244                         }
245                     }
246                 } else if (type == CallEndpoint.TYPE_BLUETOOTH) {
247                     for (BluetoothDevice device : state.getSupportedBluetoothDevices()) {
248                         CallEndpoint endpoint = findMatchingBluetoothEndpoint(device);
249                         if (endpoint == null) {
250                             String deviceName = device.getName();
251                             endpoint = new CallEndpoint(
252                                     deviceName != null ? deviceName : "",
253                                     CallEndpoint.TYPE_BLUETOOTH);
254                         }
255                         newAvailableEndpoints.add(endpoint);
256                         newBluetoothDevices.put(endpoint.getIdentifier(), device.getAddress());
257 
258                         BluetoothDevice activeDevice = state.getActiveBluetoothDevice();
259                         if (state.getRoute() == route && device.equals(activeDevice)) {
260                             mActiveCallEndpoint = endpoint;
261                         }
262                     }
263                 } else {
264                     CallEndpoint endpoint = findMatchingTypeEndpoint(type);
265                     if (endpoint == null) {
266                         endpoint = new CallEndpoint(
267                                 getEndpointName(type) != null ? getEndpointName(type) : "", type);
268                     }
269                     newAvailableEndpoints.add(endpoint);
270                     if (state.getRoute() == route) {
271                         mActiveCallEndpoint = endpoint;
272                     }
273                 }
274             }
275         });
276         mAvailableCallEndpoints.clear();
277         mAvailableCallEndpoints.addAll(newAvailableEndpoints);
278         mBluetoothAddressMap.clear();
279         mBluetoothAddressMap.putAll(newBluetoothDevices);
280     }
281 
findMatchingTypeEndpoint(int targetType)282     private CallEndpoint findMatchingTypeEndpoint(int targetType) {
283         for (CallEndpoint endpoint : mAvailableCallEndpoints) {
284             if (endpoint.getEndpointType() == targetType) {
285                 return endpoint;
286             }
287         }
288         return null;
289     }
290 
findMatchingBluetoothEndpoint(BluetoothDevice device)291     private CallEndpoint findMatchingBluetoothEndpoint(BluetoothDevice device) {
292         final String targetAddress = device.getAddress();
293         if (targetAddress != null) {
294             for (CallEndpoint endpoint : mAvailableCallEndpoints) {
295                 final String address = mBluetoothAddressMap.get(endpoint.getIdentifier());
296                 if (targetAddress.equals(address)) {
297                     return endpoint;
298                 }
299             }
300         }
301         return null;
302     }
303 
isAvailableEndpointChanged(CallAudioState oldState, CallAudioState newState)304     private boolean isAvailableEndpointChanged(CallAudioState oldState, CallAudioState newState) {
305         if (oldState == null) {
306             return true;
307         }
308         if ((oldState.getSupportedRouteMask() ^ newState.getSupportedRouteMask()) != 0) {
309             return true;
310         }
311         if (oldState.getSupportedBluetoothDevices().size() !=
312                 newState.getSupportedBluetoothDevices().size()) {
313             return true;
314         }
315         for (BluetoothDevice device : newState.getSupportedBluetoothDevices()) {
316             if (!oldState.getSupportedBluetoothDevices().contains(device)) {
317                 return true;
318             }
319         }
320         return false;
321     }
322 
isEndpointChanged(CallAudioState oldState, CallAudioState newState)323     private boolean isEndpointChanged(CallAudioState oldState, CallAudioState newState) {
324         if (oldState == null) {
325             return true;
326         }
327         if (oldState.getRoute() != newState.getRoute()) {
328             return true;
329         }
330         if (newState.getRoute() == CallAudioState.ROUTE_BLUETOOTH) {
331             if (oldState.getActiveBluetoothDevice() == null) {
332                 if (newState.getActiveBluetoothDevice() == null) {
333                     return false;
334                 }
335                 return true;
336             }
337             return !oldState.getActiveBluetoothDevice().equals(newState.getActiveBluetoothDevice());
338         }
339         return false;
340     }
341 
isMuteStateChanged(CallAudioState oldState, CallAudioState newState)342     private boolean isMuteStateChanged(CallAudioState oldState, CallAudioState newState) {
343         if (oldState == null) {
344             return true;
345         }
346         return oldState.isMuted() != newState.isMuted();
347     }
348 
getEndpointName(int endpointType)349     private CharSequence getEndpointName(int endpointType) {
350         switch (endpointType) {
351             case CallEndpoint.TYPE_EARPIECE:
352                 return mContext.getText(R.string.callendpoint_name_earpiece);
353             case CallEndpoint.TYPE_BLUETOOTH:
354                 return mContext.getText(R.string.callendpoint_name_bluetooth);
355             case CallEndpoint.TYPE_WIRED_HEADSET:
356                 return mContext.getText(R.string.callendpoint_name_wiredheadset);
357             case CallEndpoint.TYPE_SPEAKER:
358                 return mContext.getText(R.string.callendpoint_name_speaker);
359             case CallEndpoint.TYPE_STREAMING:
360                 return mContext.getText(R.string.callendpoint_name_streaming);
361             default:
362                 return mContext.getText(R.string.callendpoint_name_unknown);
363         }
364     }
365 
366     @Override
onCallAudioStateChanged(CallAudioState oldState, CallAudioState newState)367     public void onCallAudioStateChanged(CallAudioState oldState, CallAudioState newState) {
368         Log.i(this, "onCallAudioStateChanged, audioState: %s -> %s", oldState, newState);
369 
370         if (newState == null) {
371             Log.i(this, "onCallAudioStateChanged, invalid audioState");
372             return;
373         }
374 
375         createAvailableCallEndpoints(newState);
376 
377         boolean isforce = true;
378         if (isAvailableEndpointChanged(oldState, newState)) {
379             notifyAvailableCallEndpointsChange();
380             isforce = false;
381         }
382 
383         if (isEndpointChanged(oldState, newState)) {
384             notifyCallEndpointChange();
385             isforce = false;
386         }
387 
388         if (isMuteStateChanged(oldState, newState)) {
389             notifyMuteStateChange(newState.isMuted());
390             isforce = false;
391         }
392 
393         if (isforce) {
394             notifyAvailableCallEndpointsChange();
395             notifyCallEndpointChange();
396             notifyMuteStateChange(newState.isMuted());
397         }
398     }
399 }