Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 99050c12 authored by Junho's avatar Junho
Browse files

Implement new APIs to notify call audio routing information as a form of CallEndpoint

Add CallEndpointController to transform the CallAudioState into CallEndpoint and notify InCallService and Connection of changes.

bug: 260952109
Test: unit test
Change-Id: I61130a02499b9538b2558ab78d05d4f5cb2c892e
parent dc90a440
Loading
Loading
Loading
Loading
+12 −0
Original line number Diff line number Diff line
@@ -383,4 +383,16 @@
    <string name="cancel">Cancel</string>
    <!-- Button label for generic back action [CHAR LIMIT=20] -->
    <string name="back">Back</string>
    <!-- The user-visible name of the earpiece type CallEndpoint -->
    <string name="callendpoint_name_earpiece">Earpiece</string>
    <!-- The user-visible name of the bluetooth type CallEndpoint -->
    <string name="callendpoint_name_bluetooth">Bluetooth</string>
    <!-- The user-visible name of the wired headset type CallEndpoint -->
    <string name="callendpoint_name_wiredheadset">Wired headset</string>
    <!-- The user-visible name of the speaker type CallEndpoint -->
    <string name="callendpoint_name_speaker">Speaker</string>
    <!-- The user-visible name of the streaming type CallEndpoint -->
    <string name="callendpoint_name_streaming">External</string>
    <!-- The user-visible name of the unknown new type CallEndpoint -->
    <string name="callendpoint_name_unknown">Unknown</string>
</resources>
+2 −1
Original line number Diff line number Diff line
@@ -410,7 +410,8 @@ public class CallAudioManager extends CallsManagerListenerBase {
     * @param bluetoothAddress the address of the desired bluetooth device, if route is
     * {@link CallAudioState#ROUTE_BLUETOOTH}.
     */
    void setAudioRoute(int route, String bluetoothAddress) {
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED)
    public void setAudioRoute(int route, String bluetoothAddress) {
        Log.v(this, "setAudioRoute, route: %s", CallAudioState.audioRouteToString(route));
        switch (route) {
            case CallAudioState.ROUTE_BLUETOOTH:
+345 −0
Original line number Diff line number Diff line
/*
 * Copyright 2022, The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.telecom;

import android.content.Context;
import android.bluetooth.BluetoothDevice;
import android.os.Bundle;
import android.os.ParcelUuid;
import android.os.ResultReceiver;
import android.telecom.CallAudioState;
import android.telecom.CallEndpoint;
import android.telecom.CallEndpointException;
import android.telecom.Log;
import com.android.internal.annotations.VisibleForTesting;
import java.util.HashMap;
import java.util.Map;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

/**
 * Provides to {@link CallsManager} the service that can request change of CallEndpoint to the
 * {@link CallAudioManager}. And notify change of CallEndpoint status to {@link CallsManager}
 */
public class CallEndpointController extends CallsManagerListenerBase {
    public static final int CHANGE_TIMEOUT_SEC = 2;
    public static final int RESULT_REQUEST_SUCCESS = 0;
    public static final int RESULT_ENDPOINT_DOES_NOT_EXIST = 1;
    public static final int RESULT_REQUEST_TIME_OUT = 2;
    public static final int RESULT_ANOTHER_REQUEST = 3;
    public static final int RESULT_UNSPECIFIED_ERROR = 4;

    private final Context mContext;
    private final CallsManager mCallsManager;
    private final HashMap<Integer, Integer> mRouteToTypeMap;
    private final HashMap<Integer, Integer> mTypeToRouteMap;
    private final Map<ParcelUuid, String> mBluetoothAddressMap = new HashMap<>();
    private final Set<CallEndpoint> mAvailableCallEndpoints = new HashSet<>();
    private CallEndpoint mActiveCallEndpoint;
    private ParcelUuid mRequestedEndpointId;
    private CompletableFuture<Integer> mPendingChangeRequest;

    public CallEndpointController(Context context, CallsManager callsManager) {
        mContext = context;
        mCallsManager = callsManager;

        mRouteToTypeMap = new HashMap<>(5);
        mRouteToTypeMap.put(CallAudioState.ROUTE_EARPIECE, CallEndpoint.TYPE_EARPIECE);
        mRouteToTypeMap.put(CallAudioState.ROUTE_BLUETOOTH, CallEndpoint.TYPE_BLUETOOTH);
        mRouteToTypeMap.put(CallAudioState.ROUTE_WIRED_HEADSET, CallEndpoint.TYPE_WIRED_HEADSET);
        mRouteToTypeMap.put(CallAudioState.ROUTE_SPEAKER, CallEndpoint.TYPE_SPEAKER);

        mTypeToRouteMap = new HashMap<>(5);
        mTypeToRouteMap.put(CallEndpoint.TYPE_EARPIECE, CallAudioState.ROUTE_EARPIECE);
        mTypeToRouteMap.put(CallEndpoint.TYPE_BLUETOOTH, CallAudioState.ROUTE_BLUETOOTH);
        mTypeToRouteMap.put(CallEndpoint.TYPE_WIRED_HEADSET, CallAudioState.ROUTE_WIRED_HEADSET);
        mTypeToRouteMap.put(CallEndpoint.TYPE_SPEAKER, CallAudioState.ROUTE_SPEAKER);
    }

    @VisibleForTesting
    public CallEndpoint getCurrentCallEndpoint() {
        return mActiveCallEndpoint;
    }

    @VisibleForTesting
    public Set<CallEndpoint> getAvailableEndpoints() {
        return mAvailableCallEndpoints;
    }

    public void requestCallEndpointChange(CallEndpoint endpoint, ResultReceiver callback) {
        Log.d(this, "requestCallEndpointChange %s", endpoint);
        int route = mTypeToRouteMap.get(endpoint.getEndpointType());
        String bluetoothAddress = getBluetoothAddress(endpoint);

        if (findMatchingTypeEndpoint(endpoint.getEndpointType()) == null ||
                (route == CallAudioState.ROUTE_BLUETOOTH && bluetoothAddress == null)) {
            callback.send(CallEndpoint.ENDPOINT_OPERATION_FAILED,
                    getErrorResult(RESULT_ENDPOINT_DOES_NOT_EXIST));
            return;
        }

        if (mPendingChangeRequest != null && !mPendingChangeRequest.isDone()) {
            mPendingChangeRequest.complete(RESULT_ANOTHER_REQUEST);
            mPendingChangeRequest = null;
            mRequestedEndpointId = null;
        }

        mPendingChangeRequest = new CompletableFuture<Integer>()
                .completeOnTimeout(RESULT_REQUEST_TIME_OUT, CHANGE_TIMEOUT_SEC, TimeUnit.SECONDS);

        mPendingChangeRequest.thenAcceptAsync((result) -> {
            if (result == RESULT_REQUEST_SUCCESS) {
                callback.send(CallEndpoint.ENDPOINT_OPERATION_SUCCESS, new Bundle());
            } else {
                callback.send(CallEndpoint.ENDPOINT_OPERATION_FAILED, getErrorResult(result));
            }
        });
        mRequestedEndpointId = endpoint.getIdentifier();
        mCallsManager.getCallAudioManager().setAudioRoute(route, bluetoothAddress);
    }

    private Bundle getErrorResult(int result) {
        String message;
        int resultCode;
        switch (result) {
            case RESULT_ENDPOINT_DOES_NOT_EXIST:
                message = "Requested CallEndpoint does not exist";
                resultCode = CallEndpointException.ERROR_ENDPOINT_DOES_NOT_EXIST;
                break;
            case RESULT_REQUEST_TIME_OUT:
                message = "The operation was not completed on time";
                resultCode = CallEndpointException.ERROR_REQUEST_TIME_OUT;
                break;
            case RESULT_ANOTHER_REQUEST:
                message = "The operation was canceled by another request";
                resultCode = CallEndpointException.ERROR_ANOTHER_REQUEST;
                break;
            default:
                message = "The operation has failed due to an unknown or unspecified error";
                resultCode = CallEndpointException.ERROR_UNSPECIFIED;
        }
        CallEndpointException exception = new CallEndpointException(message, resultCode);
        Bundle extras = new Bundle();
        extras.putParcelable(CallEndpointException.CHANGE_ERROR, exception);
        return extras;
    }

    @VisibleForTesting
    public String getBluetoothAddress(CallEndpoint endpoint) {
        return mBluetoothAddressMap.get(endpoint.getIdentifier());
    }

    private void notifyCallEndpointChange() {
        if (mActiveCallEndpoint == null) {
            Log.i(this, "notifyCallEndpointChange, invalid CallEndpoint");
            return;
        }

        if (mRequestedEndpointId != null && mPendingChangeRequest != null &&
                mRequestedEndpointId.equals(mActiveCallEndpoint.getIdentifier())) {
            mPendingChangeRequest.complete(RESULT_REQUEST_SUCCESS);
            mPendingChangeRequest = null;
            mRequestedEndpointId = null;
        }
        mCallsManager.updateCallEndpoint(mActiveCallEndpoint);

        Set<Call> calls = mCallsManager.getTrackedCalls();
        for (Call call : calls) {
            if (call != null && call.getConnectionService() != null) {
                call.getConnectionService().onCallEndpointChanged(call, mActiveCallEndpoint);
            }
        }
    }

    private void notifyAvailableCallEndpointsChange() {
        mCallsManager.updateAvailableCallEndpoints(mAvailableCallEndpoints);

        Set<Call> calls = mCallsManager.getTrackedCalls();
        for (Call call : calls) {
            if (call != null && call.getConnectionService() != null) {
                call.getConnectionService().onAvailableCallEndpointsChanged(call,
                        mAvailableCallEndpoints);
            }
        }
    }

    private void notifyMuteStateChange(boolean isMuted) {
        mCallsManager.updateMuteState(isMuted);

        Set<Call> calls = mCallsManager.getTrackedCalls();
        for (Call call : calls) {
            if (call != null && call.getConnectionService() != null) {
                call.getConnectionService().onMuteStateChanged(call, isMuted);
            }
        }
    }

    private void createAvailableCallEndpoints(CallAudioState state) {
        Set<CallEndpoint> newAvailableEndpoints = new HashSet<>();
        Map<ParcelUuid, String> newBluetoothDevices = new HashMap<>();

        mRouteToTypeMap.forEach((route, type)->{
            if ((state.getSupportedRouteMask() & route) != 0) {
                if (type == CallEndpoint.TYPE_BLUETOOTH) {
                    for (BluetoothDevice device : state.getSupportedBluetoothDevices()) {
                        CallEndpoint endpoint = findMatchingBluetoothEndpoint(device);
                        if (endpoint == null) {
                            endpoint = new CallEndpoint(
                                    device.getName() != null ? device.getName() : "",
                                    CallEndpoint.TYPE_BLUETOOTH);
                        }
                        newAvailableEndpoints.add(endpoint);
                        newBluetoothDevices.put(endpoint.getIdentifier(), device.getAddress());

                        BluetoothDevice activeDevice = state.getActiveBluetoothDevice();
                        if (state.getRoute() == route && device.equals(activeDevice)) {
                            mActiveCallEndpoint = endpoint;
                        }
                    }
                } else {
                    CallEndpoint endpoint = findMatchingTypeEndpoint(type);
                    if (endpoint == null) {
                        endpoint = new CallEndpoint(
                                getEndpointName(type) != null ? getEndpointName(type) : "", type);
                    }
                    newAvailableEndpoints.add(endpoint);
                    if (state.getRoute() == route) {
                        mActiveCallEndpoint = endpoint;
                    }
                }
            }
        });
        mAvailableCallEndpoints.clear();
        mAvailableCallEndpoints.addAll(newAvailableEndpoints);
        mBluetoothAddressMap.clear();
        mBluetoothAddressMap.putAll(newBluetoothDevices);
    }

    private CallEndpoint findMatchingTypeEndpoint(int targetType) {
        for (CallEndpoint endpoint : mAvailableCallEndpoints) {
            if (endpoint.getEndpointType() == targetType) {
                return endpoint;
            }
        }
        return null;
    }

    private CallEndpoint findMatchingBluetoothEndpoint(BluetoothDevice device) {
        final String targetAddress = device.getAddress();
        if (targetAddress != null) {
            for (CallEndpoint endpoint : mAvailableCallEndpoints) {
                final String address = mBluetoothAddressMap.get(endpoint.getIdentifier());
                if (targetAddress.equals(address)) {
                    return endpoint;
                }
            }
        }
        return null;
    }

    private boolean isAvailableEndpointChanged(CallAudioState oldState, CallAudioState newState) {
        if (oldState == null) {
            return true;
        }
        if ((oldState.getSupportedRouteMask() ^ newState.getSupportedRouteMask()) != 0) {
            return true;
        }
        if (oldState.getSupportedBluetoothDevices().size() !=
                newState.getSupportedBluetoothDevices().size()) {
            return true;
        }
        for (BluetoothDevice device : newState.getSupportedBluetoothDevices()) {
            if (!oldState.getSupportedBluetoothDevices().contains(device)) {
                return true;
            }
        }
        return false;
    }

    private boolean isEndpointChanged(CallAudioState oldState, CallAudioState newState) {
        if (oldState == null) {
            return true;
        }
        if (oldState.getRoute() != newState.getRoute()) {
            return true;
        }
        if (newState.getRoute() == CallAudioState.ROUTE_BLUETOOTH) {
            return !oldState.getActiveBluetoothDevice().equals(newState.getActiveBluetoothDevice());
        }
        return false;
    }

    private boolean isMuteStateChanged(CallAudioState oldState, CallAudioState newState) {
        if (oldState == null) {
            return true;
        }
        return oldState.isMuted() != newState.isMuted();
    }

    private CharSequence getEndpointName(int endpointType) {
        switch (endpointType) {
            case CallEndpoint.TYPE_EARPIECE:
                return mContext.getText(R.string.callendpoint_name_earpiece);
            case CallEndpoint.TYPE_BLUETOOTH:
                return mContext.getText(R.string.callendpoint_name_bluetooth);
            case CallEndpoint.TYPE_WIRED_HEADSET:
                return mContext.getText(R.string.callendpoint_name_wiredheadset);
            case CallEndpoint.TYPE_SPEAKER:
                return mContext.getText(R.string.callendpoint_name_speaker);
            case CallEndpoint.TYPE_STREAMING:
                return mContext.getText(R.string.callendpoint_name_streaming);
            default:
                return mContext.getText(R.string.callendpoint_name_unknown);
        }
    }

    @Override
    public void onCallAudioStateChanged(CallAudioState oldState, CallAudioState newState) {
        Log.i(this, "onCallAudioStateChanged, audioState: %s -> %s", oldState, newState);

        if (newState == null) {
            Log.i(this, "onCallAudioStateChanged, invalid audioState");
            return;
        }

        createAvailableCallEndpoints(newState);

        boolean isforce = true;
        if (isAvailableEndpointChanged(oldState, newState)) {
            notifyAvailableCallEndpointsChange();
            isforce = false;
        }

        if (isEndpointChanged(oldState, newState)) {
            notifyCallEndpointChange();
            isforce = false;
        }

        if (isMuteStateChanged(oldState, newState)) {
            notifyMuteStateChange(newState.isMuted());
            isforce = false;
        }

        if (isforce) {
            notifyAvailableCallEndpointsChange();
            notifyCallEndpointChange();
            notifyMuteStateChange(newState.isMuted());
        }
    }
}
 No newline at end of file
+27 −0
Original line number Diff line number Diff line
/*
 * Copyright 2022, The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.telecom;

import android.content.Context;

/**
 * Abstracts out creation of CallEndpointController for unit test purposes.
 */
public interface CallEndpointControllerFactory {
    CallEndpointController create(Context context, TelecomSystem.SyncRoot lock,
            CallsManager callsManager);
}
 No newline at end of file
+46 −1
Original line number Diff line number Diff line
@@ -66,6 +66,7 @@ import android.os.HandlerThread;
import android.os.Looper;
import android.os.PersistableBundle;
import android.os.Process;
import android.os.ResultReceiver;
import android.os.SystemClock;
import android.os.SystemVibrator;
import android.os.Trace;
@@ -77,6 +78,7 @@ import android.provider.CallLog.Calls;
import android.provider.Settings;
import android.sysprop.TelephonyProperties;
import android.telecom.CallAudioState;
import android.telecom.CallEndpoint;
import android.telecom.CallScreeningService;
import android.telecom.CallerInfo;
import android.telecom.Conference;
@@ -174,6 +176,9 @@ public class CallsManager extends Call.ListenerBase
        void onIncomingCallAnswered(Call call);
        void onIncomingCallRejected(Call call, boolean rejectWithMessage, String textMessage);
        void onCallAudioStateChanged(CallAudioState oldAudioState, CallAudioState newAudioState);
        void onCallEndpointChanged(CallEndpoint callEndpoint);
        void onAvailableCallEndpointsChanged(Set<CallEndpoint> availableCallEndpoints);
        void onMuteStateChanged(boolean isMuted);
        void onRingbackRequested(Call call, boolean ringback);
        void onIsConferencedChanged(Call call);
        void onIsVoipAudioModeChanged(Call call);
@@ -376,6 +381,7 @@ public class CallsManager extends Call.ListenerBase
    private final Handler mHandler = new Handler(Looper.getMainLooper());
    private final EmergencyCallHelper mEmergencyCallHelper;
    private final RoleManagerAdapter mRoleManagerAdapter;
    private final CallEndpointController mCallEndpointController;

    private final ConnectionServiceFocusManager.CallsManagerRequester mRequester =
            new ConnectionServiceFocusManager.CallsManagerRequester() {
@@ -496,7 +502,8 @@ public class CallsManager extends Call.ListenerBase
            InCallControllerFactory inCallControllerFactory,
            CallDiagnosticServiceController callDiagnosticServiceController,
            RoleManagerAdapter roleManagerAdapter,
            ToastFactory toastFactory) {
            ToastFactory toastFactory,
            CallEndpointControllerFactory callEndpointControllerFactory) {
        mContext = context;
        mLock = lock;
        mPhoneNumberUtilsAdapter = phoneNumberUtilsAdapter;
@@ -550,6 +557,7 @@ public class CallsManager extends Call.ListenerBase
        mInCallController = inCallControllerFactory.create(context, mLock, this,
                systemStateHelper, defaultDialerCache, mTimeoutsAdapter,
                emergencyCallHelper);
        mCallEndpointController = callEndpointControllerFactory.create(context, mLock, this);
        mCallDiagnosticServiceController = callDiagnosticServiceController;
        mCallDiagnosticServiceController.setInCallTonePlayerFactory(playerFactory);
        mRinger = new Ringer(playerFactory, context, systemSettingsUtil, asyncRingtonePlayer,
@@ -581,6 +589,7 @@ public class CallsManager extends Call.ListenerBase
        mListeners.add(statusBarNotifier);
        mListeners.add(mCallLogManager);
        mListeners.add(mInCallController);
        mListeners.add(mCallEndpointController);
        mListeners.add(mCallDiagnosticServiceController);
        mListeners.add(mCallAudioManager);
        mListeners.add(mCallRecordingTonePlayer);
@@ -1193,6 +1202,10 @@ public class CallsManager extends Call.ListenerBase
        return mInCallController;
    }

    public CallEndpointController getCallEndpointController() {
        return mCallEndpointController;
    }

    EmergencyCallHelper getEmergencyCallHelper() {
        return mEmergencyCallHelper;
    }
@@ -3009,6 +3022,14 @@ public class CallsManager extends Call.ListenerBase
        mCallAudioManager.setAudioRoute(route, bluetoothAddress);
    }

    /**
      * Called by the in-call UI to change the CallEndpoint
      */
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
    public void requestCallEndpointChange(CallEndpoint endpoint, ResultReceiver callback) {
        mCallEndpointController.requestCallEndpointChange(endpoint, callback);
    }

    /** Called by the in-call UI to turn the proximity sensor on. */
    void turnOnProximitySensor() {
        mProximitySensorManager.turnOn();
@@ -3067,6 +3088,30 @@ public class CallsManager extends Call.ListenerBase
        }
    }

    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
    public void updateCallEndpoint(CallEndpoint callEndpoint) {
        Log.v(this, "updateCallEndpoint");
        for (CallsManagerListener listener : mListeners) {
            listener.onCallEndpointChanged(callEndpoint);
        }
    }

    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
    public void updateAvailableCallEndpoints(Set<CallEndpoint> availableCallEndpoints) {
        Log.v(this, "updateAvailableCallEndpoints");
        for (CallsManagerListener listener : mListeners) {
            listener.onAvailableCallEndpointsChanged(availableCallEndpoints);
        }
    }

    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
    public void updateMuteState(boolean isMuted) {
        Log.v(this, "updateMuteState");
        for (CallsManagerListener listener : mListeners) {
            listener.onMuteStateChanged(isMuted);
        }
    }

    /**
     * Called when disconnect tone is started or stopped, including any InCallTone
     * after disconnected call.
Loading