| /* |
| * 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 android.hardware.hdmi.DeviceFeatures.FEATURE_NOT_SUPPORTED; |
| import static android.hardware.hdmi.DeviceFeatures.FEATURE_SUPPORTED; |
| |
| 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.annotation.Nullable; |
| import android.content.ActivityNotFoundException; |
| import android.content.Intent; |
| import android.hardware.hdmi.DeviceFeatures; |
| import android.hardware.hdmi.HdmiControlManager; |
| import android.hardware.hdmi.HdmiDeviceInfo; |
| import android.hardware.hdmi.HdmiPortInfo; |
| import android.hardware.hdmi.IHdmiControlCallback; |
| import android.media.AudioDeviceInfo; |
| import android.media.AudioFormat; |
| import android.media.AudioManager; |
| import android.media.AudioSystem; |
| import android.media.tv.TvContract; |
| import android.media.tv.TvInputInfo; |
| import android.media.tv.TvInputManager.TvInputCallback; |
| import android.os.SystemProperties; |
| import android.sysprop.HdmiProperties; |
| import android.util.Slog; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.util.IndentingPrintWriter; |
| import com.android.server.hdmi.Constants.AudioCodec; |
| import com.android.server.hdmi.DeviceDiscoveryAction.DeviceDiscoveryCallback; |
| import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly; |
| import com.android.server.hdmi.HdmiUtils.CodecSad; |
| import com.android.server.hdmi.HdmiUtils.DeviceConfig; |
| |
| import org.xmlpull.v1.XmlPullParserException; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.stream.Collectors; |
| |
| /** |
| * Represent a logical device of type {@link HdmiDeviceInfo#DEVICE_AUDIO_SYSTEM} residing in Android |
| * system. |
| */ |
| public class HdmiCecLocalDeviceAudioSystem extends HdmiCecLocalDeviceSource { |
| |
| private static final String TAG = "HdmiCecLocalDeviceAudioSystem"; |
| |
| private static final boolean WAKE_ON_HOTPLUG = false; |
| private static final int MAX_CHANNELS = 8; |
| private static final HashMap<Integer, List<Integer>> AUDIO_CODECS_MAP = |
| mapAudioCodecWithAudioFormat(); |
| |
| // Whether the System Audio Control feature is enabled or not. True by default. |
| @GuardedBy("mLock") |
| private boolean mSystemAudioControlFeatureEnabled; |
| |
| /** |
| * Indicates if the TV that the current device is connected to supports System Audio Mode or not |
| * |
| * <p>If the current device has no information on this, keep mTvSystemAudioModeSupport null |
| * |
| * <p>The boolean will be reset to null every time when the current device goes to standby |
| * or loses its physical address. |
| */ |
| private Boolean mTvSystemAudioModeSupport = null; |
| |
| // Whether ARC is available or not. "true" means that ARC is established between TV and |
| // AVR as audio receiver. |
| @ServiceThreadOnly private boolean mArcEstablished = false; |
| |
| // If the current device uses TvInput for ARC. We assume all other inputs also use TvInput |
| // when ARC is using TvInput. |
| private boolean mArcIntentUsed = HdmiProperties.arc_port().orElse("0").contains("tvinput"); |
| |
| // Keeps the mapping (HDMI port ID to TV input URI) to keep track of the TV inputs ready to |
| // accept input switching request from HDMI devices. |
| @GuardedBy("mLock") |
| private final HashMap<Integer, String> mPortIdToTvInputs = new HashMap<>(); |
| |
| // A map from TV input id to HDMI device info. |
| @GuardedBy("mLock") |
| private final HashMap<String, HdmiDeviceInfo> mTvInputsToDeviceInfo = new HashMap<>(); |
| |
| // Message buffer used to buffer selected messages to process later. <Active Source> |
| // from a source device, for instance, needs to be buffered if the device is not |
| // discovered yet. The buffered commands are taken out and when they are ready to |
| // handle. |
| private final DelayedMessageBuffer mDelayedMessageBuffer = new DelayedMessageBuffer(this); |
| |
| protected HdmiCecLocalDeviceAudioSystem(HdmiControlService service) { |
| super(service, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM); |
| mRoutingControlFeatureEnabled = mService.getHdmiCecConfig().getIntValue( |
| HdmiControlManager.CEC_SETTING_NAME_ROUTING_CONTROL) |
| == HdmiControlManager.ROUTING_CONTROL_ENABLED; |
| mSystemAudioControlFeatureEnabled = mService.getHdmiCecConfig().getIntValue( |
| HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_CONTROL) |
| == HdmiControlManager.SYSTEM_AUDIO_CONTROL_ENABLED; |
| mStandbyHandler = new HdmiCecStandbyModeHandler(service, this); |
| } |
| |
| private static final String SHORT_AUDIO_DESCRIPTOR_CONFIG_PATH = "/vendor/etc/sadConfig.xml"; |
| |
| private final TvInputCallback mTvInputCallback = new TvInputCallback() { |
| @Override |
| public void onInputAdded(String inputId) { |
| addOrUpdateTvInput(inputId); |
| } |
| |
| @Override |
| public void onInputRemoved(String inputId) { |
| removeTvInput(inputId); |
| } |
| |
| @Override |
| public void onInputUpdated(String inputId) { |
| addOrUpdateTvInput(inputId); |
| } |
| }; |
| |
| @ServiceThreadOnly |
| private void addOrUpdateTvInput(String inputId) { |
| assertRunOnServiceThread(); |
| synchronized (mLock) { |
| TvInputInfo tvInfo = mService.getTvInputManager().getTvInputInfo(inputId); |
| if (tvInfo == null) { |
| return; |
| } |
| HdmiDeviceInfo info = tvInfo.getHdmiDeviceInfo(); |
| if (info == null) { |
| return; |
| } |
| mPortIdToTvInputs.put(info.getPortId(), inputId); |
| mTvInputsToDeviceInfo.put(inputId, info); |
| if (info.isCecDevice()) { |
| processDelayedActiveSource(info.getLogicalAddress()); |
| } |
| } |
| } |
| |
| @ServiceThreadOnly |
| private void removeTvInput(String inputId) { |
| assertRunOnServiceThread(); |
| synchronized (mLock) { |
| if (mTvInputsToDeviceInfo.get(inputId) == null) { |
| return; |
| } |
| int portId = mTvInputsToDeviceInfo.get(inputId).getPortId(); |
| mPortIdToTvInputs.remove(portId); |
| mTvInputsToDeviceInfo.remove(inputId); |
| } |
| } |
| |
| @Override |
| @ServiceThreadOnly |
| protected boolean isInputReady(int portId) { |
| assertRunOnServiceThread(); |
| String tvInputId = mPortIdToTvInputs.get(portId); |
| HdmiDeviceInfo info = mTvInputsToDeviceInfo.get(tvInputId); |
| return info != null; |
| } |
| |
| @Override |
| protected DeviceFeatures computeDeviceFeatures() { |
| boolean arcSupport = SystemProperties.getBoolean(Constants.PROPERTY_ARC_SUPPORT, true); |
| |
| return DeviceFeatures.NO_FEATURES_SUPPORTED.toBuilder() |
| .setArcRxSupport(arcSupport ? FEATURE_SUPPORTED : FEATURE_NOT_SUPPORTED) |
| .build(); |
| } |
| |
| @Override |
| @ServiceThreadOnly |
| void onHotplug(int portId, boolean connected) { |
| assertRunOnServiceThread(); |
| if (WAKE_ON_HOTPLUG && connected) { |
| mService.wakeUp(); |
| } |
| if (mService.getPortInfo(portId).getType() == HdmiPortInfo.PORT_OUTPUT) { |
| mCecMessageCache.flushAll(); |
| if (!connected) { |
| if (isSystemAudioActivated()) { |
| mTvSystemAudioModeSupport = null; |
| checkSupportAndSetSystemAudioMode(false); |
| } |
| if (isArcEnabled()) { |
| setArcStatus(false); |
| } |
| } |
| } else if (!connected && mPortIdToTvInputs.get(portId) != null) { |
| String tvInputId = mPortIdToTvInputs.get(portId); |
| HdmiDeviceInfo info = mTvInputsToDeviceInfo.get(tvInputId); |
| if (info == null) { |
| return; |
| } |
| // Update with TIF on the device removal. TIF callback will update |
| // mPortIdToTvInputs and mPortIdToTvInputs. |
| mService.getHdmiCecNetwork().removeCecDevice(this, info.getLogicalAddress()); |
| } |
| } |
| |
| @Override |
| @ServiceThreadOnly |
| protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) { |
| terminateAudioReturnChannel(); |
| |
| super.disableDevice(initiatedByCec, callback); |
| assertRunOnServiceThread(); |
| mService.unregisterTvInputCallback(mTvInputCallback); |
| } |
| |
| @Override |
| @ServiceThreadOnly |
| protected void onStandby(boolean initiatedByCec, int standbyAction) { |
| assertRunOnServiceThread(); |
| // Invalidate the internal active source record when goes to standby |
| // This set will also update mIsActiveSource |
| mService.setActiveSource(Constants.ADDR_INVALID, Constants.INVALID_PHYSICAL_ADDRESS, |
| "HdmiCecLocalDeviceAudioSystem#onStandby()"); |
| mTvSystemAudioModeSupport = null; |
| // Record the last state of System Audio Control before going to standby |
| synchronized (mLock) { |
| mService.writeStringSystemProperty( |
| Constants.PROPERTY_LAST_SYSTEM_AUDIO_CONTROL, |
| isSystemAudioActivated() ? "true" : "false"); |
| } |
| terminateSystemAudioMode(); |
| } |
| |
| @Override |
| @ServiceThreadOnly |
| protected void onAddressAllocated(int logicalAddress, int reason) { |
| assertRunOnServiceThread(); |
| if (reason == mService.INITIATED_BY_ENABLE_CEC) { |
| mService.setAndBroadcastActiveSource(mService.getPhysicalAddress(), |
| getDeviceInfo().getDeviceType(), Constants.ADDR_BROADCAST, |
| "HdmiCecLocalDeviceAudioSystem#onAddressAllocated()"); |
| } |
| mService.sendCecCommand( |
| HdmiCecMessageBuilder.buildReportPhysicalAddressCommand( |
| getDeviceInfo().getLogicalAddress(), |
| mService.getPhysicalAddress(), |
| mDeviceType)); |
| mService.sendCecCommand( |
| HdmiCecMessageBuilder.buildDeviceVendorIdCommand( |
| getDeviceInfo().getLogicalAddress(), mService.getVendorId())); |
| mService.registerTvInputCallback(mTvInputCallback); |
| // Some TVs, for example Mi TV, need ARC on before turning System Audio Mode on |
| // to request Short Audio Descriptor. Since ARC and SAM are independent, |
| // we can turn on ARC anyways when audio system device just boots up. |
| initArcOnFromAvr(); |
| |
| // This prevents turning on of System Audio Mode during a quiescent boot. If the quiescent |
| // boot is exited just after this check, this code will be executed only at the next |
| // wake-up. |
| if (!mService.isScreenOff()) { |
| 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); |
| } |
| mService.getHdmiCecNetwork().clearDeviceList(); |
| launchDeviceDiscovery(); |
| startQueuedActions(); |
| } |
| |
| @Override |
| protected int findKeyReceiverAddress() { |
| if (getActiveSource().isValid()) { |
| return getActiveSource().logicalAddress; |
| } |
| return Constants.ADDR_INVALID; |
| } |
| |
| @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 && isSystemAudioControlFeatureEnabled())) { |
| addAndStartAction(new SystemAudioInitiationActionFromAvr(this)); |
| } |
| } |
| |
| @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(); |
| mService.writeStringSystemProperty( |
| Constants.PROPERTY_PREFERRED_ADDRESS_AUDIO_SYSTEM, String.valueOf(addr)); |
| } |
| |
| @ServiceThreadOnly |
| void processDelayedActiveSource(int address) { |
| assertRunOnServiceThread(); |
| mDelayedMessageBuffer.processActiveSource(address); |
| } |
| |
| @Override |
| @ServiceThreadOnly |
| @Constants.HandleMessageResult |
| protected int handleActiveSource(HdmiCecMessage message) { |
| assertRunOnServiceThread(); |
| int logicalAddress = message.getSource(); |
| int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams()); |
| if (HdmiUtils.getLocalPortFromPhysicalAddress( |
| physicalAddress, mService.getPhysicalAddress()) |
| == HdmiUtils.TARGET_NOT_UNDER_LOCAL_DEVICE) { |
| return super.handleActiveSource(message); |
| } |
| // If the new Active Source is under the current device, check if the device info and the TV |
| // input is ready to switch to the new Active Source. If not ready, buffer the cec command |
| // to handle later when the device is ready. |
| HdmiDeviceInfo info = mService.getHdmiCecNetwork().getCecDeviceInfo(logicalAddress); |
| if (info == null) { |
| HdmiLogger.debug("Device info %X not found; buffering the command", logicalAddress); |
| mDelayedMessageBuffer.add(message); |
| } else if (!isInputReady(info.getPortId())){ |
| HdmiLogger.debug("Input not ready for device: %X; buffering the command", info.getId()); |
| mDelayedMessageBuffer.add(message); |
| } else { |
| mDelayedMessageBuffer.removeActiveSource(); |
| return super.handleActiveSource(message); |
| } |
| return Constants.HANDLED; |
| } |
| |
| @Override |
| @ServiceThreadOnly |
| @Constants.HandleMessageResult |
| protected int handleInitiateArc(HdmiCecMessage message) { |
| assertRunOnServiceThread(); |
| // TODO(amyjojo): implement initiate arc handler |
| HdmiLogger.debug(TAG + "Stub handleInitiateArc"); |
| return Constants.HANDLED; |
| } |
| |
| @Override |
| @ServiceThreadOnly |
| @Constants.HandleMessageResult |
| protected int handleReportArcInitiate(HdmiCecMessage message) { |
| assertRunOnServiceThread(); |
| /* |
| * Ideally, we should have got this response before the {@link ArcInitiationActionFromAvr} |
| * has timed out. Even if the response is late, {@link ArcInitiationActionFromAvr |
| * #handleInitiateArcTimeout()} would not have disabled ARC. So nothing needs to be done |
| * here. |
| */ |
| return Constants.HANDLED; |
| } |
| |
| @Override |
| @ServiceThreadOnly |
| @Constants.HandleMessageResult |
| protected int handleReportArcTermination(HdmiCecMessage message) { |
| assertRunOnServiceThread(); |
| processArcTermination(); |
| return Constants.HANDLED; |
| } |
| |
| @Override |
| @ServiceThreadOnly |
| @Constants.HandleMessageResult |
| protected int handleGiveAudioStatus(HdmiCecMessage message) { |
| assertRunOnServiceThread(); |
| if (isSystemAudioControlFeatureEnabled() && mService.getHdmiCecVolumeControl() |
| == HdmiControlManager.VOLUME_CONTROL_ENABLED) { |
| reportAudioStatus(message.getSource()); |
| return Constants.HANDLED; |
| } |
| return Constants.ABORT_REFUSED; |
| } |
| |
| @Override |
| @ServiceThreadOnly |
| @Constants.HandleMessageResult |
| protected int handleGiveSystemAudioModeStatus(HdmiCecMessage message) { |
| assertRunOnServiceThread(); |
| // If the audio system is initiating the system audio mode on and TV asks the sam status at |
| // the same time, respond with true. Since we know TV supports sam in this situation. |
| // If the query comes from STB, we should respond with the current sam status and the STB |
| // should listen to the <Set System Audio Mode> broadcasting. |
| boolean isSystemAudioModeOnOrTurningOn = isSystemAudioActivated(); |
| if (!isSystemAudioModeOnOrTurningOn |
| && message.getSource() == Constants.ADDR_TV |
| && hasAction(SystemAudioInitiationActionFromAvr.class)) { |
| isSystemAudioModeOnOrTurningOn = true; |
| } |
| mService.sendCecCommand( |
| HdmiCecMessageBuilder.buildReportSystemAudioMode( |
| getDeviceInfo().getLogicalAddress(), |
| message.getSource(), |
| isSystemAudioModeOnOrTurningOn)); |
| return Constants.HANDLED; |
| } |
| |
| @Override |
| @ServiceThreadOnly |
| @Constants.HandleMessageResult |
| protected int handleRequestArcInitiate(HdmiCecMessage message) { |
| assertRunOnServiceThread(); |
| removeAction(ArcInitiationActionFromAvr.class); |
| if (!mService.readBooleanSystemProperty(Constants.PROPERTY_ARC_SUPPORT, true)) { |
| return Constants.ABORT_UNRECOGNIZED_OPCODE; |
| } else if (!isDirectConnectToTv()) { |
| HdmiLogger.debug("AVR device is not directly connected with TV"); |
| return Constants.ABORT_NOT_IN_CORRECT_MODE; |
| } else { |
| addAndStartAction(new ArcInitiationActionFromAvr(this)); |
| return Constants.HANDLED; |
| } |
| } |
| |
| @Override |
| @ServiceThreadOnly |
| @Constants.HandleMessageResult |
| protected int handleRequestArcTermination(HdmiCecMessage message) { |
| assertRunOnServiceThread(); |
| if (!SystemProperties.getBoolean(Constants.PROPERTY_ARC_SUPPORT, true)) { |
| return Constants.ABORT_UNRECOGNIZED_OPCODE; |
| } else if (!isArcEnabled()) { |
| HdmiLogger.debug("ARC is not established between TV and AVR device"); |
| return Constants.ABORT_NOT_IN_CORRECT_MODE; |
| } else { |
| if (!getActions(ArcTerminationActionFromAvr.class).isEmpty() |
| && !getActions(ArcTerminationActionFromAvr.class).get(0).mCallbacks.isEmpty()) { |
| IHdmiControlCallback callback = |
| getActions(ArcTerminationActionFromAvr.class).get(0).mCallbacks.get(0); |
| removeAction(ArcTerminationActionFromAvr.class); |
| addAndStartAction(new ArcTerminationActionFromAvr(this, callback)); |
| } else { |
| removeAction(ArcTerminationActionFromAvr.class); |
| addAndStartAction(new ArcTerminationActionFromAvr(this)); |
| } |
| return Constants.HANDLED; |
| } |
| } |
| |
| @ServiceThreadOnly |
| @Constants.HandleMessageResult |
| protected int handleRequestShortAudioDescriptor(HdmiCecMessage message) { |
| assertRunOnServiceThread(); |
| HdmiLogger.debug(TAG + "Stub handleRequestShortAudioDescriptor"); |
| if (!isSystemAudioControlFeatureEnabled()) { |
| return Constants.ABORT_REFUSED; |
| } |
| if (!isSystemAudioActivated()) { |
| return Constants.ABORT_NOT_IN_CORRECT_MODE; |
| } |
| |
| List<DeviceConfig> config = null; |
| File file = new File(SHORT_AUDIO_DESCRIPTOR_CONFIG_PATH); |
| if (file.exists()) { |
| try { |
| InputStream in = new FileInputStream(file); |
| config = HdmiUtils.ShortAudioDescriptorXmlParser.parse(in); |
| in.close(); |
| } catch (IOException e) { |
| Slog.e(TAG, "Error reading file: " + file, e); |
| } catch (XmlPullParserException e) { |
| Slog.e(TAG, "Unable to parse file: " + file, e); |
| } |
| } |
| |
| @AudioCodec int[] audioCodecs = parseAudioCodecs(message.getParams()); |
| byte[] sadBytes; |
| if (config != null && config.size() > 0) { |
| sadBytes = getSupportedShortAudioDescriptorsFromConfig(config, audioCodecs); |
| } else { |
| AudioDeviceInfo deviceInfo = getSystemAudioDeviceInfo(); |
| if (deviceInfo == null) { |
| return Constants.ABORT_UNABLE_TO_DETERMINE; |
| } |
| |
| sadBytes = getSupportedShortAudioDescriptors(deviceInfo, audioCodecs); |
| } |
| |
| if (sadBytes.length == 0) { |
| return Constants.ABORT_INVALID_OPERAND; |
| } else { |
| mService.sendCecCommand( |
| HdmiCecMessageBuilder.buildReportShortAudioDescriptor( |
| getDeviceInfo().getLogicalAddress(), message.getSource(), sadBytes)); |
| return Constants.HANDLED; |
| } |
| } |
| |
| @VisibleForTesting |
| byte[] getSupportedShortAudioDescriptors( |
| AudioDeviceInfo deviceInfo, @AudioCodec int[] audioCodecs) { |
| ArrayList<byte[]> sads = new ArrayList<>(audioCodecs.length); |
| for (@AudioCodec int audioCodec : audioCodecs) { |
| byte[] sad = getSupportedShortAudioDescriptor(deviceInfo, audioCodec); |
| if (sad != null) { |
| if (sad.length == 3) { |
| |
| sads.add(sad); |
| } else { |
| HdmiLogger.warning( |
| "Dropping Short Audio Descriptor with length %d for requested codec %x", |
| sad.length, audioCodec); |
| } |
| } |
| } |
| return getShortAudioDescriptorBytes(sads); |
| } |
| |
| private byte[] getSupportedShortAudioDescriptorsFromConfig( |
| List<DeviceConfig> deviceConfig, @AudioCodec int[] audioCodecs) { |
| DeviceConfig deviceConfigToUse = null; |
| String audioDeviceName = SystemProperties.get( |
| Constants.PROPERTY_SYSTEM_AUDIO_MODE_AUDIO_PORT, |
| "VX_AUDIO_DEVICE_IN_HDMI_ARC"); |
| for (DeviceConfig device : deviceConfig) { |
| if (device.name.equals(audioDeviceName)) { |
| deviceConfigToUse = device; |
| break; |
| } |
| } |
| if (deviceConfigToUse == null) { |
| Slog.w(TAG, "sadConfig.xml does not have required device info for " + audioDeviceName); |
| return new byte[0]; |
| } |
| HashMap<Integer, byte[]> map = new HashMap<>(); |
| ArrayList<byte[]> sads = new ArrayList<>(audioCodecs.length); |
| for (CodecSad codecSad : deviceConfigToUse.supportedCodecs) { |
| map.put(codecSad.audioCodec, codecSad.sad); |
| } |
| for (int i = 0; i < audioCodecs.length; i++) { |
| if (map.containsKey(audioCodecs[i])) { |
| byte[] sad = map.get(audioCodecs[i]); |
| if (sad != null && sad.length == 3) { |
| sads.add(sad); |
| } |
| } |
| } |
| return getShortAudioDescriptorBytes(sads); |
| } |
| |
| private byte[] getShortAudioDescriptorBytes(ArrayList<byte[]> sads) { |
| // Short Audio Descriptors are always 3 bytes long. |
| byte[] bytes = new byte[sads.size() * 3]; |
| int index = 0; |
| for (byte[] sad : sads) { |
| System.arraycopy(sad, 0, bytes, index, 3); |
| index += 3; |
| } |
| return bytes; |
| } |
| |
| /** |
| * Returns a 3 byte short audio descriptor as described in CEC 1.4 table 29 or null if the |
| * audioCodec is not supported. |
| */ |
| @Nullable |
| @VisibleForTesting |
| byte[] getSupportedShortAudioDescriptor( |
| AudioDeviceInfo deviceInfo, @AudioCodec int audioCodec) { |
| byte[] shortAudioDescriptor = new byte[3]; |
| |
| int[] deviceSupportedAudioFormats = deviceInfo.getEncodings(); |
| // Return null when audioCodec or device does not support any audio formats. |
| if (!AUDIO_CODECS_MAP.containsKey(audioCodec) || deviceSupportedAudioFormats.length == 0) { |
| return null; |
| } |
| List<Integer> audioCodecSupportedAudioFormats = AUDIO_CODECS_MAP.get(audioCodec); |
| |
| for (int supportedAudioFormat : deviceSupportedAudioFormats) { |
| if (audioCodecSupportedAudioFormats.contains(supportedAudioFormat)) { |
| // Initialise the first two bytes of short audio descriptor. |
| shortAudioDescriptor[0] = getFirstByteOfSAD(deviceInfo, audioCodec); |
| shortAudioDescriptor[1] = getSecondByteOfSAD(deviceInfo); |
| switch (audioCodec) { |
| case Constants.AUDIO_CODEC_NONE: { |
| return null; |
| } |
| case Constants.AUDIO_CODEC_LPCM: { |
| if (supportedAudioFormat == AudioFormat.ENCODING_PCM_16BIT) { |
| shortAudioDescriptor[2] = (byte) 0x01; |
| } else if (supportedAudioFormat |
| == AudioFormat.ENCODING_PCM_24BIT_PACKED) { |
| shortAudioDescriptor[2] = (byte) 0x04; |
| } else { |
| // Since no bit is reserved for these audio formats in LPCM codec. |
| shortAudioDescriptor[2] = (byte) 0x00; |
| } |
| return shortAudioDescriptor; |
| } |
| case Constants.AUDIO_CODEC_DD: |
| case Constants.AUDIO_CODEC_MPEG1: |
| case Constants.AUDIO_CODEC_MP3: |
| case Constants.AUDIO_CODEC_MPEG2: |
| case Constants.AUDIO_CODEC_AAC: |
| case Constants.AUDIO_CODEC_DTS: { |
| shortAudioDescriptor[2] = getThirdSadByteForCodecs2Through8(deviceInfo); |
| return shortAudioDescriptor; |
| } |
| case Constants.AUDIO_CODEC_DDP: |
| case Constants.AUDIO_CODEC_DTSHD: |
| case Constants.AUDIO_CODEC_TRUEHD: { |
| // Default value is 0x0 unless defined by Audio Codec Vendor. |
| shortAudioDescriptor[2] = (byte) 0x00; |
| return shortAudioDescriptor; |
| } |
| case Constants.AUDIO_CODEC_ATRAC: |
| case Constants.AUDIO_CODEC_ONEBITAUDIO: |
| case Constants.AUDIO_CODEC_DST: |
| case Constants.AUDIO_CODEC_WMAPRO: |
| // Not supported. |
| default: { |
| return null; |
| } |
| } |
| } |
| } |
| return null; |
| } |
| |
| private static HashMap<Integer, List<Integer>> mapAudioCodecWithAudioFormat() { |
| // Mapping the values of @AudioCodec audio codecs with @AudioFormat audio formats. |
| HashMap<Integer, List<Integer>> audioCodecsMap = new HashMap<Integer, List<Integer>>(); |
| |
| audioCodecsMap.put(Constants.AUDIO_CODEC_NONE, List.of(AudioFormat.ENCODING_DEFAULT)); |
| audioCodecsMap.put( |
| Constants.AUDIO_CODEC_LPCM, |
| List.of( |
| AudioFormat.ENCODING_PCM_8BIT, |
| AudioFormat.ENCODING_PCM_16BIT, |
| AudioFormat.ENCODING_PCM_FLOAT, |
| AudioFormat.ENCODING_PCM_24BIT_PACKED, |
| AudioFormat.ENCODING_PCM_32BIT)); |
| audioCodecsMap.put(Constants.AUDIO_CODEC_DD, List.of(AudioFormat.ENCODING_AC3)); |
| audioCodecsMap.put(Constants.AUDIO_CODEC_MPEG1, List.of(AudioFormat.ENCODING_AAC_HE_V1)); |
| audioCodecsMap.put(Constants.AUDIO_CODEC_MPEG2, List.of(AudioFormat.ENCODING_AAC_HE_V2)); |
| audioCodecsMap.put(Constants.AUDIO_CODEC_MP3, List.of(AudioFormat.ENCODING_MP3)); |
| audioCodecsMap.put(Constants.AUDIO_CODEC_AAC, List.of(AudioFormat.ENCODING_AAC_LC)); |
| audioCodecsMap.put(Constants.AUDIO_CODEC_DTS, List.of(AudioFormat.ENCODING_DTS)); |
| audioCodecsMap.put( |
| Constants.AUDIO_CODEC_DDP, |
| List.of(AudioFormat.ENCODING_E_AC3, AudioFormat.ENCODING_E_AC3_JOC)); |
| audioCodecsMap.put(Constants.AUDIO_CODEC_DTSHD, List.of(AudioFormat.ENCODING_DTS_HD)); |
| audioCodecsMap.put( |
| Constants.AUDIO_CODEC_TRUEHD, |
| List.of(AudioFormat.ENCODING_DOLBY_TRUEHD, AudioFormat.ENCODING_DOLBY_MAT)); |
| |
| return audioCodecsMap; |
| } |
| |
| private byte getFirstByteOfSAD(AudioDeviceInfo deviceInfo, @AudioCodec int audioCodec) { |
| byte firstByte = 0; |
| int maxNumberOfChannels = getMaxNumberOfChannels(deviceInfo); |
| |
| // Fill bits 0-2 of the first byte. |
| firstByte |= (maxNumberOfChannels - 1); |
| |
| // Fill bits 3-6 of the first byte. |
| firstByte |= (audioCodec << 3); |
| |
| return firstByte; |
| } |
| |
| private byte getSecondByteOfSAD(AudioDeviceInfo deviceInfo) { |
| ArrayList<Integer> samplingRates = |
| new ArrayList<Integer>(Arrays.asList(32, 44, 48, 88, 96, 176, 192)); |
| |
| // samplingRatesdevicesupports is guaranteed to be not null |
| int[] samplingRatesDeviceSupports = deviceInfo.getSampleRates(); |
| if (samplingRatesDeviceSupports.length == 0) { |
| Slog.e(TAG, "Device supports arbitrary rates"); |
| // Since device supports arbitrary rates, we will return 0x7f since bit 7 is reserved. |
| return (byte) 0x7f; |
| } |
| byte secondByte = 0; |
| for (int supportedSampleRate : samplingRatesDeviceSupports) { |
| if (samplingRates.contains(supportedSampleRate)) { |
| int index = samplingRates.indexOf(supportedSampleRate); |
| // Setting the bit of a sample rate which is being supported. |
| secondByte |= (1 << index); |
| } |
| } |
| |
| return secondByte; |
| } |
| |
| /** |
| * Empty array from deviceInfo.getChannelCounts() implies device supports arbitrary channel |
| * counts and hence we assume max channels are supported by the device. |
| */ |
| private int getMaxNumberOfChannels(AudioDeviceInfo deviceInfo) { |
| int maxNumberOfChannels = MAX_CHANNELS; |
| int[] channelCounts = deviceInfo.getChannelCounts(); |
| if (channelCounts.length != 0) { |
| maxNumberOfChannels = channelCounts[channelCounts.length - 1]; |
| maxNumberOfChannels = |
| (maxNumberOfChannels > MAX_CHANNELS ? MAX_CHANNELS : maxNumberOfChannels); |
| } |
| return maxNumberOfChannels; |
| } |
| |
| private byte getThirdSadByteForCodecs2Through8(AudioDeviceInfo deviceInfo) { |
| /* |
| * Here, we are assuming that max bit rate is closely equals to the max sampling rate the |
| * device supports. |
| */ |
| int maxSamplingRate = 0; |
| int[] samplingRatesDeviceSupports = deviceInfo.getSampleRates(); |
| if (samplingRatesDeviceSupports.length == 0) { |
| maxSamplingRate = 192; |
| } else { |
| for (int sampleRate : samplingRatesDeviceSupports) { |
| if (maxSamplingRate < sampleRate) { |
| maxSamplingRate = sampleRate; |
| } |
| } |
| } |
| |
| return (byte) (maxSamplingRate / 8); |
| } |
| |
| @Nullable |
| private AudioDeviceInfo getSystemAudioDeviceInfo() { |
| AudioManager audioManager = mService.getContext().getSystemService(AudioManager.class); |
| if (audioManager == null) { |
| HdmiLogger.error( |
| "Error getting system audio device because AudioManager not available."); |
| return null; |
| } |
| AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS); |
| HdmiLogger.debug("Found %d audio input devices", devices.length); |
| for (AudioDeviceInfo device : devices) { |
| HdmiLogger.debug("%s at port %s", device.getProductName(), device.getPort()); |
| HdmiLogger.debug("Supported encodings are %s", |
| Arrays.stream(device.getEncodings()).mapToObj( |
| AudioFormat::toLogFriendlyEncoding |
| ).collect(Collectors.joining(", "))); |
| if (device.getType() == AudioDeviceInfo.TYPE_HDMI_ARC) { |
| return device; |
| } |
| } |
| return null; |
| } |
| |
| @AudioCodec |
| private int[] parseAudioCodecs(byte[] params) { |
| @AudioCodec int[] audioCodecs = new int[params.length]; |
| for (int i = 0; i < params.length; i++) { |
| byte val = params[i]; |
| audioCodecs[i] = |
| val >= 1 && val <= Constants.AUDIO_CODEC_MAX ? val : Constants.AUDIO_CODEC_NONE; |
| } |
| return audioCodecs; |
| } |
| |
| @Override |
| @ServiceThreadOnly |
| @Constants.HandleMessageResult |
| protected int handleSystemAudioModeRequest(HdmiCecMessage message) { |
| assertRunOnServiceThread(); |
| boolean systemAudioStatusOn = message.getParams().length != 0; |
| // Check if the request comes from a non-TV device. |
| // Need to check if TV supports System Audio Control |
| // if non-TV device tries to turn on the feature |
| if (message.getSource() != Constants.ADDR_TV) { |
| if (systemAudioStatusOn) { |
| return handleSystemAudioModeOnFromNonTvDevice(message); |
| } |
| } else { |
| // If TV request the feature on |
| // cache TV supporting System Audio Control |
| // until Audio System loses its physical address. |
| setTvSystemAudioModeSupport(true); |
| } |
| // If TV or Audio System does not support the feature, |
| // will send abort command. |
| if (!checkSupportAndSetSystemAudioMode(systemAudioStatusOn)) { |
| return Constants.ABORT_REFUSED; |
| } |
| |
| mService.sendCecCommand( |
| HdmiCecMessageBuilder.buildSetSystemAudioMode( |
| getDeviceInfo().getLogicalAddress(), |
| Constants.ADDR_BROADCAST, |
| systemAudioStatusOn)); |
| |
| if (systemAudioStatusOn) { |
| // If TV sends out SAM Request with a path of a non-CEC device, which should not show |
| // up in the CEC device list and not under the current AVR device, the AVR would switch |
| // to ARC. |
| int sourcePhysicalAddress = HdmiUtils.twoBytesToInt(message.getParams()); |
| if (HdmiUtils.getLocalPortFromPhysicalAddress( |
| sourcePhysicalAddress, getDeviceInfo().getPhysicalAddress()) |
| != HdmiUtils.TARGET_NOT_UNDER_LOCAL_DEVICE) { |
| return Constants.HANDLED; |
| } |
| HdmiDeviceInfo safeDeviceInfoByPath = |
| mService.getHdmiCecNetwork().getSafeDeviceInfoByPath(sourcePhysicalAddress); |
| if (safeDeviceInfoByPath == null) { |
| switchInputOnReceivingNewActivePath(sourcePhysicalAddress); |
| } |
| } |
| return Constants.HANDLED; |
| } |
| |
| @Override |
| @ServiceThreadOnly |
| @Constants.HandleMessageResult |
| protected int handleSetSystemAudioMode(HdmiCecMessage message) { |
| assertRunOnServiceThread(); |
| if (!checkSupportAndSetSystemAudioMode( |
| HdmiUtils.parseCommandParamSystemAudioStatus(message))) { |
| return Constants.ABORT_REFUSED; |
| } |
| return Constants.HANDLED; |
| } |
| |
| @Override |
| @ServiceThreadOnly |
| @Constants.HandleMessageResult |
| protected int handleSystemAudioModeStatus(HdmiCecMessage message) { |
| assertRunOnServiceThread(); |
| if (!checkSupportAndSetSystemAudioMode( |
| HdmiUtils.parseCommandParamSystemAudioStatus(message))) { |
| return Constants.ABORT_REFUSED; |
| } |
| return Constants.HANDLED; |
| } |
| |
| @ServiceThreadOnly |
| void setArcStatus(boolean enabled) { |
| 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; |
| } |
| |
| void processArcTermination() { |
| setArcStatus(false); |
| // Switch away from ARC input when ARC is terminated. |
| if (getLocalActivePort() == Constants.CEC_SWITCH_ARC) { |
| routeToInputFromPortId(getRoutingPort()); |
| } |
| } |
| |
| /** Switch hardware ARC circuit in the system. */ |
| @ServiceThreadOnly |
| private void enableAudioReturnChannel(boolean enabled) { |
| assertRunOnServiceThread(); |
| mService.enableAudioReturnChannel( |
| Integer.parseInt(HdmiProperties.arc_port().orElse("0")), |
| enabled); |
| } |
| |
| private void notifyArcStatusToAudioService(boolean enabled) { |
| // Note that we don't set any name to ARC. |
| mService.getAudioManager() |
| .setWiredDeviceConnectionState(AudioSystem.DEVICE_IN_HDMI_ARC, enabled ? 1 : 0, "", ""); |
| } |
| |
| void reportAudioStatus(int source) { |
| assertRunOnServiceThread(); |
| if (mService.getHdmiCecVolumeControl() |
| == HdmiControlManager.VOLUME_CONTROL_DISABLED) { |
| return; |
| } |
| |
| 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 minVolume = mService.getAudioManager().getStreamMinVolume(AudioManager.STREAM_MUSIC); |
| int scaledVolume = VolumeControlAction.scaleToCecVolume(volume, maxVolume); |
| HdmiLogger.debug("Reporting volume %d (%d-%d) as CEC volume %d", volume, |
| minVolume, maxVolume, scaledVolume); |
| |
| mService.sendCecCommand( |
| HdmiCecMessageBuilder.buildReportAudioStatus( |
| getDeviceInfo().getLogicalAddress(), source, scaledVolume, mute)); |
| } |
| |
| /** |
| * Method to check if device support System Audio Control. If so, wake up device if necessary. |
| * |
| * <p> then call {@link #setSystemAudioMode(boolean)} to turn on or off System Audio Mode |
| * @param newSystemAudioMode turning feature on or off. True is on. False is off. |
| * @return true or false. |
| * |
| * <p>False when device does not support the feature. Otherwise returns true. |
| */ |
| protected boolean checkSupportAndSetSystemAudioMode(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]", |
| isSystemAudioActivated(), newSystemAudioMode); |
| // Wake up device if System Audio Control is turned on |
| if (newSystemAudioMode) { |
| mService.wakeUp(); |
| } |
| setSystemAudioMode(newSystemAudioMode); |
| return true; |
| } |
| |
| /** |
| * Real work to turn on or off System Audio Mode. |
| * |
| * Use {@link #checkSupportAndSetSystemAudioMode(boolean)} |
| * if trying to turn on or off the feature. |
| */ |
| private void setSystemAudioMode(boolean newSystemAudioMode) { |
| int targetPhysicalAddress = getActiveSource().physicalAddress; |
| int port = mService.pathToPortId(targetPhysicalAddress); |
| if (newSystemAudioMode && port >= 0) { |
| switchToAudioInput(); |
| } |
| // Mute device when feature is turned off and unmute device when feature is turned on. |
| // CEC_SETTING_NAME_SYSTEM_AUDIO_MODE_MUTING is false when device never needs to be muted. |
| boolean systemAudioModeMutingEnabled = mService.getHdmiCecConfig().getIntValue( |
| HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_MODE_MUTING) |
| == HdmiControlManager.SYSTEM_AUDIO_MODE_MUTING_ENABLED; |
| boolean currentMuteStatus = |
| mService.getAudioManager().isStreamMute(AudioManager.STREAM_MUSIC); |
| if (currentMuteStatus == newSystemAudioMode) { |
| if (systemAudioModeMutingEnabled || newSystemAudioMode) { |
| mService.getAudioManager() |
| .adjustStreamVolume( |
| AudioManager.STREAM_MUSIC, |
| newSystemAudioMode |
| ? AudioManager.ADJUST_UNMUTE |
| : AudioManager.ADJUST_MUTE, |
| 0); |
| } |
| } |
| updateAudioManagerForSystemAudio(newSystemAudioMode); |
| synchronized (mLock) { |
| if (isSystemAudioActivated() != newSystemAudioMode) { |
| mService.setSystemAudioActivated(newSystemAudioMode); |
| mService.announceSystemAudioModeChange(newSystemAudioMode); |
| } |
| } |
| // Since ARC is independent from System Audio Mode control, when the TV requests |
| // System Audio Mode off, it does not need to terminate ARC at the same time. |
| // When the current audio device is using ARC as a TV input and disables muting, |
| // it needs to automatically switch to the previous active input source when System |
| // Audio Mode is off even without terminating the ARC. This can stop the current |
| // audio device from playing audio when system audio mode is off. |
| if (mArcIntentUsed |
| && !systemAudioModeMutingEnabled |
| && !newSystemAudioMode |
| && getLocalActivePort() == Constants.CEC_SWITCH_ARC) { |
| routeToInputFromPortId(getRoutingPort()); |
| } |
| // Init arc whenever System Audio Mode is on |
| // Since some TVs don't request ARC on with System Audio Mode on request |
| if (SystemProperties.getBoolean(Constants.PROPERTY_ARC_SUPPORT, true) |
| && isDirectConnectToTv() && mService.isSystemAudioActivated()) { |
| if (!hasAction(ArcInitiationActionFromAvr.class)) { |
| addAndStartAction(new ArcInitiationActionFromAvr(this)); |
| } |
| } |
| } |
| |
| protected void switchToAudioInput() { |
| } |
| |
| 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); |
| } |
| |
| void onSystemAudioControlFeatureSupportChanged(boolean enabled) { |
| setSystemAudioControlFeatureEnabled(enabled); |
| if (enabled) { |
| addAndStartAction(new SystemAudioInitiationActionFromAvr(this)); |
| } |
| } |
| |
| @ServiceThreadOnly |
| void setSystemAudioControlFeatureEnabled(boolean enabled) { |
| assertRunOnServiceThread(); |
| synchronized (mLock) { |
| mSystemAudioControlFeatureEnabled = enabled; |
| } |
| } |
| |
| @ServiceThreadOnly |
| void setRoutingControlFeatureEnabled(boolean enabled) { |
| assertRunOnServiceThread(); |
| synchronized (mLock) { |
| mRoutingControlFeatureEnabled = enabled; |
| } |
| } |
| |
| @ServiceThreadOnly |
| void doManualPortSwitching(int portId, IHdmiControlCallback callback) { |
| assertRunOnServiceThread(); |
| if (!mService.isValidPortId(portId)) { |
| invokeCallback(callback, HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE); |
| return; |
| } |
| if (portId == getLocalActivePort()) { |
| invokeCallback(callback, HdmiControlManager.RESULT_SUCCESS); |
| return; |
| } |
| if (!mService.isCecControlEnabled()) { |
| setRoutingPort(portId); |
| setLocalActivePort(portId); |
| invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE); |
| return; |
| } |
| int oldPath = getRoutingPort() != Constants.CEC_SWITCH_HOME |
| ? mService.portIdToPath(getRoutingPort()) |
| : getDeviceInfo().getPhysicalAddress(); |
| int newPath = mService.portIdToPath(portId); |
| if (oldPath == newPath) { |
| return; |
| } |
| setRoutingPort(portId); |
| setLocalActivePort(portId); |
| HdmiCecMessage routingChange = |
| HdmiCecMessageBuilder.buildRoutingChange( |
| getDeviceInfo().getLogicalAddress(), oldPath, newPath); |
| mService.sendCecCommand(routingChange); |
| } |
| |
| boolean isSystemAudioControlFeatureEnabled() { |
| synchronized (mLock) { |
| return mSystemAudioControlFeatureEnabled; |
| } |
| } |
| |
| protected boolean isSystemAudioActivated() { |
| return mService.isSystemAudioActivated(); |
| } |
| |
| protected void terminateSystemAudioMode() { |
| // remove pending initiation actions |
| removeAction(SystemAudioInitiationActionFromAvr.class); |
| if (!isSystemAudioActivated()) { |
| return; |
| } |
| |
| if (checkSupportAndSetSystemAudioMode(false)) { |
| // send <Set System Audio Mode> [“Off”] |
| mService.sendCecCommand( |
| HdmiCecMessageBuilder.buildSetSystemAudioMode( |
| getDeviceInfo().getLogicalAddress(), Constants.ADDR_BROADCAST, false)); |
| } |
| } |
| |
| private void terminateAudioReturnChannel() { |
| // remove pending initiation actions |
| removeAction(ArcInitiationActionFromAvr.class); |
| if (!isArcEnabled() |
| || !mService.readBooleanSystemProperty(Constants.PROPERTY_ARC_SUPPORT, true)) { |
| return; |
| } |
| addAndStartAction(new ArcTerminationActionFromAvr(this)); |
| } |
| |
| /** 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 == null) { |
| addAndStartAction(new DetectTvSystemAudioModeSupportAction(this, callback)); |
| } else { |
| callback.onResult(mTvSystemAudioModeSupport); |
| } |
| } |
| |
| /** |
| * Handler of System Audio Mode Request on from non TV device |
| */ |
| @Constants.HandleMessageResult |
| int handleSystemAudioModeOnFromNonTvDevice(HdmiCecMessage message) { |
| if (!isSystemAudioControlFeatureEnabled()) { |
| HdmiLogger.debug( |
| "Cannot turn on" + "system audio mode " |
| + "because the System Audio Control feature is disabled."); |
| return Constants.ABORT_REFUSED; |
| } |
| // Wake up device |
| mService.wakeUp(); |
| // If Audio device is the active source or is on the active path, |
| // enable system audio mode without querying TV's support on sam. |
| // This is per HDMI spec 1.4b CEC 13.15.4.2. |
| if (mService.pathToPortId(getActiveSource().physicalAddress) |
| != Constants.INVALID_PORT_ID) { |
| setSystemAudioMode(true); |
| mService.sendCecCommand( |
| HdmiCecMessageBuilder.buildSetSystemAudioMode( |
| getDeviceInfo().getLogicalAddress(), Constants.ADDR_BROADCAST, true)); |
| return Constants.HANDLED; |
| } |
| // Check if TV supports System Audio Control. |
| // Handle broadcasting setSystemAudioMode on or aborting message on callback. |
| queryTvSystemAudioModeSupport(new TvSystemAudioModeSupportedCallback() { |
| public void onResult(boolean supported) { |
| if (supported) { |
| setSystemAudioMode(true); |
| mService.sendCecCommand( |
| HdmiCecMessageBuilder.buildSetSystemAudioMode( |
| getDeviceInfo().getLogicalAddress(), |
| Constants.ADDR_BROADCAST, |
| true)); |
| } else { |
| mService.maySendFeatureAbortCommand(message, Constants.ABORT_REFUSED); |
| } |
| } |
| }); |
| return Constants.HANDLED; |
| } |
| |
| void setTvSystemAudioModeSupport(boolean supported) { |
| mTvSystemAudioModeSupport = supported; |
| } |
| |
| @VisibleForTesting |
| protected boolean isArcEnabled() { |
| synchronized (mLock) { |
| return mArcEstablished; |
| } |
| } |
| |
| private void initArcOnFromAvr() { |
| removeAction(ArcTerminationActionFromAvr.class); |
| if (SystemProperties.getBoolean(Constants.PROPERTY_ARC_SUPPORT, true) |
| && isDirectConnectToTv() && !isArcEnabled()) { |
| removeAction(ArcInitiationActionFromAvr.class); |
| addAndStartAction(new ArcInitiationActionFromAvr(this)); |
| } |
| } |
| |
| @Override |
| protected void switchInputOnReceivingNewActivePath(int physicalAddress) { |
| int port = mService.pathToPortId(physicalAddress); |
| if (isSystemAudioActivated() && port < 0) { |
| // If system audio mode is on and the new active source is not under the current device, |
| // Will switch to ARC input. |
| routeToInputFromPortId(Constants.CEC_SWITCH_ARC); |
| } else if (mIsSwitchDevice && port >= 0) { |
| // If current device is a switch and the new active source is under it, |
| // will switch to the corresponding active path. |
| routeToInputFromPortId(port); |
| } |
| } |
| |
| protected void routeToInputFromPortId(int portId) { |
| if (!isRoutingControlFeatureEnabled()) { |
| HdmiLogger.debug("Routing Control Feature is not enabled."); |
| return; |
| } |
| if (mArcIntentUsed) { |
| routeToTvInputFromPortId(portId); |
| } else { |
| // TODO(): implement input switching for devices not using TvInput. |
| } |
| } |
| |
| protected void routeToTvInputFromPortId(int portId) { |
| if (portId < 0 || portId >= Constants.CEC_SWITCH_PORT_MAX) { |
| HdmiLogger.debug("Invalid port number for Tv Input switching."); |
| return; |
| } |
| // Wake up if the current device if ready to route. |
| mService.wakeUp(); |
| if ((getLocalActivePort() == portId) && (portId != Constants.CEC_SWITCH_ARC)) { |
| HdmiLogger.debug("Not switching to the same port " + portId + " except for arc"); |
| return; |
| } |
| // Switch to HOME if the current active port is not HOME yet |
| if (portId == Constants.CEC_SWITCH_HOME && mService.isPlaybackDevice()) { |
| switchToHomeTvInput(); |
| } else if (portId == Constants.CEC_SWITCH_ARC) { |
| switchToTvInput(HdmiProperties.arc_port().orElse("0")); |
| setLocalActivePort(portId); |
| return; |
| } else { |
| String uri = mPortIdToTvInputs.get(portId); |
| if (uri != null) { |
| switchToTvInput(uri); |
| } else { |
| HdmiLogger.debug("Port number does not match any Tv Input."); |
| return; |
| } |
| } |
| |
| setLocalActivePort(portId); |
| setRoutingPort(portId); |
| } |
| |
| // For device to switch to specific TvInput with corresponding URI. |
| private void switchToTvInput(String uri) { |
| try { |
| mService.getContext().startActivity(new Intent(Intent.ACTION_VIEW, |
| TvContract.buildChannelUriForPassthroughInput(uri)) |
| .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); |
| } catch (ActivityNotFoundException e) { |
| Slog.e(TAG, "Can't find activity to switch to " + uri, e); |
| } |
| } |
| |
| // For device using TvInput to switch to Home. |
| private void switchToHomeTvInput() { |
| try { |
| Intent activityIntent = new Intent(Intent.ACTION_MAIN) |
| .addCategory(Intent.CATEGORY_HOME) |
| .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP |
| | Intent.FLAG_ACTIVITY_SINGLE_TOP |
| | Intent.FLAG_ACTIVITY_NEW_TASK |
| | Intent.FLAG_ACTIVITY_NO_ANIMATION); |
| mService.getContext().startActivity(activityIntent); |
| } catch (ActivityNotFoundException e) { |
| Slog.e(TAG, "Can't find activity to switch to HOME", e); |
| } |
| } |
| |
| @Override |
| protected void handleRoutingChangeAndInformation(int physicalAddress, HdmiCecMessage message) { |
| int port = mService.pathToPortId(physicalAddress); |
| // Routing change or information sent from switches under the current device can be ignored. |
| if (port > 0) { |
| return; |
| } |
| // When other switches route to some other devices not under the current device, |
| // check system audio mode status and do ARC switch if needed. |
| if (port < 0 && isSystemAudioActivated()) { |
| handleRoutingChangeAndInformationForSystemAudio(); |
| return; |
| } |
| // When other switches route to the current device |
| // and the current device is also a switch. |
| if (port == 0) { |
| handleRoutingChangeAndInformationForSwitch(message); |
| } |
| } |
| |
| // Handle the system audio(ARC) part of the logic on receiving routing change or information. |
| private void handleRoutingChangeAndInformationForSystemAudio() { |
| routeToInputFromPortId(Constants.CEC_SWITCH_ARC); |
| } |
| |
| // Handle the routing control part of the logic on receiving routing change or information. |
| private void handleRoutingChangeAndInformationForSwitch(HdmiCecMessage message) { |
| if (getRoutingPort() == Constants.CEC_SWITCH_HOME && mService.isPlaybackDevice()) { |
| routeToInputFromPortId(Constants.CEC_SWITCH_HOME); |
| mService.setAndBroadcastActiveSourceFromOneDeviceType( |
| message.getSource(), mService.getPhysicalAddress(), |
| "HdmiCecLocalDeviceAudioSystem#handleRoutingChangeAndInformationForSwitch()"); |
| return; |
| } |
| |
| int routingInformationPath = mService.portIdToPath(getRoutingPort()); |
| // If current device is already the leaf of the whole HDMI system, will do nothing. |
| if (routingInformationPath == mService.getPhysicalAddress()) { |
| HdmiLogger.debug("Current device can't assign valid physical address" |
| + "to devices under it any more. " |
| + "It's physical address is " |
| + routingInformationPath); |
| return; |
| } |
| // Otherwise will switch to the current active port and broadcast routing information. |
| mService.sendCecCommand( |
| HdmiCecMessageBuilder.buildRoutingInformation( |
| getDeviceInfo().getLogicalAddress(), routingInformationPath)); |
| routeToInputFromPortId(getRoutingPort()); |
| } |
| |
| @ServiceThreadOnly |
| private void launchDeviceDiscovery() { |
| assertRunOnServiceThread(); |
| if (mService.isDeviceDiscoveryHandledByPlayback()) { |
| return; |
| } |
| if (hasAction(DeviceDiscoveryAction.class)) { |
| Slog.i(TAG, "Device Discovery Action is in progress. Restarting."); |
| removeAction(DeviceDiscoveryAction.class); |
| } |
| DeviceDiscoveryAction action = new DeviceDiscoveryAction(this, |
| new DeviceDiscoveryCallback() { |
| @Override |
| public void onDeviceDiscoveryDone(List<HdmiDeviceInfo> deviceInfos) { |
| for (HdmiDeviceInfo info : deviceInfos) { |
| mService.getHdmiCecNetwork().addCecDevice(info); |
| } |
| } |
| }); |
| addAndStartAction(action); |
| } |
| |
| @Override |
| protected void dump(IndentingPrintWriter pw) { |
| pw.println("HdmiCecLocalDeviceAudioSystem:"); |
| pw.increaseIndent(); |
| pw.println("isRoutingFeatureEnabled " + isRoutingControlFeatureEnabled()); |
| pw.println("mSystemAudioControlFeatureEnabled: " + mSystemAudioControlFeatureEnabled); |
| pw.println("mTvSystemAudioModeSupport: " + mTvSystemAudioModeSupport); |
| pw.println("mArcEstablished: " + mArcEstablished); |
| pw.println("mArcIntentUsed: " + mArcIntentUsed); |
| pw.println("mRoutingPort: " + getRoutingPort()); |
| pw.println("mLocalActivePort: " + getLocalActivePort()); |
| HdmiUtils.dumpMap(pw, "mPortIdToTvInputs:", mPortIdToTvInputs); |
| HdmiUtils.dumpMap(pw, "mTvInputsToDeviceInfo:", mTvInputsToDeviceInfo); |
| pw.decreaseIndent(); |
| super.dump(pw); |
| } |
| } |