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

Commit 0498bfd9 authored by Alex Dadukin's avatar Alex Dadukin Committed by Automerger Merge Worker
Browse files

Merge "Implement selectRoute in AudioPoliciesDeviceRouteController" into...

Merge "Implement selectRoute in AudioPoliciesDeviceRouteController" into udc-dev am: c0fca287 am: bf7935bf

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/21673440



Change-Id: I0269ccf82517da812e21fe7ad97fb2736acf7c91
Signed-off-by: default avatarAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
parents 0a83ef2f bf7935bf
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();
        }
    }
}