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

Commit 8f18957e authored by Yan Han's avatar Yan Han
Browse files

Implement support for Absolute Volume Control

- Adds logic for enabling or disabling AVC. This is checked whenever one
  of the necessary conditions for AVC changes.
  (HdmiControlService#checkAbsoluteVolumeConditions)
- Adds code for listening for audio status changes from AudioService
  when using AVC (HdmiControlService#AbsoluteVolumeChangedListener).
- Adds code for notifying AudioService of volume changes on the System
  Audio device (HdmiControlService#notifyAvcVolumeChange,
  HdmiControlService#notifyAvcMuteChange)
- Adds action for querying and monitoring the audio status of the System
  Audio device (AbsoluteVolumeAudioStatusAction).
- Removes SystemAudioStatusAction: it is no longer needed because we
  use full volume when in System Audio Mode, and don't track audio status.

Test: atest PlaybackDeviceToAudioSystemAvcTest
PlaybackDeviceToTvAvcTest TvToAudioSystemAvcTest
Bug: 205817429

Change-Id: I4d7772de851b12085bb2e9844a41d52245ee1d4a
parent 24da93d9
Loading
Loading
Loading
Loading
+103 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2014 The Android Open Source Project
 * Copyright (C) 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.
@@ -16,97 +16,88 @@

package com.android.server.hdmi;

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

import com.android.server.hdmi.HdmiControlService.SendMessageCallback;

import java.util.List;

/**
 * Action to update audio status (volume or mute) of audio amplifier
 * Action to query and track the audio status of the System Audio device when enabling or using
 * Absolute Volume Control. Must be removed when AVC is disabled. Performs two main functions:
 * 1. When enabling AVC: queries the starting audio status of the System Audio device and
 *    enables the feature upon receiving a response.
 * 2. While AVC is enabled: monitors <Report Audio Status> messages from the System Audio device and
 *    notifies AudioService if the audio status changes.
 */
final class SystemAudioStatusAction extends HdmiCecFeatureAction {
    private static final String TAG = "SystemAudioStatusAction";
final class AbsoluteVolumeAudioStatusAction extends HdmiCecFeatureAction {
    private static final String TAG = "AbsoluteVolumeAudioStatusAction";

    // State that waits for <ReportAudioStatus>.
    private static final int STATE_WAIT_FOR_REPORT_AUDIO_STATUS = 1;
    private int mInitialAudioStatusRetriesLeft = 2;

    private final int mAvrAddress;
    private static final int STATE_WAIT_FOR_INITIAL_AUDIO_STATUS = 1;
    private static final int STATE_MONITOR_AUDIO_STATUS = 2;

    SystemAudioStatusAction(
            HdmiCecLocalDevice source, int avrAddress, List<IHdmiControlCallback> callbacks) {
        super(source, callbacks);
        mAvrAddress = avrAddress;
    }
    private final int mTargetAddress;

    SystemAudioStatusAction(HdmiCecLocalDevice source, int avrAddress,
            IHdmiControlCallback callback) {
        super(source, callback);
        mAvrAddress = avrAddress;
    private AudioStatus mLastAudioStatus;

    AbsoluteVolumeAudioStatusAction(HdmiCecLocalDevice source, int targetAddress) {
        super(source);
        mTargetAddress = targetAddress;
    }

    @Override
    boolean start() {
        mState = STATE_WAIT_FOR_REPORT_AUDIO_STATUS;
        addTimer(mState, HdmiConfig.TIMEOUT_MS);
        mState = STATE_WAIT_FOR_INITIAL_AUDIO_STATUS;
        sendGiveAudioStatus();
        return true;
    }

    private void sendGiveAudioStatus() {
        sendCommand(HdmiCecMessageBuilder.buildGiveAudioStatus(getSourceAddress(), mAvrAddress),
                new SendMessageCallback() {
            @Override
            public void onSendCompleted(int error) {
                if (error != SendMessageResult.SUCCESS) {
                    handleSendGiveAudioStatusFailure();
                }
    void updateVolume(int volumeIndex) {
        mLastAudioStatus = new AudioStatus(volumeIndex, mLastAudioStatus.getMute());
    }
        });
    }

    private void handleSendGiveAudioStatusFailure() {

        // Still return SUCCESS to callback.
        finishWithCallback(HdmiControlManager.RESULT_SUCCESS);
    private void sendGiveAudioStatus() {
        addTimer(mState, HdmiConfig.TIMEOUT_MS);
        sendCommand(HdmiCecMessageBuilder.buildGiveAudioStatus(getSourceAddress(), mTargetAddress));
    }

    @Override
    boolean processCommand(HdmiCecMessage cmd) {
        if (mState != STATE_WAIT_FOR_REPORT_AUDIO_STATUS || mAvrAddress != cmd.getSource()) {
            return false;
        }

        switch (cmd.getOpcode()) {
            case Constants.MESSAGE_REPORT_AUDIO_STATUS:
                handleReportAudioStatus(cmd);
                return true;
                return handleReportAudioStatus(cmd);
        }

        return false;
    }

    private void handleReportAudioStatus(HdmiCecMessage cmd) {
        byte[] params = cmd.getParams();
    private boolean handleReportAudioStatus(HdmiCecMessage cmd) {
        if (mTargetAddress != cmd.getSource() || cmd.getParams().length == 0) {
            return false;
        }

        boolean mute = HdmiUtils.isAudioStatusMute(cmd);
        int volume = HdmiUtils.getAudioStatusVolume(cmd);
        tv().setAudioStatus(mute, volume);

        if (!(tv().isSystemAudioActivated() ^ mute)) {
            // Toggle AVR's mute status to match with the system audio status.
            sendUserControlPressedAndReleased(mAvrAddress, HdmiCecKeycode.CEC_KEYCODE_MUTE);
        AudioStatus audioStatus = new AudioStatus(volume, mute);
        if (mState == STATE_WAIT_FOR_INITIAL_AUDIO_STATUS) {
            localDevice().getService().enableAbsoluteVolumeControl(audioStatus);
            mState = STATE_MONITOR_AUDIO_STATUS;
        } else if (mState == STATE_MONITOR_AUDIO_STATUS) {
            if (audioStatus.getVolume() != mLastAudioStatus.getVolume()) {
                localDevice().getService().notifyAvcVolumeChange(audioStatus.getVolume());
            }
        finishWithCallback(HdmiControlManager.RESULT_SUCCESS);
            if (audioStatus.getMute() != mLastAudioStatus.getMute()) {
                localDevice().getService().notifyAvcMuteChange(audioStatus.getMute());
            }
        }
        mLastAudioStatus = audioStatus;

        return true;
    }

    @Override
    void handleTimerEvent(int state) {
        if (mState != state) {
            return;
        } else if (mInitialAudioStatusRetriesLeft > 0) {
            mInitialAudioStatusRetriesLeft--;
            sendGiveAudioStatus();
        }

        handleSendGiveAudioStatusFailure();
    }
}
+67 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 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.hdmi;

import android.annotation.Nullable;

import java.util.Objects;

/**
 * Immutable representation of the information in the [Audio Status] operand:
 * volume status (0 <= N <= 100) and mute status (muted or unmuted).
 */
public class AudioStatus {
    public static final int MAX_VOLUME = 100;
    public static final int MIN_VOLUME = 0;

    int mVolume;
    boolean mMute;

    public AudioStatus(int volume, boolean mute) {
        mVolume = volume;
        mMute = mute;
    }

    public int getVolume() {
        return mVolume;
    }

    public boolean getMute() {
        return mMute;
    }

    @Override
    public boolean equals(@Nullable Object obj) {
        if (!(obj instanceof AudioStatus)) {
            return false;
        }

        AudioStatus other = (AudioStatus) obj;
        return mVolume == other.mVolume
                && mMute == other.mMute;
    }

    @Override
    public int hashCode() {
        return Objects.hash(mVolume, mMute);
    }

    @Override
    public String toString() {
        return "AudioStatus mVolume:" + mVolume + " mMute:" + mMute;
    }
}
+2 −1
Original line number Diff line number Diff line
@@ -118,6 +118,7 @@ final class Constants {
            MESSAGE_SYSTEM_AUDIO_MODE_REQUEST,
            MESSAGE_GIVE_AUDIO_STATUS,
            MESSAGE_SET_SYSTEM_AUDIO_MODE,
            MESSAGE_SET_AUDIO_VOLUME_LEVEL,
            MESSAGE_REPORT_AUDIO_STATUS,
            MESSAGE_GIVE_SYSTEM_AUDIO_MODE_STATUS,
            MESSAGE_SYSTEM_AUDIO_MODE_STATUS,
@@ -197,9 +198,9 @@ final class Constants {
    static final int MESSAGE_SYSTEM_AUDIO_MODE_REQUEST = 0x70;
    static final int MESSAGE_GIVE_AUDIO_STATUS = 0x71;
    static final int MESSAGE_SET_SYSTEM_AUDIO_MODE = 0x72;
    static final int MESSAGE_SET_AUDIO_VOLUME_LEVEL = 0x73;
    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;
+54 −0
Original line number Diff line number Diff line
@@ -1002,6 +1002,57 @@ abstract class HdmiCecLocalDevice {
        action.start();
    }

    void addAvcAudioStatusAction(int targetAddress) {
        if (!hasAction(AbsoluteVolumeAudioStatusAction.class)) {
            addAndStartAction(new AbsoluteVolumeAudioStatusAction(this, targetAddress));
        }
    }

    void removeAvcAudioStatusAction() {
        removeAction(AbsoluteVolumeAudioStatusAction.class);
    }

    void updateAvcVolume(int volumeIndex) {
        for (AbsoluteVolumeAudioStatusAction action :
                getActions(AbsoluteVolumeAudioStatusAction.class)) {
            action.updateVolume(volumeIndex);
        }
    }

    /**
     * Determines whether {@code targetAddress} supports <Set Audio Volume Level>. Does two things
     * in parallel: send <Give Features> (to get <Report Features> in response),
     * and send <Set Audio Volume Level> (to see if it gets a <Feature Abort> in response).
     */
    @ServiceThreadOnly
    void queryAvcSupport(int targetAddress) {
        assertRunOnServiceThread();

        // Send <Give Features> if using CEC 2.0 or above.
        if (mService.getCecVersion() >= HdmiControlManager.HDMI_CEC_VERSION_2_0) {
            synchronized (mLock) {
                mService.sendCecCommand(HdmiCecMessageBuilder.buildGiveFeatures(
                        getDeviceInfo().getLogicalAddress(), targetAddress));
            }
        }

        // If we don't already have a {@link SetAudioVolumeLevelDiscoveryAction} for the target
        // device, start one.
        List<SetAudioVolumeLevelDiscoveryAction> savlDiscoveryActions =
                getActions(SetAudioVolumeLevelDiscoveryAction.class);
        if (savlDiscoveryActions.stream().noneMatch(a -> a.getTargetAddress() == targetAddress)) {
            addAndStartAction(new SetAudioVolumeLevelDiscoveryAction(this, targetAddress,
                    new IHdmiControlCallback.Stub() {
                            @Override
                            public void onComplete(int result) {
                                if (result == HdmiControlManager.RESULT_SUCCESS) {
                                    getService().checkAndUpdateAbsoluteVolumeControlState();
                                }
                            }
                        }));
        }
    }

    @ServiceThreadOnly
    void startQueuedActions() {
        assertRunOnServiceThread();
@@ -1205,6 +1256,9 @@ abstract class HdmiCecLocalDevice {
     */
    protected void disableDevice(
            boolean initiatedByCec, final PendingActionClearedCallback originalCallback) {
        removeAction(AbsoluteVolumeAudioStatusAction.class);
        removeAction(SetAudioVolumeLevelDiscoveryAction.class);

        mPendingActionClearedCallback =
                new PendingActionClearedCallback() {
                    @Override
+1 −0
Original line number Diff line number Diff line
@@ -307,6 +307,7 @@ abstract class HdmiCecLocalDeviceSource extends HdmiCecLocalDevice {
    protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) {
        removeAction(OneTouchPlayAction.class);
        removeAction(DevicePowerStatusAction.class);
        removeAction(AbsoluteVolumeAudioStatusAction.class);

        super.disableDevice(initiatedByCec, callback);
    }
Loading