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

Commit c0fca287 authored by Alex Dadukin's avatar Alex Dadukin Committed by Android (Google) Code Review
Browse files

Merge "Implement selectRoute in AudioPoliciesDeviceRouteController" into udc-dev

parents 82472631 0f3fc4b2
Loading
Loading
Loading
Loading
+77 −12
Original line number Diff line number Diff line
@@ -62,7 +62,11 @@ import java.util.Objects;
    private final AudioRoutesObserver mAudioRoutesObserver = new AudioRoutesObserver();

    private int mDeviceVolume;

    @NonNull
    private MediaRoute2Info mDeviceRoute;
    @Nullable
    private MediaRoute2Info mSelectedRoute;

    @VisibleForTesting
    /* package */ AudioPoliciesDeviceRouteController(@NonNull Context context,
@@ -91,14 +95,26 @@ import java.util.Objects;
    }

    @Override
    public boolean selectRoute(@Nullable Integer type) {
        // No-op as the controller does not support selection from the outside of the class.
    public synchronized boolean selectRoute(@Nullable Integer type) {
        if (type == null) {
            mSelectedRoute = null;
            return true;
        }

        if (!isDeviceRouteType(type)) {
            return false;
        }

        mSelectedRoute = createRouteFromAudioInfo(type);
        return true;
    }

    @Override
    @NonNull
    public synchronized MediaRoute2Info getDeviceRoute() {
        if (mSelectedRoute != null) {
            return mSelectedRoute;
        }
        return mDeviceRoute;
    }

@@ -109,6 +125,13 @@ import java.util.Objects;
        }

        mDeviceVolume = volume;

        if (mSelectedRoute != null) {
            mSelectedRoute = new MediaRoute2Info.Builder(mSelectedRoute)
                    .setVolume(volume)
                    .build();
        }

        mDeviceRoute = new MediaRoute2Info.Builder(mDeviceRoute)
                .setVolume(volume)
                .build();
@@ -116,29 +139,47 @@ import java.util.Objects;
        return true;
    }

    @NonNull
    private MediaRoute2Info createRouteFromAudioInfo(@Nullable AudioRoutesInfo newRoutes) {
        int name = R.string.default_audio_route_name;
        int type = TYPE_BUILTIN_SPEAKER;

        if (newRoutes != null) {
            if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADPHONES) != 0) {
                type = TYPE_WIRED_HEADPHONES;
                name = R.string.default_audio_route_name_headphones;
            } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADSET) != 0) {
                type = TYPE_WIRED_HEADSET;
                name = R.string.default_audio_route_name_headphones;
            } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) {
                type = TYPE_DOCK;
                name = R.string.default_audio_route_name_dock_speakers;
            } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HDMI) != 0) {
                type = TYPE_HDMI;
                name = R.string.default_audio_route_name_external_device;
            } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_USB) != 0) {
                type = TYPE_USB_DEVICE;
                name = R.string.default_audio_route_name_usb;
            }
        }

        return createRouteFromAudioInfo(type);
    }

    @NonNull
    private MediaRoute2Info createRouteFromAudioInfo(@MediaRoute2Info.Type int type) {
        int name = R.string.default_audio_route_name;

        switch (type) {
            case TYPE_WIRED_HEADPHONES:
            case TYPE_WIRED_HEADSET:
                name = R.string.default_audio_route_name_headphones;
                break;
            case TYPE_DOCK:
                name = R.string.default_audio_route_name_dock_speakers;
                break;
            case TYPE_HDMI:
                name = R.string.default_audio_route_name_external_device;
                break;
            case TYPE_USB_DEVICE:
                name = R.string.default_audio_route_name_usb;
                break;
        }

        synchronized (this) {
            return new MediaRoute2Info.Builder(
                    DEVICE_ROUTE_ID, mContext.getResources().getText(name).toString())
@@ -156,19 +197,43 @@ import java.util.Objects;
        }
    }

    private void notifyDeviceRouteUpdate(@NonNull MediaRoute2Info deviceRoute) {
        mOnDeviceRouteChangedListener.onDeviceRouteChanged(deviceRoute);
    /**
     * Checks if the given type is a device route.
     *
     * <p>Device route means a route which is either built-in or wired to the current device.
     *
     * @param type specifies the type of the device.
     * @return {@code true} if the device is wired or built-in and {@code false} otherwise.
     */
    private boolean isDeviceRouteType(@MediaRoute2Info.Type int type) {
        switch (type) {
            case TYPE_BUILTIN_SPEAKER:
            case TYPE_WIRED_HEADPHONES:
            case TYPE_WIRED_HEADSET:
            case TYPE_DOCK:
            case TYPE_HDMI:
            case TYPE_USB_DEVICE:
                return true;
            default:
                return false;
        }
    }

    private class AudioRoutesObserver extends IAudioRoutesObserver.Stub {

        @Override
        public void dispatchAudioRoutesChanged(AudioRoutesInfo newAudioRoutes) {
            boolean isDeviceRouteChanged;
            MediaRoute2Info deviceRoute = createRouteFromAudioInfo(newAudioRoutes);

            synchronized (AudioPoliciesDeviceRouteController.this) {
                mDeviceRoute = deviceRoute;
                isDeviceRouteChanged = mSelectedRoute == null;
            }

            if (isDeviceRouteChanged) {
                mOnDeviceRouteChangedListener.onDeviceRouteChanged(deviceRoute);
            }
            notifyDeviceRouteUpdate(deviceRoute);
        }
    }

+247 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.media;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;

import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.when;

import android.content.Context;
import android.content.res.Resources;
import android.media.AudioManager;
import android.media.AudioRoutesInfo;
import android.media.IAudioRoutesObserver;
import android.media.MediaRoute2Info;
import android.os.RemoteException;

import com.android.internal.R;
import com.android.server.audio.AudioService;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

@RunWith(JUnit4.class)
public class AudioPoliciesDeviceRouteControllerTest {

    private static final String ROUTE_NAME_DEFAULT = "default";
    private static final String ROUTE_NAME_DOCK = "dock";
    private static final String ROUTE_NAME_HEADPHONES = "headphones";

    private static final int VOLUME_SAMPLE_1 = 25;

    @Mock
    private Context mContext;
    @Mock
    private Resources mResources;
    @Mock
    private AudioManager mAudioManager;
    @Mock
    private AudioService mAudioService;
    @Mock
    private DeviceRouteController.OnDeviceRouteChangedListener mOnDeviceRouteChangedListener;

    @Captor
    private ArgumentCaptor<IAudioRoutesObserver.Stub> mAudioRoutesObserverCaptor;

    private AudioPoliciesDeviceRouteController mController;

    private IAudioRoutesObserver.Stub mAudioRoutesObserver;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);

        when(mContext.getResources()).thenReturn(mResources);
        when(mResources.getText(anyInt())).thenReturn(ROUTE_NAME_DEFAULT);

        // Setting built-in speaker as default speaker.
        AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo();
        audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_SPEAKER;
        when(mAudioService.startWatchingRoutes(mAudioRoutesObserverCaptor.capture()))
                .thenReturn(audioRoutesInfo);

        mController = new AudioPoliciesDeviceRouteController(
                mContext, mAudioManager, mAudioService, mOnDeviceRouteChangedListener);

        mAudioRoutesObserver = mAudioRoutesObserverCaptor.getValue();
    }

    @Test
    public void getDeviceRoute_noSelectedRoutes_returnsDefaultDevice() {
        MediaRoute2Info route2Info = mController.getDeviceRoute();

        assertThat(route2Info.getName()).isEqualTo(ROUTE_NAME_DEFAULT);
        assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_BUILTIN_SPEAKER);
    }

    @Test
    public void getDeviceRoute_audioRouteHasChanged_returnsRouteFromAudioService() {
        when(mResources.getText(R.string.default_audio_route_name_headphones))
                .thenReturn(ROUTE_NAME_HEADPHONES);

        AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo();
        audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_HEADPHONES;
        callAudioRoutesObserver(audioRoutesInfo);

        MediaRoute2Info route2Info = mController.getDeviceRoute();
        assertThat(route2Info.getName()).isEqualTo(ROUTE_NAME_HEADPHONES);
        assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_WIRED_HEADPHONES);
    }

    @Test
    public void getDeviceRoute_selectDevice_returnsSelectedRoute() {
        when(mResources.getText(R.string.default_audio_route_name_dock_speakers))
                .thenReturn(ROUTE_NAME_DOCK);

        mController.selectRoute(MediaRoute2Info.TYPE_DOCK);

        MediaRoute2Info route2Info = mController.getDeviceRoute();
        assertThat(route2Info.getName()).isEqualTo(ROUTE_NAME_DOCK);
        assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_DOCK);
    }

    @Test
    public void getDeviceRoute_hasSelectedAndAudioServiceRoutes_returnsSelectedRoute() {
        when(mResources.getText(R.string.default_audio_route_name_headphones))
                .thenReturn(ROUTE_NAME_HEADPHONES);
        when(mResources.getText(R.string.default_audio_route_name_dock_speakers))
                .thenReturn(ROUTE_NAME_DOCK);

        AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo();
        audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_HEADPHONES;
        callAudioRoutesObserver(audioRoutesInfo);

        mController.selectRoute(MediaRoute2Info.TYPE_DOCK);

        MediaRoute2Info route2Info = mController.getDeviceRoute();
        assertThat(route2Info.getName()).isEqualTo(ROUTE_NAME_DOCK);
        assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_DOCK);
    }

    @Test
    public void getDeviceRoute_unselectRoute_returnsAudioServiceRoute() {
        when(mResources.getText(R.string.default_audio_route_name_headphones))
                .thenReturn(ROUTE_NAME_HEADPHONES);
        when(mResources.getText(R.string.default_audio_route_name_dock_speakers))
                .thenReturn(ROUTE_NAME_DOCK);

        mController.selectRoute(MediaRoute2Info.TYPE_DOCK);

        AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo();
        audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_HEADPHONES;
        callAudioRoutesObserver(audioRoutesInfo);

        mController.selectRoute(null);

        MediaRoute2Info route2Info = mController.getDeviceRoute();
        assertThat(route2Info.getName()).isEqualTo(ROUTE_NAME_HEADPHONES);
        assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_WIRED_HEADPHONES);
    }

    @Test
    public void getDeviceRoute_selectRouteFails_returnsAudioServiceRoute() {
        when(mResources.getText(R.string.default_audio_route_name_headphones))
                .thenReturn(ROUTE_NAME_HEADPHONES);

        AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo();
        audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_HEADPHONES;
        callAudioRoutesObserver(audioRoutesInfo);

        mController.selectRoute(MediaRoute2Info.TYPE_BLUETOOTH_A2DP);

        MediaRoute2Info route2Info = mController.getDeviceRoute();
        assertThat(route2Info.getName()).isEqualTo(ROUTE_NAME_HEADPHONES);
        assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_WIRED_HEADPHONES);
    }

    @Test
    public void selectRoute_selectWiredRoute_returnsTrue() {
        assertThat(mController.selectRoute(MediaRoute2Info.TYPE_HDMI)).isTrue();
    }

    @Test
    public void selectRoute_selectBluetoothRoute_returnsFalse() {
        assertThat(mController.selectRoute(MediaRoute2Info.TYPE_BLUETOOTH_A2DP)).isFalse();
    }

    @Test
    public void selectRoute_unselectRoute_returnsTrue() {
        assertThat(mController.selectRoute(null)).isTrue();
    }

    @Test
    public void updateVolume_noSelectedRoute_deviceRouteVolumeChanged() {
        when(mResources.getText(R.string.default_audio_route_name_headphones))
                .thenReturn(ROUTE_NAME_HEADPHONES);

        AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo();
        audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_HEADPHONES;
        callAudioRoutesObserver(audioRoutesInfo);

        mController.updateVolume(VOLUME_SAMPLE_1);

        MediaRoute2Info route2Info = mController.getDeviceRoute();
        assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_WIRED_HEADPHONES);
        assertThat(route2Info.getVolume()).isEqualTo(VOLUME_SAMPLE_1);
    }

    @Test
    public void updateVolume_connectSelectedRouteLater_selectedRouteVolumeChanged() {
        when(mResources.getText(R.string.default_audio_route_name_headphones))
                .thenReturn(ROUTE_NAME_HEADPHONES);
        when(mResources.getText(R.string.default_audio_route_name_dock_speakers))
                .thenReturn(ROUTE_NAME_DOCK);

        AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo();
        audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_HEADPHONES;
        callAudioRoutesObserver(audioRoutesInfo);

        mController.updateVolume(VOLUME_SAMPLE_1);

        mController.selectRoute(MediaRoute2Info.TYPE_DOCK);

        MediaRoute2Info route2Info = mController.getDeviceRoute();
        assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_DOCK);
        assertThat(route2Info.getVolume()).isEqualTo(VOLUME_SAMPLE_1);
    }

    /**
     * Simulates {@link IAudioRoutesObserver.Stub#dispatchAudioRoutesChanged(AudioRoutesInfo)}
     * from {@link AudioService}. This happens when there is a wired route change,
     * like a wired headset being connected.
     *
     * @param audioRoutesInfo updated state of connected wired device
     */
    private void callAudioRoutesObserver(AudioRoutesInfo audioRoutesInfo) {
        try {
            // this is a captured observer implementation
            // from WiredRoutesController's AudioService#startWatchingRoutes call
            mAudioRoutesObserver.dispatchAudioRoutesChanged(audioRoutesInfo);
        } catch (RemoteException exception) {
            // Should not happen since the object is mocked.
            assertWithMessage("An unexpected RemoteException happened.").fail();
        }
    }
}