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

Commit 62086fda authored by Yan Han's avatar Yan Han
Browse files

Add action to query <Set Audio Volume Level> support

The action sends <Set Audio Volume Level> with an operand that indicates
no change in volume level. The target device supports the message if it
does not respond with <Feature Abort>.

Bug: 199144863
Test: atest SetAudioVolumeLevelDiscoveryActionTest
Change-Id: I958029a4805d68ef8b18458715408fc72e025e82
parent dcccddfb
Loading
Loading
Loading
Loading
+12 −0
Original line number Diff line number Diff line
@@ -199,6 +199,7 @@ final class Constants {
    static final int MESSAGE_SET_SYSTEM_AUDIO_MODE = 0x72;
    static final int MESSAGE_REPORT_AUDIO_STATUS = 0x7A;
    static final int MESSAGE_GIVE_SYSTEM_AUDIO_MODE_STATUS = 0x7D;
    static final int MESSAGE_SET_AUDIO_VOLUME_LEVEL = 0x73;
    static final int MESSAGE_SYSTEM_AUDIO_MODE_STATUS = 0x7E;
    static final int MESSAGE_ROUTING_CHANGE = 0x80;
    static final int MESSAGE_ROUTING_INFORMATION = 0x81;
@@ -389,6 +390,17 @@ final class Constants {

    static final int UNKNOWN_VOLUME = -1;

    // This constant is used in two operands in the CEC spec.
    //
    // CEC 1.4: [Audio Volume Status] (part of [Audio Status]) - operand for <Report Audio Status>
    // Indicates that the current audio volume status is unknown.
    //
    // CEC 2.1a: [Audio Volume Level] - operand for <Set Audio Volume Level>
    // Part of the Absolute Volume Control feature. Indicates that no change shall be made to the
    // volume level of the recipient. This allows <Set Audio Volume Level> to be sent to determine
    // whether the recipient supports Absolute Volume Control.
    static final int AUDIO_VOLUME_STATUS_UNKNOWN = 0x7F;

    // States of property PROPERTY_SYSTEM_AUDIO_CONTROL_ON_POWER_ON
    // to decide if turn on the system audio control when power on the device
    @IntDef({
+2 −0
Original line number Diff line number Diff line
@@ -75,6 +75,8 @@ public class HdmiCecMessage {
     */
    static HdmiCecMessage build(int source, int destination, int opcode, byte[] params) {
        switch (opcode & 0xFF) {
            case Constants.MESSAGE_SET_AUDIO_VOLUME_LEVEL:
                return SetAudioVolumeLevelMessage.build(source, destination, params);
            case Constants.MESSAGE_REPORT_FEATURES:
                return ReportFeaturesMessage.build(source, destination, params);
            default:
+126 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.hdmi;

import static android.hardware.hdmi.DeviceFeatures.FEATURE_NOT_SUPPORTED;
import static android.hardware.hdmi.DeviceFeatures.FEATURE_SUPPORTED;

import static com.android.server.hdmi.Constants.MESSAGE_SET_AUDIO_VOLUME_LEVEL;

import android.hardware.hdmi.DeviceFeatures;
import android.hardware.hdmi.HdmiControlManager;
import android.hardware.hdmi.HdmiDeviceInfo;
import android.hardware.hdmi.IHdmiControlCallback;
import android.hardware.tv.cec.V1_0.SendMessageResult;

/**
 * Determines whether a target device supports the <Set Audio Volume Level> message.
 *
 * Sends the device <Set Audio Volume Level>[0x7F]. The value 0x7F is defined by the spec such that
 * setting the volume to this level results in no change to the current volume level.
 *
 * The target device supports <Set Audio Volume Level> only if it does not respond with
 * <Feature Abort> within {@link HdmiConfig.TIMEOUT_MS} milliseconds.
 */
public class SetAudioVolumeLevelDiscoveryAction extends HdmiCecFeatureAction {
    private static final String TAG = "SetAudioVolumeLevelDiscoveryAction";

    private static final int STATE_WAITING_FOR_FEATURE_ABORT = 1;

    private final int mTargetAddress;

    public SetAudioVolumeLevelDiscoveryAction(HdmiCecLocalDevice source,
            int targetAddress, IHdmiControlCallback callback) {
        super(source, callback);

        mTargetAddress = targetAddress;
    }

    boolean start() {
        sendCommand(SetAudioVolumeLevelMessage.build(
                getSourceAddress(), mTargetAddress, Constants.AUDIO_VOLUME_STATUS_UNKNOWN),
                result -> {
                    if (result == SendMessageResult.SUCCESS) {
                        // Message sent successfully; wait for <Feature Abort> in response
                        mState = STATE_WAITING_FOR_FEATURE_ABORT;
                        addTimer(mState, HdmiConfig.TIMEOUT_MS);
                    } else {
                        finishWithCallback(HdmiControlManager.RESULT_COMMUNICATION_FAILED);
                    }
                });
        return true;
    }

    boolean processCommand(HdmiCecMessage cmd) {
        if (mState != STATE_WAITING_FOR_FEATURE_ABORT) {
            return false;
        }
        switch (cmd.getOpcode()) {
            case Constants.MESSAGE_FEATURE_ABORT:
                return handleFeatureAbort(cmd);
            default:
                return false;
        }
    }

    private boolean handleFeatureAbort(HdmiCecMessage cmd) {
        int originalOpcode = cmd.getParams()[0] & 0xFF;
        if (originalOpcode == MESSAGE_SET_AUDIO_VOLUME_LEVEL && cmd.getSource() == mTargetAddress) {
            if (updateAvcSupport(FEATURE_NOT_SUPPORTED)) {
                finishWithCallback(HdmiControlManager.RESULT_SUCCESS);
            } else {
                finishWithCallback(HdmiControlManager.RESULT_EXCEPTION);
            }
            return true;
        }
        return false;
    }

    void handleTimerEvent(int state) {
        if (updateAvcSupport(FEATURE_SUPPORTED)) {
            finishWithCallback(HdmiControlManager.RESULT_SUCCESS);
        } else {
            finishWithCallback(HdmiControlManager.RESULT_EXCEPTION);
        }
    }

    /**
     * Updates the System Audio device's support for <Set Audio Volume Level> in the
     * {@link HdmiCecNetwork}. Can fail if the System Audio device is not in our
     * {@link HdmiCecNetwork}.
     *
     * @return Whether support was successfully updated in the network.
     */
    private boolean updateAvcSupport(
            @DeviceFeatures.FeatureSupportStatus int setAudioVolumeLevelSupport) {
        HdmiCecNetwork network = source().mService.getHdmiCecNetwork();
        HdmiDeviceInfo currentDeviceInfo = network.getCecDeviceInfo(mTargetAddress);

        if (currentDeviceInfo == null) {
            return false;
        } else {
            network.updateCecDevice(
                    currentDeviceInfo.toBuilder()
                            .setDeviceFeatures(currentDeviceInfo.getDeviceFeatures().toBuilder()
                                    .setSetAudioVolumeLevelSupport(setAudioVolumeLevelSupport)
                                    .build())
                            .build()
            );
            return true;
        }
    }
}
+102 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.hdmi;

import static com.android.server.hdmi.HdmiCecMessageValidator.ERROR_PARAMETER_SHORT;
import static com.android.server.hdmi.HdmiCecMessageValidator.OK;
import static com.android.server.hdmi.HdmiCecMessageValidator.ValidationResult;

/**
 * Represents a validated <Set Audio Volume Level> message with parsed parameters.
 */
public class SetAudioVolumeLevelMessage extends HdmiCecMessage {
    private final int mAudioVolumeLevel;

    private SetAudioVolumeLevelMessage(int source, int destination, byte[] params,
            int audioVolumeLevel) {
        super(source, destination, Constants.MESSAGE_SET_AUDIO_VOLUME_LEVEL, params, OK);
        mAudioVolumeLevel = audioVolumeLevel;
    }

    /**
     * Static factory method. Intended for constructing outgoing or test messages, as it uses
     * structured types instead of raw bytes to construct the parameters.
     *
     * @param source Initiator address. Cannot be {@link Constants#ADDR_UNREGISTERED}
     * @param destination Destination address. Cannot be {@link Constants#ADDR_BROADCAST}
     * @param audioVolumeLevel [Audio Volume Level]. Either 0x7F (representing no volume change)
     *                         or between 0 and 100 inclusive (representing volume percentage).
     */
    public static HdmiCecMessage build(int source, int destination, int audioVolumeLevel) {
        byte[] params = { (byte) (audioVolumeLevel & 0xFF) };

        @ValidationResult
        int addressValidationResult = validateAddress(source, destination);
        if (addressValidationResult == OK) {
            return new SetAudioVolumeLevelMessage(source, destination, params, audioVolumeLevel);
        } else {
            return new HdmiCecMessage(source, destination, Constants.MESSAGE_SET_AUDIO_VOLUME_LEVEL,
                    params, addressValidationResult);
        }
    }

    /**
     * Must only be called from {@link HdmiCecMessage#build}.
     *
     * Parses and validates CEC message data as a <SetAudioVolumeLevel> message. Intended for
     * constructing a representation of an incoming message, as it takes raw bytes for
     * parameters.
     *
     * If successful, returns an instance of {@link SetAudioVolumeLevelMessage}.
     * If unsuccessful, returns an {@link HdmiCecMessage} with the reason for validation failure
     * accessible through {@link HdmiCecMessage#getValidationResult}.
     */
    public static HdmiCecMessage build(int source, int destination, byte[] params) {
        if (params.length == 0) {
            return new HdmiCecMessage(source, destination, Constants.MESSAGE_SET_AUDIO_VOLUME_LEVEL,
                    params, ERROR_PARAMETER_SHORT);
        }

        int audioVolumeLevel = params[0];

        @ValidationResult
        int addressValidationResult = validateAddress(source, destination);
        if (addressValidationResult == OK) {
            return new SetAudioVolumeLevelMessage(source, destination, params, audioVolumeLevel);
        } else {
            return new HdmiCecMessage(source, destination, Constants.MESSAGE_SET_AUDIO_VOLUME_LEVEL,
                    params, addressValidationResult);
        }
    }

    /**
     * Validates the source and destination addresses for a <Set Audio Volume Level> message.
     */
    public static int validateAddress(int source, int destination) {
        return HdmiCecMessageValidator.validateAddress(source, destination,
                HdmiCecMessageValidator.DEST_DIRECT);
    }

    /**
     * Returns the contents of the [Audio Volume Level] operand:
     * either 0x7F, indicating no change to the current volume level,
     * or a percentage between 0 and 100 (inclusive).
     */
    public int getAudioVolumeLevel() {
        return mAudioVolumeLevel;
    }
}
+218 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.hdmi;

import static android.hardware.hdmi.DeviceFeatures.FEATURE_NOT_SUPPORTED;
import static android.hardware.hdmi.DeviceFeatures.FEATURE_SUPPORTED;
import static android.hardware.hdmi.DeviceFeatures.FEATURE_SUPPORT_UNKNOWN;

import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;

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

import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.spy;

import android.content.Context;
import android.content.ContextWrapper;
import android.hardware.hdmi.DeviceFeatures;
import android.hardware.hdmi.HdmiControlManager;
import android.hardware.hdmi.HdmiDeviceInfo;
import android.hardware.hdmi.IHdmiControlCallback;
import android.hardware.tv.cec.V1_0.SendMessageResult;
import android.os.Looper;
import android.os.RemoteException;
import android.os.test.TestLooper;
import android.platform.test.annotations.Presubmit;

import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;

import com.android.server.SystemService;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.util.ArrayList;
import java.util.Collections;

@SmallTest
@Presubmit
@RunWith(JUnit4.class)
public class SetAudioVolumeLevelDiscoveryActionTest {
    private HdmiControlService mHdmiControlServiceSpy;
    private HdmiCecController mHdmiCecController;
    private HdmiCecLocalDevicePlayback mPlaybackDevice;
    private FakeNativeWrapper mNativeWrapper;
    private FakePowerManagerWrapper mPowerManager;
    private Looper mLooper;
    private Context mContextSpy;
    private TestLooper mTestLooper = new TestLooper();
    private int mPhysicalAddress = 0x1100;
    private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
    private int mPlaybackLogicalAddress;

    private TestCallback mTestCallback;
    private SetAudioVolumeLevelDiscoveryAction mAction;

    /**
     * Setup: Local Playback device attempts to determine whether a connected TV supports
     * <Set Audio Volume Level>.
     */
    @Before
    public void setUp() throws RemoteException {
        mContextSpy = spy(new ContextWrapper(
                InstrumentationRegistry.getInstrumentation().getTargetContext()));

        mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy, Collections.emptyList()));
        doNothing().when(mHdmiControlServiceSpy)
                .writeStringSystemProperty(anyString(), anyString());

        mLooper = mTestLooper.getLooper();
        mHdmiControlServiceSpy.setIoLooper(mLooper);
        mHdmiControlServiceSpy.setHdmiCecConfig(new FakeHdmiCecConfig(mContextSpy));

        mNativeWrapper = new FakeNativeWrapper();
        mNativeWrapper.setPhysicalAddress(mPhysicalAddress);

        mHdmiCecController = HdmiCecController.createWithNativeWrapper(
                mHdmiControlServiceSpy, mNativeWrapper, mHdmiControlServiceSpy.getAtomWriter());
        mHdmiControlServiceSpy.setCecController(mHdmiCecController);
        mHdmiControlServiceSpy.setHdmiMhlController(
                HdmiMhlControllerStub.create(mHdmiControlServiceSpy));
        mHdmiControlServiceSpy.initService();
        mPowerManager = new FakePowerManagerWrapper(mContextSpy);
        mHdmiControlServiceSpy.setPowerManager(mPowerManager);

        mPlaybackDevice = new HdmiCecLocalDevicePlayback(mHdmiControlServiceSpy);
        mPlaybackDevice.init();
        mLocalDevices.add(mPlaybackDevice);

        mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
        mHdmiControlServiceSpy.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
        mTestLooper.dispatchAll();

        synchronized (mPlaybackDevice.mLock) {
            mPlaybackLogicalAddress = mPlaybackDevice.getDeviceInfo().getLogicalAddress();
        }

        // Setup specific to these tests
        mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
                Constants.ADDR_TV, 0x0000, HdmiDeviceInfo.DEVICE_TV));
        mTestLooper.dispatchAll();

        mTestCallback = new TestCallback();
        mAction = new SetAudioVolumeLevelDiscoveryAction(mPlaybackDevice,
                Constants.ADDR_TV, mTestCallback);
    }

    @Test
    public void sendsSetAudioVolumeLevel() {
        mPlaybackDevice.addAndStartAction(mAction);
        mTestLooper.dispatchAll();

        HdmiCecMessage setAudioVolumeLevel = SetAudioVolumeLevelMessage.build(
                mPlaybackLogicalAddress, Constants.ADDR_TV,
                Constants.AUDIO_VOLUME_STATUS_UNKNOWN);
        assertThat(mNativeWrapper.getResultMessages()).contains(setAudioVolumeLevel);
    }

    @Test
    public void noMatchingFeatureAbortReceived_actionSucceedsAndSetsFeatureSupported() {
        mPlaybackDevice.addAndStartAction(mAction);
        mTestLooper.dispatchAll();

        // Wrong opcode
        mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildFeatureAbortCommand(
                Constants.ADDR_TV,
                mPlaybackLogicalAddress,
                Constants.MESSAGE_GIVE_DECK_STATUS,
                Constants.ABORT_UNRECOGNIZED_OPCODE));
        // Wrong source
        mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildFeatureAbortCommand(
                Constants.ADDR_AUDIO_SYSTEM,
                mPlaybackLogicalAddress,
                Constants.MESSAGE_SET_AUDIO_VOLUME_LEVEL,
                Constants.ABORT_UNRECOGNIZED_OPCODE));
        mTestLooper.dispatchAll();

        mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
        mTestLooper.dispatchAll();

        @DeviceFeatures.FeatureSupportStatus int avcSupport =
                mHdmiControlServiceSpy.getHdmiCecNetwork().getCecDeviceInfo(Constants.ADDR_TV)
                        .getDeviceFeatures().getSetAudioVolumeLevelSupport();

        assertThat(avcSupport).isEqualTo(FEATURE_SUPPORTED);
        assertThat(mTestCallback.getResult()).isEqualTo(HdmiControlManager.RESULT_SUCCESS);
    }

    @Test
    public void matchingFeatureAbortReceived_actionSucceedsAndSetsFeatureNotSupported() {
        mPlaybackDevice.addAndStartAction(mAction);
        mTestLooper.dispatchAll();

        mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildFeatureAbortCommand(
                Constants.ADDR_TV,
                mPlaybackLogicalAddress,
                Constants.MESSAGE_SET_AUDIO_VOLUME_LEVEL,
                Constants.ABORT_UNRECOGNIZED_OPCODE));
        mTestLooper.dispatchAll();

        @DeviceFeatures.FeatureSupportStatus int avcSupport =
                mHdmiControlServiceSpy.getHdmiCecNetwork().getCecDeviceInfo(Constants.ADDR_TV)
                        .getDeviceFeatures().getSetAudioVolumeLevelSupport();

        assertThat(avcSupport).isEqualTo(FEATURE_NOT_SUPPORTED);
        assertThat(mTestCallback.getResult()).isEqualTo(HdmiControlManager.RESULT_SUCCESS);
    }

    @Test
    public void messageFailedToSend_actionFailsAndDoesNotUpdateFeatureSupport() {
        mNativeWrapper.setMessageSendResult(Constants.MESSAGE_SET_AUDIO_VOLUME_LEVEL,
                SendMessageResult.FAIL);
        mTestLooper.dispatchAll();

        mPlaybackDevice.addAndStartAction(mAction);
        mTestLooper.dispatchAll();

        @DeviceFeatures.FeatureSupportStatus int avcSupport =
                mHdmiControlServiceSpy.getHdmiCecNetwork().getCecDeviceInfo(Constants.ADDR_TV)
                        .getDeviceFeatures().getSetAudioVolumeLevelSupport();

        assertThat(avcSupport).isEqualTo(FEATURE_SUPPORT_UNKNOWN);
        assertThat(mTestCallback.getResult()).isEqualTo(
                HdmiControlManager.RESULT_COMMUNICATION_FAILED);
    }

    private static class TestCallback extends IHdmiControlCallback.Stub {
        private final ArrayList<Integer> mCallbackResult = new ArrayList<Integer>();

        @Override
        public void onComplete(int result) {
            mCallbackResult.add(result);
        }

        private int getResult() {
            assertThat(mCallbackResult.size()).isEqualTo(1);
            return mCallbackResult.get(0);
        }
    }
}
Loading