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