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