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

Commit 2affe32f authored by Junho Yoon's avatar Junho Yoon Committed by Android (Google) Code Review
Browse files

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

parents 557f47c0 99050c12
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
@@ -413,7 +413,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;
    }
@@ -3010,6 +3023,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();
@@ -3068,6 +3089,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