blob: 14af15f0a3fddb95e976e0db5fd4348b5bfb31fb [file] [log] [blame]
/*
* Copyright (C) 2018 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.Constants.ALWAYS_SYSTEM_AUDIO_CONTROL_ON_POWER_ON;
import static com.android.server.hdmi.Constants.PROPERTY_SYSTEM_AUDIO_CONTROL_ON_POWER_ON;
import static com.android.server.hdmi.Constants.USE_LAST_STATE_SYSTEM_AUDIO_CONTROL_ON_POWER_ON;
import android.hardware.hdmi.HdmiDeviceInfo;
import android.media.AudioManager;
import android.media.AudioSystem;
import android.os.SystemProperties;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly;
/**
* Represent a logical device of type {@link HdmiDeviceInfo#DEVICE_AUDIO_SYSTEM} residing in Android
* system.
*/
public class HdmiCecLocalDeviceAudioSystem extends HdmiCecLocalDevice {
private static final String TAG = "HdmiCecLocalDeviceAudioSystem";
// Whether System audio mode is activated or not.
// This becomes true only when all system audio sequences are finished.
@GuardedBy("mLock")
private boolean mSystemAudioActivated;
// Whether the System Audio Control feature is enabled or not. True by default.
@GuardedBy("mLock")
private boolean mSystemAudioControlFeatureEnabled;
private boolean mTvSystemAudioModeSupport;
// Whether ARC is available or not. "true" means that ARC is established between TV and
// AVR as audio receiver.
@ServiceThreadOnly
private boolean mArcEstablished = false;
protected HdmiCecLocalDeviceAudioSystem(HdmiControlService service) {
super(service, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
mSystemAudioControlFeatureEnabled = true;
// TODO(amyjojo) make System Audio Control controllable by users
/*mSystemAudioControlFeatureEnabled =
mService.readBooleanSetting(Global.HDMI_SYSTEM_AUDIO_CONTROL_ENABLED, true);*/
}
@Override
@ServiceThreadOnly
protected void onStandby(boolean initiatedByCec, int standbyAction) {
assertRunOnServiceThread();
mTvSystemAudioModeSupport = false;
// Record the last state of System Audio Control before going to standby
synchronized (mLock) {
SystemProperties.set(
Constants.PROPERTY_LAST_SYSTEM_AUDIO_CONTROL,
mSystemAudioActivated ? "true" : "false");
}
terminateSystemAudioMode();
}
@Override
@ServiceThreadOnly
protected void onAddressAllocated(int logicalAddress, int reason) {
assertRunOnServiceThread();
mService.sendCecCommand(
HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
mAddress, mService.getPhysicalAddress(), mDeviceType));
mService.sendCecCommand(
HdmiCecMessageBuilder.buildDeviceVendorIdCommand(mAddress, mService.getVendorId()));
int systemAudioControlOnPowerOnProp =
SystemProperties.getInt(
PROPERTY_SYSTEM_AUDIO_CONTROL_ON_POWER_ON,
ALWAYS_SYSTEM_AUDIO_CONTROL_ON_POWER_ON);
boolean lastSystemAudioControlStatus =
SystemProperties.getBoolean(Constants.PROPERTY_LAST_SYSTEM_AUDIO_CONTROL, true);
systemAudioControlOnPowerOn(systemAudioControlOnPowerOnProp, lastSystemAudioControlStatus);
startQueuedActions();
}
@VisibleForTesting
protected void systemAudioControlOnPowerOn(
int systemAudioOnPowerOnProp, boolean lastSystemAudioControlStatus) {
if ((systemAudioOnPowerOnProp == ALWAYS_SYSTEM_AUDIO_CONTROL_ON_POWER_ON)
|| ((systemAudioOnPowerOnProp == USE_LAST_STATE_SYSTEM_AUDIO_CONTROL_ON_POWER_ON)
&& lastSystemAudioControlStatus)) {
addAndStartAction(new SystemAudioInitiationActionFromAvr(this));
}
}
@ServiceThreadOnly
protected boolean handleActiveSource(HdmiCecMessage message) {
assertRunOnServiceThread();
int logicalAddress = message.getSource();
int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams());
ActiveSource activeSource = ActiveSource.of(logicalAddress, physicalAddress);
if (!mActiveSource.equals(activeSource)) {
setActiveSource(activeSource);
}
return true;
}
@Override
@ServiceThreadOnly
protected int getPreferredAddress() {
assertRunOnServiceThread();
return SystemProperties.getInt(
Constants.PROPERTY_PREFERRED_ADDRESS_AUDIO_SYSTEM, Constants.ADDR_UNREGISTERED);
}
@Override
@ServiceThreadOnly
protected void setPreferredAddress(int addr) {
assertRunOnServiceThread();
SystemProperties.set(
Constants.PROPERTY_PREFERRED_ADDRESS_AUDIO_SYSTEM, String.valueOf(addr));
}
@Override
@ServiceThreadOnly
protected boolean handleReportAudioStatus(HdmiCecMessage message) {
assertRunOnServiceThread();
// TODO(amyjojo): implement report audio status handler
HdmiLogger.debug(TAG + "Stub handleReportAudioStatus");
return true;
}
@Override
@ServiceThreadOnly
protected boolean handleInitiateArc(HdmiCecMessage message) {
assertRunOnServiceThread();
// TODO(amyjojo): implement initiate arc handler
HdmiLogger.debug(TAG + "Stub handleInitiateArc");
return true;
}
@Override
@ServiceThreadOnly
protected boolean handleReportArcInitiate(HdmiCecMessage message) {
assertRunOnServiceThread();
// TODO(amyjojo): implement report arc initiate handler
HdmiLogger.debug(TAG + "Stub handleReportArcInitiate");
return true;
}
@Override
@ServiceThreadOnly
protected boolean handleReportArcTermination(HdmiCecMessage message) {
assertRunOnServiceThread();
// TODO(amyjojo): implement report arc terminate handler
HdmiLogger.debug(TAG + "Stub handleReportArcTermination");
return true;
}
@Override
@ServiceThreadOnly
protected boolean handleGiveAudioStatus(HdmiCecMessage message) {
assertRunOnServiceThread();
reportAudioStatus(message);
return true;
}
@Override
@ServiceThreadOnly
protected boolean handleGiveSystemAudioModeStatus(HdmiCecMessage message) {
assertRunOnServiceThread();
mService.sendCecCommand(
HdmiCecMessageBuilder.buildReportSystemAudioMode(
mAddress, message.getSource(), mSystemAudioActivated));
return true;
}
@Override
@ServiceThreadOnly
protected boolean handleRequestArcInitiate(HdmiCecMessage message) {
assertRunOnServiceThread();
if (!SystemProperties.getBoolean(Constants.PROPERTY_ARC_SUPPORT, true)) {
mService.maySendFeatureAbortCommand(message, Constants.ABORT_UNRECOGNIZED_OPCODE);
} else if (!isDirectConnectToTv()) {
HdmiLogger.debug("AVR device is not directly connected with TV");
mService.maySendFeatureAbortCommand(message, Constants.ABORT_NOT_IN_CORRECT_MODE);
} else {
addAndStartAction(new ArcInitiationActionFromAvr(this));
}
return true;
}
@Override
@ServiceThreadOnly
protected boolean handleRequestArcTermination(HdmiCecMessage message) {
assertRunOnServiceThread();
if (!SystemProperties.getBoolean(Constants.PROPERTY_ARC_SUPPORT, true)) {
mService.maySendFeatureAbortCommand(message, Constants.ABORT_UNRECOGNIZED_OPCODE);
} else if (!isArcEnabled()) {
HdmiLogger.debug("ARC is not established between TV and AVR device");
mService.maySendFeatureAbortCommand(message, Constants.ABORT_NOT_IN_CORRECT_MODE);
} else {
addAndStartAction(new ArcTerminationActionFromAvr(this));
}
return true;
}
@ServiceThreadOnly
protected boolean handleRequestShortAudioDescriptor(HdmiCecMessage message) {
assertRunOnServiceThread();
// TODO(b/80297701): implement request short audio descriptor
HdmiLogger.debug(TAG + "Stub handleRequestShortAudioDescriptor");
mService.maySendFeatureAbortCommand(message, Constants.ABORT_REFUSED);
return true;
}
@Override
@ServiceThreadOnly
protected boolean handleSystemAudioModeRequest(HdmiCecMessage message) {
assertRunOnServiceThread();
boolean systemAudioStatusOn = message.getParams().length != 0;
if (!setSystemAudioMode(systemAudioStatusOn)) {
mService.maySendFeatureAbortCommand(message, Constants.ABORT_REFUSED);
return true;
}
mService.sendCecCommand(
HdmiCecMessageBuilder.buildSetSystemAudioMode(
mAddress, Constants.ADDR_BROADCAST, systemAudioStatusOn));
return true;
}
@Override
@ServiceThreadOnly
protected boolean handleSetSystemAudioMode(HdmiCecMessage message) {
assertRunOnServiceThread();
if (!setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message))) {
mService.maySendFeatureAbortCommand(message, Constants.ABORT_REFUSED);
}
return true;
}
@Override
@ServiceThreadOnly
protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) {
assertRunOnServiceThread();
if (!setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message))) {
mService.maySendFeatureAbortCommand(message, Constants.ABORT_REFUSED);
}
return true;
}
@ServiceThreadOnly
void setArcStatus(boolean enabled) {
// TODO(shubang): add tests
assertRunOnServiceThread();
HdmiLogger.debug("Set Arc Status[old:%b new:%b]", mArcEstablished, enabled);
// 1. Enable/disable ARC circuit.
enableAudioReturnChannel(enabled);
// 2. Notify arc status to audio service.
notifyArcStatusToAudioService(enabled);
// 3. Update arc status;
mArcEstablished = enabled;
}
/**
* Switch hardware ARC circuit in the system.
*/
@ServiceThreadOnly
private void enableAudioReturnChannel(boolean enabled) {
assertRunOnServiceThread();
mService.enableAudioReturnChannel(
SystemProperties.getInt(
Constants.PROPERTY_SYSTEM_AUDIO_DEVICE_ARC_PORT, 0),
enabled);
}
private void notifyArcStatusToAudioService(boolean enabled) {
// Note that we don't set any name to ARC.
mService.getAudioManager().setWiredDeviceConnectionState(
AudioSystem.DEVICE_IN_HDMI,
enabled ? 1 : 0, "", "");
}
private void reportAudioStatus(HdmiCecMessage message) {
assertRunOnServiceThread();
int volume = mService.getAudioManager().getStreamVolume(AudioManager.STREAM_MUSIC);
boolean mute = mService.getAudioManager().isStreamMute(AudioManager.STREAM_MUSIC);
int maxVolume = mService.getAudioManager().getStreamMaxVolume(AudioManager.STREAM_MUSIC);
int scaledVolume = VolumeControlAction.scaleToCecVolume(volume, maxVolume);
mService.sendCecCommand(
HdmiCecMessageBuilder.buildReportAudioStatus(
mAddress, message.getSource(), scaledVolume, mute));
}
protected boolean setSystemAudioMode(boolean newSystemAudioMode) {
if (!isSystemAudioControlFeatureEnabled()) {
HdmiLogger.debug(
"Cannot turn "
+ (newSystemAudioMode ? "on" : "off")
+ "system audio mode "
+ "because the System Audio Control feature is disabled.");
return false;
}
HdmiLogger.debug(
"System Audio Mode change[old:%b new:%b]",
mSystemAudioActivated, newSystemAudioMode);
// Wake up device if System Audio Control is turned on but device is still on standby
if (newSystemAudioMode && mService.isPowerStandbyOrTransient()) {
mService.wakeUp();
}
int targetPhysicalAddress = getActiveSource().physicalAddress;
if (newSystemAudioMode &&
!isPhysicalAddressMeOrBelow(targetPhysicalAddress)) {
switchToAudioInput();
}
// TODO(b/80297700): Mute device when TV terminates the system audio control
updateAudioManagerForSystemAudio(newSystemAudioMode);
synchronized (mLock) {
if (mSystemAudioActivated != newSystemAudioMode) {
mSystemAudioActivated = newSystemAudioMode;
mService.announceSystemAudioModeChange(newSystemAudioMode);
}
}
return true;
}
/**
* Method to check if the target device belongs to the subtree of the current device or not.
* <p>Return true if it does or if the two devices share the same physical address.
*
* <p>This check assumes both device physical address and target address are valid.
* @param targetPhysicalAddress is the physical address of the target device
*/
protected boolean isPhysicalAddressMeOrBelow(int targetPhysicalAddress) {
int myPhysicalAddress = mService.getPhysicalAddress();
int xor = targetPhysicalAddress ^ myPhysicalAddress;
// Return true if two addresses are the same
// or if they only differs for one byte, but not the first byte,
// and myPhysicalAddress is 0 after that byte
if (xor == 0
|| ((xor & 0x0f00) == xor && (myPhysicalAddress & 0x0fff) == 0)
|| ((xor & 0x00f0) == xor && (myPhysicalAddress & 0x00ff) == 0)
|| ((xor & 0x000f) == xor && (myPhysicalAddress & 0x000f) == 0)) {
return true;
}
return false;
}
protected void switchToAudioInput() {
// TODO(b/111396634): switch input according to PROPERTY_SYSTEM_AUDIO_MODE_AUDIO_PORT
}
protected boolean isDirectConnectToTv() {
int myPhysicalAddress = mService.getPhysicalAddress();
return (myPhysicalAddress & Constants.ROUTING_PATH_TOP_MASK) == myPhysicalAddress;
}
private void updateAudioManagerForSystemAudio(boolean on) {
int device = mService.getAudioManager().setHdmiSystemAudioSupported(on);
HdmiLogger.debug("[A]UpdateSystemAudio mode[on=%b] output=[%X]", on, device);
}
protected boolean isSystemAudioControlFeatureEnabled() {
synchronized (mLock) {
return mSystemAudioControlFeatureEnabled;
}
}
protected boolean isSystemAudioActivated() {
synchronized (mLock) {
return mSystemAudioActivated;
}
}
protected void terminateSystemAudioMode() {
// remove pending initiation actions
removeAction(SystemAudioInitiationActionFromAvr.class);
if (!isSystemAudioActivated()) {
return;
}
if (setSystemAudioMode(false)) {
// send <Set System Audio Mode> [“Off”]
mService.sendCecCommand(
HdmiCecMessageBuilder.buildSetSystemAudioMode(
mAddress, Constants.ADDR_BROADCAST, false));
}
}
/** Reports if System Audio Mode is supported by the connected TV */
interface TvSystemAudioModeSupportedCallback {
/** {@code supported} is true if the TV is connected and supports System Audio Mode. */
void onResult(boolean supported);
}
/**
* Queries the connected TV to detect if System Audio Mode is supported by the TV.
*
* <p>This query may take up to 2 seconds to complete.
*
* <p>The result of the query may be cached until Audio device type is put in standby or loses
* its physical address.
*/
void queryTvSystemAudioModeSupport(TvSystemAudioModeSupportedCallback callback) {
if (!mTvSystemAudioModeSupport) {
addAndStartAction(new DetectTvSystemAudioModeSupportAction(this, callback));
} else {
callback.onResult(true);
}
}
void setTvSystemAudioModeSupport(boolean supported) {
mTvSystemAudioModeSupport = supported;
}
@VisibleForTesting
protected boolean isArcEnabled() {
synchronized (mLock) {
return mArcEstablished;
}
}
}