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

Commit 8b0815c4 authored by Pranav Madapurmath's avatar Pranav Madapurmath
Browse files

Resolve CallAudioRouteController issues.

This CL resolves recent bugs that have been filed around the refactored
audio route changes based on the initial fix that was put up. Namely, it
takes care of a stack overflow issue with SPEAKER_ON, resolves a NPE
with the LE audio service being unavailable, handles late SCO audio
connected messages received via BluetoothStateReceiver, handles removing
BT routes when there's a service disconnect from the
BluetoothProfileServiceListener, avoids processing duplicate pending
messages in PendingAudioRoute, ensures that video calls are defaulted
to speaker (unless a wired headset or BT device is connected), ensures
that mute functionality is working as intended both during and at the
end of a call (reset mute), and in the case that BT_AUDIO_CONNECTED is
processed before the HFP device is connected, ensures that we don't wait
to process it again.

There are still known issues around WhatsApp calls that occur as a
result of timing issues that are due to how the new code is structured
which will be investigated in a subsequent CL. Use cases involving
multiple BT devices have not been verified as there is currently an
issue with the bluetooth stack that is blocking Telecom, which is also
being investigated.

Bug: 328287261
Test: Manual tests with single BT scenarios (LE/HFP) in simple audio
routing switching cases as well as toggling on/off bluetooth and
disconnecting/reconnecting device during oncall. Also ran through MO/MT
cases to ensure that BT audio is connected accordingly during subsequent
calls.
Test: atest CallAudioRouteControllerTest

Change-Id: Ief1f0fe5e0b311594087414ff6a9084063468c73
parent 058c4154
Loading
Loading
Loading
Loading
+15 −15
Original line number Diff line number Diff line
@@ -28,6 +28,7 @@ import android.bluetooth.BluetoothStatusCodes;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.telecom.Log;
import android.util.Pair;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.telecom.bluetooth.BluetoothRouteManager;
@@ -226,7 +227,7 @@ public class AudioRoute {
        AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.put(TYPE_BLUETOOTH_LE, bluetoothLeDeviceInfoTypes);
    }

    int getType() {
    public int getType() {
        return mAudioRouteType;
    }

@@ -237,7 +238,7 @@ public class AudioRoute {
    // Invoked when entered pending route whose dest route is this route
    void onDestRouteAsPendingRoute(boolean active, PendingAudioRoute pendingAudioRoute,
            BluetoothDevice device, AudioManager audioManager,
            BluetoothRouteManager bluetoothRouteManager) {
            BluetoothRouteManager bluetoothRouteManager, boolean isScoAudioConnected) {
        Log.i(this, "onDestRouteAsPendingRoute: active (%b), type (%d)", active, mAudioRouteType);
        if (pendingAudioRoute.isActive() && !active) {
            clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager, audioManager);
@@ -251,20 +252,19 @@ public class AudioRoute {
                    // Check if the communication device was set for the device, even if
                    // BluetoothHeadset#connectAudio reports that the SCO connection wasn't
                    // successfully established.
                    boolean scoConnected = audioManager.getCommunicationDevice().equals(mInfo);
                    if (connectedBtAudio || scoConnected) {
                    if (connectedBtAudio || isScoAudioConnected) {
                        pendingAudioRoute.setCommunicationDeviceType(mAudioRouteType);
                        if (!isScoAudioConnected) {
                            pendingAudioRoute.addMessage(BT_AUDIO_CONNECTED, mBluetoothAddress);
                        }
                    if (connectedBtAudio) {
                        pendingAudioRoute.addMessage(BT_AUDIO_CONNECTED);
                    } else if (!scoConnected) {
                        pendingAudioRoute.onMessageReceived(
                                PENDING_ROUTE_FAILED, mBluetoothAddress);
                    } else {
                        pendingAudioRoute.onMessageReceived(new Pair<>(PENDING_ROUTE_FAILED,
                                mBluetoothAddress), mBluetoothAddress);
                    }
                    return;
                }
            } else if (mAudioRouteType == TYPE_SPEAKER) {
                pendingAudioRoute.addMessage(SPEAKER_ON);
                pendingAudioRoute.addMessage(SPEAKER_ON, null);
            }

            boolean result = false;
@@ -291,7 +291,7 @@ public class AudioRoute {
            // before being able to successfully set the communication device. Refrain from sending
            // pending route failed message for BT route until the second attempt fails.
            if (!result && !BT_AUDIO_ROUTE_TYPES.contains(mAudioRouteType)) {
                pendingAudioRoute.onMessageReceived(PENDING_ROUTE_FAILED, null);
                pendingAudioRoute.onMessageReceived(new Pair<>(PENDING_ROUTE_FAILED, null), null);
            }
        }
    }
@@ -303,13 +303,13 @@ public class AudioRoute {
        Log.i(this, "onOrigRouteAsPendingRoute: active (%b), type (%d)", active, mAudioRouteType);
        if (active) {
            if (mAudioRouteType == TYPE_SPEAKER) {
                pendingAudioRoute.addMessage(SPEAKER_OFF);
                pendingAudioRoute.addMessage(SPEAKER_OFF, null);
            }
            int result = clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager,
                    audioManager);
            // Only send BT_AUDIO_DISCONNECTED for SCO if disconnect was successful.
            if (mAudioRouteType == TYPE_BLUETOOTH_SCO && result == BluetoothStatusCodes.SUCCESS) {
                pendingAudioRoute.addMessage(BT_AUDIO_DISCONNECTED);
                pendingAudioRoute.addMessage(BT_AUDIO_DISCONNECTED, mBluetoothAddress);
            }
        }
    }
@@ -370,7 +370,7 @@ public class AudioRoute {
        return success;
    }

    private int clearCommunicationDevice(PendingAudioRoute pendingAudioRoute,
    int clearCommunicationDevice(PendingAudioRoute pendingAudioRoute,
            BluetoothRouteManager bluetoothRouteManager, AudioManager audioManager) {
        // Try to see if there's a previously set device for communication that should be cleared.
        // This only serves to help in the SCO case to ensure that we disconnect the headset.
+72 −24
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.server.telecom;

import static com.android.server.telecom.AudioRoute.BT_AUDIO_ROUTE_TYPES;
import static com.android.server.telecom.AudioRoute.TYPE_INVALID;
import static com.android.server.telecom.AudioRoute.TYPE_SPEAKER;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
@@ -40,7 +41,9 @@ import android.os.RemoteException;
import android.telecom.CallAudioState;
import android.telecom.Log;
import android.telecom.Logging.Session;
import android.telecom.VideoProfile;
import android.util.ArrayMap;
import android.util.Pair;

import androidx.annotation.NonNull;

@@ -94,7 +97,9 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {
    private StatusBarNotifier mStatusBarNotifier;
    private FeatureFlags mFeatureFlags;
    private int mFocusType;
    private boolean mIsScoAudioConnected;
    private final Object mLock = new Object();
    private final TelecomSystem.SyncRoot mTelecomLock;
    private final BroadcastReceiver mSpeakerPhoneChangeReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
@@ -105,7 +110,9 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {
                        AudioDeviceInfo info = mAudioManager.getCommunicationDevice();
                        if ((info != null) &&
                                (info.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER)) {
                            if (mCurrentRoute.getType() != AudioRoute.TYPE_SPEAKER) {
                                sendMessageWithSessionInfo(SPEAKER_ON);
                            }
                        } else {
                            sendMessageWithSessionInfo(SPEAKER_OFF);
                        }
@@ -171,6 +178,8 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {
        mStatusBarNotifier = statusBarNotifier;
        mFeatureFlags = featureFlags;
        mFocusType = NO_FOCUS;
        mIsScoAudioConnected = false;
        mTelecomLock = callsManager.getLock();
        HandlerThread handlerThread = new HandlerThread(this.getClass().getSimpleName());
        handlerThread.start();

@@ -282,7 +291,7 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {
                            handleMuteChanged(false);
                            break;
                        case MUTE_EXTERNALLY_CHANGED:
                            handleMuteChanged(mAudioManager.isMasterMute());
                            handleMuteChanged(mAudioManager.isMicrophoneMute());
                            break;
                        case SWITCH_FOCUS:
                            focus = msg.arg1;
@@ -455,11 +464,13 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {
            Log.i(this, "Override current pending route destination from %s(active=%b) to "
                            + "%s(active=%b)",
                    mPendingAudioRoute.getDestRoute(), mIsActive, destRoute, active);
            // Ensure we don't keep waiting for SPEAKER_ON if dest route gets overridden.
            if (active && mPendingAudioRoute.getDestRoute().getType() == TYPE_SPEAKER) {
                mPendingAudioRoute.clearPendingMessage(new Pair<>(SPEAKER_ON, null));
            }
            // override pending route while keep waiting for still pending messages for the
            // previous pending route
            mPendingAudioRoute.setOrigRoute(mIsActive, mPendingAudioRoute.getDestRoute());
            mPendingAudioRoute.setDestRoute(active, destRoute, mBluetoothRoutes.get(destRoute));
            mIsActive = active;
        } else {
            if (mCurrentRoute.equals(destRoute) && (mIsActive == active)) {
                return;
@@ -473,10 +484,11 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {
                // Avoid waiting for pending messages for an unavailable route
                mPendingAudioRoute.setOrigRoute(mIsActive, DUMMY_ROUTE);
            }
            mPendingAudioRoute.setDestRoute(active, destRoute, mBluetoothRoutes.get(destRoute));
            mIsActive = active;
            mIsPending = true;
        }
        mPendingAudioRoute.setDestRoute(active, destRoute, mBluetoothRoutes.get(destRoute),
                mIsScoAudioConnected);
        mIsActive = active;
        mPendingAudioRoute.evaluatePendingState();
        postTimeoutMessage();
    }
@@ -599,7 +611,8 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {
            Log.i(this, "handleBtAudioActive: is pending path");
            if (Objects.equals(mPendingAudioRoute.getDestRoute().getBluetoothAddress(),
                    bluetoothDevice.getAddress())) {
                mPendingAudioRoute.onMessageReceived(BT_AUDIO_CONNECTED, null);
                mPendingAudioRoute.onMessageReceived(new Pair<>(BT_AUDIO_CONNECTED,
                        bluetoothDevice.getAddress()), null);
            }
        } else {
            // ignore, not triggered by telecom
@@ -620,7 +633,8 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {
            Log.i(this, "handleBtAudioInactive: is pending path");
            if (Objects.equals(mPendingAudioRoute.getOrigRoute().getBluetoothAddress(),
                    bluetoothDevice.getAddress())) {
                mPendingAudioRoute.onMessageReceived(BT_AUDIO_DISCONNECTED, null);
                mPendingAudioRoute.onMessageReceived(new Pair<>(BT_AUDIO_DISCONNECTED,
                        bluetoothDevice.getAddress()), null);
            }
        } else {
            // ignore, not triggered by telecom
@@ -719,7 +733,7 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {

    private void handleMuteChanged(boolean mute) {
        mIsMute = mute;
        if (mIsMute != mAudioManager.isMasterMute() && mIsActive) {
        if (mIsMute != mAudioManager.isMicrophoneMute() && mIsActive) {
            IAudioService audioService = mAudioServiceFactory.getAudioService();
            Log.i(this, "changing microphone mute state to: %b [serviceIsNull=%b]", mute,
                    audioService == null);
@@ -752,11 +766,10 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {
                }
            }
            case ACTIVE_FOCUS -> {
                // Route to active baseline route, otherwise ignore if route is already active.
                if (!mIsActive) {
                // Route to active baseline route (we may need to change audio route in the case
                // when a video call is put on hold).
                routeTo(true, getBaseRoute(true, null));
            }
            }
            case RINGING_FOCUS -> {
                if (!mIsActive) {
                    AudioRoute route = getBaseRoute(true, null);
@@ -836,7 +849,7 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {
    private void handleSpeakerOn() {
        if (isPending()) {
            Log.i(this, "handleSpeakerOn: sending SPEAKER_ON to pending audio route");
            mPendingAudioRoute.onMessageReceived(SPEAKER_ON, null);
            mPendingAudioRoute.onMessageReceived(new Pair<>(SPEAKER_ON, null), null);
            // Update status bar notification if we are in a call.
            mStatusBarNotifier.notifySpeakerphone(mCallsManager.hasAnyCalls());
        } else {
@@ -854,7 +867,7 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {
    private void handleSpeakerOff() {
        if (isPending()) {
            Log.i(this, "handleSpeakerOff - sending SPEAKER_OFF to pending audio route");
            mPendingAudioRoute.onMessageReceived(SPEAKER_OFF, null);
            mPendingAudioRoute.onMessageReceived(new Pair<>(SPEAKER_OFF, null), null);
            // Update status bar notification
            mStatusBarNotifier.notifySpeakerphone(false);
        } else if (mCurrentRoute.getType() == AudioRoute.TYPE_SPEAKER) {
@@ -878,6 +891,7 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {
            Log.addEvent(mCallsManager.getForegroundCall(), LogUtils.Events.AUDIO_ROUTE,
                    "Entering audio route: " + mCurrentRoute + " (active=" + mIsActive + ")");
            mIsPending = false;
            mPendingAudioRoute.clearPendingMessages();
            onCurrentRouteChanged();
        }
    }
@@ -909,7 +923,8 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {
                    BluetoothDevice deviceToAdd = mBluetoothRoutes.get(route);
                    // Only include the lead device for LE audio (otherwise, the routes will show
                    // two separate devices in the UI).
                    if (route.getType() == AudioRoute.TYPE_BLUETOOTH_LE) {
                    if (route.getType() == AudioRoute.TYPE_BLUETOOTH_LE
                            && getLeAudioService() != null) {
                        int groupId = getLeAudioService().getGroupId(deviceToAdd);
                        if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) {
                            deviceToAdd = getLeAudioService().getConnectedGroupLeadDevice(groupId);
@@ -988,6 +1003,12 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {

    private AudioRoute getPreferredAudioRouteFromDefault(boolean includeBluetooth,
            String btAddressToExclude) {
        boolean skipEarpiece;
        Call foregroundCall = mCallAudioManager.getForegroundCall();
        synchronized (mTelecomLock) {
            skipEarpiece = foregroundCall != null
                    && VideoProfile.isVideo(foregroundCall.getVideoState());
        }
        // Route to earpiece, wired, or speaker route if there are not bluetooth routes or if there
        // are only wearables available.
        AudioRoute activeWatchOrNonWatchDeviceRoute =
@@ -996,7 +1017,17 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {
                || activeWatchOrNonWatchDeviceRoute == null) {
            Log.i(this, "getPreferredAudioRouteFromDefault: Audio routing defaulting to "
                    + "available non-BT route.");
            return mEarpieceWiredRoute != null ? mEarpieceWiredRoute : mSpeakerDockRoute;
            AudioRoute defaultRoute = mEarpieceWiredRoute != null
                    ? mEarpieceWiredRoute
                    : mSpeakerDockRoute;
            // Ensure that we default to speaker route if we're in a video call, but disregard it if
            // a wired headset is plugged in.
            if (skipEarpiece && defaultRoute.getType() == AudioRoute.TYPE_EARPIECE) {
                Log.i(this, "getPreferredAudioRouteFromDefault: Audio routing defaulting to "
                        + "speaker route for video call.");
                defaultRoute = mSpeakerDockRoute;
            }
            return defaultRoute;
        } else {
            // Most recent active route will always be the last in the array (ensure that we don't
            // auto route to a wearable device unless it's already active).
@@ -1042,7 +1073,7 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {
        return mCurrentRoute;
    }

    private AudioRoute getBluetoothRoute(@AudioRoute.AudioRouteType int audioRouteType,
    public AudioRoute getBluetoothRoute(@AudioRoute.AudioRouteType int audioRouteType,
            String address) {
        for (AudioRoute route : mBluetoothRoutes.keySet()) {
            if (route.getType() == audioRouteType && route.getBluetoothAddress().equals(address)) {
@@ -1129,7 +1160,7 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {
            BluetoothDevice device = mBluetoothRoutes.get(route);
            // Skip excluded BT address and LE audio if it's not the lead device.
            if (route.getBluetoothAddress().equals(btAddressToExclude)
                    || isLeAudioNonLeadDevice(route.getType(), device)) {
                    || isLeAudioNonLeadDeviceOrServiceUnavailable(route.getType(), device)) {
                continue;
            }
            // Check if the most recently active device is a watch device.
@@ -1158,7 +1189,8 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {
        for (int i = bluetoothRoutes.size() - 1; i >= 0; i--) {
            AudioRoute route = bluetoothRoutes.get(i);
            // Skip LE route if it's not the lead device.
            if (isLeAudioNonLeadDevice(route.getType(), mBluetoothRoutes.get(route))) {
            if (isLeAudioNonLeadDeviceOrServiceUnavailable(
                    route.getType(), mBluetoothRoutes.get(route))) {
                continue;
            }
            if (!route.getBluetoothAddress().equals(btAddressToExclude)) {
@@ -1168,15 +1200,19 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {
        return null;
    }

    private boolean isLeAudioNonLeadDevice(@AudioRoute.AudioRouteType int type,
    private boolean isLeAudioNonLeadDeviceOrServiceUnavailable(@AudioRoute.AudioRouteType int type,
            BluetoothDevice device) {
        if (type != AudioRoute.TYPE_BLUETOOTH_LE) {
            return false;
        } else if (getLeAudioService() == null) {
            return true;
        }

        int groupId = getLeAudioService().getGroupId(device);
        if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) {
            return !device.getAddress().equals(
                    getLeAudioService().getConnectedGroupLeadDevice(groupId).getAddress());
            BluetoothDevice leadDevice = getLeAudioService().getConnectedGroupLeadDevice(groupId);
            Log.i(this, "Lead device for device (%s) is %s.", device, leadDevice);
            return leadDevice == null || !device.getAddress().equals(leadDevice.getAddress());
        }
        return false;
    }
@@ -1195,6 +1231,18 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {
        mAudioRouteFactory = audioRouteFactory;
    }

    public Map<AudioRoute, BluetoothDevice> getBluetoothRoutes() {
        return mBluetoothRoutes;
    }

    public void overrideIsPending(boolean isPending) {
        mIsPending = isPending;
    }

    public void setIsScoAudioConnected(boolean value) {
        mIsScoAudioConnected = value;
    }

    @VisibleForTesting
    public void setActive(boolean active) {
        if (active) {
+1 −0
Original line number Diff line number Diff line
@@ -659,6 +659,7 @@ public class CallsManager extends Call.ListenerBase
        }
        callAudioRouteAdapter.initialize();
        bluetoothStateReceiver.setCallAudioRouteAdapter(callAudioRouteAdapter);
        bluetoothDeviceManager.setCallAudioRouteAdapter(callAudioRouteAdapter);

        CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter =
                new CallAudioRoutePeripheralAdapter(
+23 −14
Original line number Diff line number Diff line
@@ -22,10 +22,12 @@ import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BASELINE_R
import android.bluetooth.BluetoothDevice;
import android.media.AudioManager;
import android.telecom.Log;
import android.util.ArraySet;
import android.util.Pair;

import com.android.server.telecom.bluetooth.BluetoothRouteManager;

import java.util.ArrayList;
import java.util.Set;

/**
 * Used to represent the intermediate state during audio route switching.
@@ -47,7 +49,7 @@ public class PendingAudioRoute {
     * by new switching request during the ongoing switching
     */
    private AudioRoute mDestRoute;
    private ArrayList<Integer> mPendingMessages;
    private Set<Pair<Integer, String>> mPendingMessages;
    private boolean mActive;
    /**
     * The device that has been set for communication by Telecom
@@ -59,7 +61,7 @@ public class PendingAudioRoute {
        mCallAudioRouteController = controller;
        mAudioManager = audioManager;
        mBluetoothRouteManager = bluetoothRouteManager;
        mPendingMessages = new ArrayList<>();
        mPendingMessages = new ArraySet<>();
        mActive = false;
        mCommunicationDeviceType = AudioRoute.TYPE_INVALID;
    }
@@ -73,24 +75,25 @@ public class PendingAudioRoute {
        return mOrigRoute;
    }

    void setDestRoute(boolean active, AudioRoute destRoute, BluetoothDevice device) {
    void setDestRoute(boolean active, AudioRoute destRoute, BluetoothDevice device,
            boolean isScoAudioConnected) {
        destRoute.onDestRouteAsPendingRoute(active, this, device,
                mAudioManager, mBluetoothRouteManager);
                mAudioManager, mBluetoothRouteManager, isScoAudioConnected);
        mActive = active;
        mDestRoute = destRoute;
    }

    AudioRoute getDestRoute() {
    public AudioRoute getDestRoute() {
        return mDestRoute;
    }

    public void addMessage(int message) {
        mPendingMessages.add(message);
    public void addMessage(int message, String bluetoothDevice) {
        mPendingMessages.add(new Pair<>(message, bluetoothDevice));
    }

    public void onMessageReceived(int message, String btAddressToExclude) {
    public void onMessageReceived(Pair<Integer, String> message, String btAddressToExclude) {
        Log.i(this, "onMessageReceived: message - %s", message);
        if (message == PENDING_ROUTE_FAILED) {
        if (message.first == PENDING_ROUTE_FAILED) {
            // Fallback to base route
            mCallAudioRouteController.sendMessageWithSessionInfo(
                    SWITCH_BASELINE_ROUTE, 0, btAddressToExclude);
@@ -98,7 +101,7 @@ public class PendingAudioRoute {
        }

        // Removes the first occurrence of the specified message from this list, if it is present.
        mPendingMessages.remove((Object) message);
        mPendingMessages.remove(message);
        evaluatePendingState();
    }

@@ -107,9 +110,7 @@ public class PendingAudioRoute {
            mCallAudioRouteController.sendMessageWithSessionInfo(
                    CallAudioRouteAdapter.EXIT_PENDING_ROUTE);
        } else {
            for(Integer i: mPendingMessages) {
                Log.d(this, "evaluatePendingState: pending Messages - %d", i);
            }
            Log.i(this, "evaluatePendingState: mPendingMessages - %s", mPendingMessages);
        }
    }

@@ -117,6 +118,10 @@ public class PendingAudioRoute {
        mPendingMessages.clear();
    }

    public void clearPendingMessage(Pair<Integer, String> message) {
        mPendingMessages.remove(message);
    }

    public boolean isActive() {
        return mActive;
    }
@@ -129,4 +134,8 @@ public class PendingAudioRoute {
            @AudioRoute.AudioRouteType int communicationDeviceType) {
        mCommunicationDeviceType = communicationDeviceType;
    }

    public void overrideDestRoute(AudioRoute route) {
        mDestRoute = route;
    }
}
+59 −5

File changed.

Preview size limit exceeded, changes collapsed.

Loading