diff --git a/services/core/java/com/android/server/hdmi/DeviceSelectAction.java b/services/core/java/com/android/server/hdmi/DeviceSelectAction.java index fd3341ac4409e..b97350dea9faa 100644 --- a/services/core/java/com/android/server/hdmi/DeviceSelectAction.java +++ b/services/core/java/com/android/server/hdmi/DeviceSelectAction.java @@ -164,8 +164,10 @@ final class DeviceSelectAction extends FeatureAction { } private void turnOnDevice() { - sendRemoteKeyCommand(HdmiConstants.UI_COMMAND_POWER); - sendRemoteKeyCommand(HdmiConstants.UI_COMMAND_POWER_ON_FUNCTION); + sendUserControlPressedAndReleased(mTarget.getLogicalAddress(), + HdmiConstants.UI_COMMAND_POWER); + sendUserControlPressedAndReleased(mTarget.getLogicalAddress(), + HdmiConstants.UI_COMMAND_POWER_ON_FUNCTION); mState = STATE_WAIT_FOR_DEVICE_POWER_ON; addTimer(mState, TIMEOUT_POWER_ON_MS); } @@ -177,13 +179,6 @@ final class DeviceSelectAction extends FeatureAction { addTimer(mState, TIMEOUT_ACTIVE_SOURCE_MS); } - private void sendRemoteKeyCommand(int keyCode) { - sendCommand(HdmiCecMessageBuilder.buildUserControlPressed(getSourceAddress(), - mTarget.getLogicalAddress(), keyCode)); - sendCommand(HdmiCecMessageBuilder.buildUserControlReleased(getSourceAddress(), - mTarget.getLogicalAddress())); - } - @Override public void handleTimerEvent(int timeoutState) { if (mState != timeoutState) { diff --git a/services/core/java/com/android/server/hdmi/FeatureAction.java b/services/core/java/com/android/server/hdmi/FeatureAction.java index 0ec17f6bc1020..cf28f05c8bad9 100644 --- a/services/core/java/com/android/server/hdmi/FeatureAction.java +++ b/services/core/java/com/android/server/hdmi/FeatureAction.java @@ -248,4 +248,11 @@ abstract class FeatureAction { protected final int getSourcePath() { return mSource.getDeviceInfo().getPhysicalAddress(); } + + protected void sendUserControlPressedAndReleased(int targetAddress, int uiCommand) { + sendCommand(HdmiCecMessageBuilder.buildUserControlPressed( + getSourceAddress(), targetAddress, uiCommand)); + sendCommand(HdmiCecMessageBuilder.buildUserControlReleased( + getSourceAddress(), targetAddress)); + } } diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java index f72d3f026825c..bf7e57b0f2364 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java @@ -151,6 +151,8 @@ abstract class HdmiCecLocalDevice { return handleSetSystemAudioMode(message); case HdmiCec.MESSAGE_SYSTEM_AUDIO_MODE_STATUS: return handleSystemAudioModeStatus(message); + case HdmiCec.MESSAGE_REPORT_AUDIO_STATUS: + return handleReportAudioStatus(message); default: return false; } @@ -263,6 +265,10 @@ abstract class HdmiCecLocalDevice { return false; } + protected boolean handleReportAudioStatus(HdmiCecMessage message) { + return false; + } + @ServiceThreadOnly final void handleAddressAllocated(int logicalAddress) { assertRunOnServiceThread(); diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java index 2431ec4d01e17..718072a7eb0da 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java @@ -56,6 +56,12 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { @GuardedBy("mLock") private int mPrevPortId; + @GuardedBy("mLock") + private int mSystemAudioVolume = HdmiConstants.UNKNOWN_VOLUME; + + @GuardedBy("mLock") + private boolean mSystemAudioMute = false; + // Copy of mDeviceInfos to guarantee thread-safety. @GuardedBy("mLock") private List mSafeAllDeviceInfos = Collections.emptyList(); @@ -353,6 +359,22 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { return true; } + @Override + @ServiceThreadOnly + protected boolean handleReportAudioStatus(HdmiCecMessage message) { + assertRunOnServiceThread(); + + byte params[] = message.getParams(); + if (params.length < 1) { + Slog.w(TAG, "Invalide message:" + message); + return true; + } + int mute = params[0] & 0x80; + int volume = params[0] & 0x7F; + setAudioStatus(mute == 0x80, volume); + return true; + } + @ServiceThreadOnly private void launchDeviceDiscovery() { assertRunOnServiceThread(); @@ -458,9 +480,64 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { } } - @ServiceThreadOnly void setAudioStatus(boolean mute, int volume) { - mService.setAudioStatus(mute, volume); + synchronized (mLock) { + mSystemAudioMute = mute; + mSystemAudioVolume = volume; + // TODO: pass volume to service (audio service) after scale it to local volume level. + mService.setAudioStatus(mute, volume); + } + } + + @ServiceThreadOnly + void changeVolume(int curVolume, int delta, int maxVolume) { + assertRunOnServiceThread(); + if (delta == 0 || !isSystemAudioOn()) { + return; + } + + int targetVolume = curVolume + delta; + int cecVolume = VolumeControlAction.scaleToCecVolume(targetVolume, maxVolume); + synchronized (mLock) { + // If new volume is the same as current system audio volume, just ignore it. + // Note that UNKNOWN_VOLUME is not in range of cec volume scale. + if (cecVolume == mSystemAudioVolume) { + // Update tv volume with system volume value. + mService.setAudioStatus(false, + VolumeControlAction.scaleToCustomVolume(mSystemAudioVolume, maxVolume)); + return; + } + } + + // Remove existing volume action. + removeAction(VolumeControlAction.class); + + HdmiCecDeviceInfo avr = getAvrDeviceInfo(); + addAndStartAction(VolumeControlAction.ofVolumeChange(this, avr.getLogicalAddress(), + cecVolume, delta > 0)); + } + + @ServiceThreadOnly + void changeMute(boolean mute) { + assertRunOnServiceThread(); + if (!isSystemAudioOn()) { + return; + } + + // Remove existing volume action. + removeAction(VolumeControlAction.class); + HdmiCecDeviceInfo avr = getAvrDeviceInfo(); + addAndStartAction(VolumeControlAction.ofMute(this, avr.getLogicalAddress(), mute)); + } + + private boolean isSystemAudioOn() { + if (getAvrDeviceInfo() == null) { + return false; + } + + synchronized (mLock) { + return mSystemAudioMode; + } } @Override diff --git a/services/core/java/com/android/server/hdmi/HdmiConstants.java b/services/core/java/com/android/server/hdmi/HdmiConstants.java index 5294506f2125f..ab5b8d84487b5 100644 --- a/services/core/java/com/android/server/hdmi/HdmiConstants.java +++ b/services/core/java/com/android/server/hdmi/HdmiConstants.java @@ -97,5 +97,12 @@ final class HdmiConstants { static final int UNKNOWN_VOLUME = -1; + // IRT(Initiator Repetition Time) in millisecond as recommended in the standard. + // Outgoing UCP commands, when in 'Press and Hold' mode, should be this much apart + // from the adjacent one so as not to place unnecessarily heavy load on the CEC line. + // TODO: This value might need tweaking per product basis. Consider putting it + // in config.xml to allow customization. + static final int IRT_MS = 300; + private HdmiConstants() { /* cannot be instantiated */ } } diff --git a/services/core/java/com/android/server/hdmi/SendKeyAction.java b/services/core/java/com/android/server/hdmi/SendKeyAction.java index c3078a2182cd2..5d81251be1bc1 100644 --- a/services/core/java/com/android/server/hdmi/SendKeyAction.java +++ b/services/core/java/com/android/server/hdmi/SendKeyAction.java @@ -15,6 +15,8 @@ */ package com.android.server.hdmi; +import static com.android.server.hdmi.HdmiConstants.IRT_MS; + import android.hardware.hdmi.HdmiCecMessage; import android.util.Slog; import android.view.KeyEvent; @@ -38,13 +40,6 @@ final class SendKeyAction extends FeatureAction { // persists throughout the process till it is set back to {@code STATE_NONE} at the end. private static final int STATE_PROCESSING_KEYCODE = 1; - // IRT(Initiator Repetition Time) in millisecond as recommended in the standard. - // Outgoing UCP commands, when in 'Press and Hold' mode, should be this much apart - // from the adjacent one so as not to place unnecessarily heavy load on the CEC line. - // TODO: This value might need tweaking per product basis. Consider putting it - // in config.xml to allow customization. - private static final int IRT_MS = 450; - // Logical address of the device to which the UCP/UCP commands are sent. private final int mTargetAddress; @@ -77,7 +72,6 @@ final class SendKeyAction extends FeatureAction { * * @param keyCode key code of {@link KeyEvent} object * @param isPressed true if the key event is of {@link KeyEvent#ACTION_DOWN} - * @param param additional parameter that comes with the key event */ void processKeyEvent(int keyCode, boolean isPressed) { if (mState != STATE_PROCESSING_KEYCODE) { diff --git a/services/core/java/com/android/server/hdmi/SystemAudioStatusAction.java b/services/core/java/com/android/server/hdmi/SystemAudioStatusAction.java index 89206a71abb0e..ecb158b56e155 100644 --- a/services/core/java/com/android/server/hdmi/SystemAudioStatusAction.java +++ b/services/core/java/com/android/server/hdmi/SystemAudioStatusAction.java @@ -72,19 +72,12 @@ final class SystemAudioStatusAction extends FeatureAction { int uiCommand = tv().getSystemAudioMode() ? HdmiConstants.UI_COMMAND_RESTORE_VOLUME_FUNCTION // SystemAudioMode: ON : HdmiConstants.UI_COMMAND_MUTE_FUNCTION; // SystemAudioMode: OFF - sendUserControlPressedAndReleased(uiCommand); + sendUserControlPressedAndReleased(mAvrAddress, uiCommand); // Still return SUCCESS to callback. finishWithCallback(HdmiCec.RESULT_SUCCESS); } - private void sendUserControlPressedAndReleased(int uiCommand) { - sendCommand(HdmiCecMessageBuilder.buildUserControlPressed( - getSourceAddress(), mAvrAddress, uiCommand)); - sendCommand(HdmiCecMessageBuilder.buildUserControlReleased( - getSourceAddress(), mAvrAddress)); - } - @Override boolean processCommand(HdmiCecMessage cmd) { if (mState != STATE_WAIT_FOR_REPORT_AUDIO_STATUS) { @@ -109,7 +102,7 @@ final class SystemAudioStatusAction extends FeatureAction { if ((tv().getSystemAudioMode() && mute) || (!tv().getSystemAudioMode() && !mute)) { // Toggle AVR's mute status to match with the system audio status. - sendUserControlPressedAndReleased(HdmiConstants.UI_COMMAND_MUTE); + sendUserControlPressedAndReleased(mAvrAddress, HdmiConstants.UI_COMMAND_MUTE); } finishWithCallback(HdmiCec.RESULT_SUCCESS); } else { diff --git a/services/core/java/com/android/server/hdmi/VolumeControlAction.java b/services/core/java/com/android/server/hdmi/VolumeControlAction.java new file mode 100644 index 0000000000000..07c72f7e943e3 --- /dev/null +++ b/services/core/java/com/android/server/hdmi/VolumeControlAction.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2014 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.HdmiConstants.IRT_MS; + +import android.hardware.hdmi.HdmiCec; +import android.hardware.hdmi.HdmiCecMessage; +import android.util.Slog; + +import com.android.internal.util.Preconditions; + +/** + * Feature action that transmits volume change to Audio Receiver. + *

+ * This action is created when a user pressed volume up/down. However, Since Android only provides a + * listener for delta of some volume change, we will set a target volume, and check reported volume + * from Audio Receiver(AVR). If TV receives no <Report Audio Status> from AVR, this action + * will be finished in {@link #IRT_MS} * {@link #VOLUME_CHANGE_TIMEOUT_MAX_COUNT} (ms). + */ +final class VolumeControlAction extends FeatureAction { + private static final String TAG = "VolumeControlAction"; + + private static final int VOLUME_MUTE = 101; + private static final int VOLUME_RESTORE = 102; + private static final int MAX_VOLUME = 100; + private static final int MIN_VOLUME = 0; + + // State where to wait for + private static final int STATE_WAIT_FOR_REPORT_VOLUME_STATUS = 1; + + // Maximum count of time out used to finish volume action. + private static final int VOLUME_CHANGE_TIMEOUT_MAX_COUNT = 2; + + private final int mAvrAddress; + private final int mTargetVolume; + private final boolean mIsVolumeUp; + private int mTimeoutCount; + + /** + * Create a {@link VolumeControlAction} for mute/restore change + * + * @param source source device sending volume change + * @param avrAddress address of audio receiver + * @param mute whether to mute sound or not. {@code true} for mute on; {@code false} for mute + * off, i.e restore volume + * @return newly created {@link VolumeControlAction} + */ + public static VolumeControlAction ofMute(HdmiCecLocalDevice source, int avrAddress, + boolean mute) { + return new VolumeControlAction(source, avrAddress, mute ? VOLUME_MUTE : VOLUME_RESTORE, + false); + } + + /** + * Create a {@link VolumeControlAction} for volume up/down change + * + * @param source source device sending volume change + * @param avrAddress address of audio receiver + * @param targetVolume target volume to be set to AVR. It should be in range of [0-100] + * @param isVolumeUp whether to volume up or not. {@code true} for volume up; {@code false} for + * volume down + * @return newly created {@link VolumeControlAction} + */ + public static VolumeControlAction ofVolumeChange(HdmiCecLocalDevice source, int avrAddress, + int targetVolume, boolean isVolumeUp) { + Preconditions.checkArgumentInRange(targetVolume, MIN_VOLUME, MAX_VOLUME, "volume"); + return new VolumeControlAction(source, avrAddress, targetVolume, isVolumeUp); + } + + /** + * Scale a custom volume value to cec volume scale. + * + * @param volume volume value in custom scale + * @param scale scale of volume (max volume) + * @return a volume scaled to cec volume range + */ + public static int scaleToCecVolume(int volume, int scale) { + return (volume * MAX_VOLUME) / scale; + } + + /** + * Scale a cec volume which is in range of 0 to 100 to custom volume level. + * + * @param cecVolume volume value in cec volume scale. It should be in a range of [0-100] + * @param scale scale of custom volume (max volume) + * @return a volume value scaled to custom volume range + */ + public static int scaleToCustomVolume(int cecVolume, int scale) { + return (cecVolume * scale) / MAX_VOLUME; + } + + private VolumeControlAction(HdmiCecLocalDevice source, int avrAddress, int targetVolume, + boolean isVolumeUp) { + super(source); + + mAvrAddress = avrAddress; + mTargetVolume = targetVolume; + mIsVolumeUp = isVolumeUp; + } + + @Override + boolean start() { + if (isForMute()) { + sendMuteChange(mTargetVolume == VOLUME_MUTE); + finish(); + return true; + } + + startVolumeChange(); + return true; + } + + + private boolean isForMute() { + return mTargetVolume == VOLUME_MUTE || mTargetVolume == VOLUME_RESTORE; + } + + + private void startVolumeChange() { + mTimeoutCount = 0; + sendVolumeChange(mIsVolumeUp); + mState = STATE_WAIT_FOR_REPORT_VOLUME_STATUS; + addTimer(mState, IRT_MS); + } + + private void sendVolumeChange(boolean up) { + sendCommand(HdmiCecMessageBuilder.buildUserControlPressed(getSourceAddress(), mAvrAddress, + up ? HdmiCecKeycode.CEC_KEYCODE_VOLUME_UP + : HdmiCecKeycode.CEC_KEYCODE_VOLUME_DOWN)); + } + + private void sendMuteChange(boolean mute) { + sendUserControlPressedAndReleased(mAvrAddress, + mute ? HdmiConstants.UI_COMMAND_MUTE_FUNCTION : + HdmiConstants.UI_COMMAND_RESTORE_VOLUME_FUNCTION); + } + + @Override + boolean processCommand(HdmiCecMessage cmd) { + if (mState != STATE_WAIT_FOR_REPORT_VOLUME_STATUS) { + return false; + } + + switch (cmd.getOpcode()) { + case HdmiCec.MESSAGE_REPORT_AUDIO_STATUS: + handleReportAudioStatus(cmd); + return true; + case HdmiCec.MESSAGE_FEATURE_ABORT: + // TODO: handle feature abort. + finish(); + return true; + default: + return false; + } + } + + private void handleReportAudioStatus(HdmiCecMessage cmd) { + byte[] params = cmd.getParams(); + if (params.length != 1) { + Slog.e(TAG, "Invalid message:" + cmd); + return; + } + + int volume = params[0] & 0x7F; + // Update volume with new value. + // Note that it will affect system volume change. + tv().setAudioStatus(false, volume); + if (mIsVolumeUp) { + if (mTargetVolume <= volume) { + finishWithVolumeChangeRelease(); + return; + } + } else { + if (mTargetVolume >= volume) { + finishWithVolumeChangeRelease(); + return; + } + } + + // Clear action status and send another volume change command. + clear(); + startVolumeChange(); + } + + private void finishWithVolumeChangeRelease() { + sendCommand(HdmiCecMessageBuilder.buildUserControlReleased( + getSourceAddress(), mAvrAddress)); + finish(); + } + + @Override + void handleTimerEvent(int state) { + if (mState != STATE_WAIT_FOR_REPORT_VOLUME_STATUS) { + return; + } + + // If no report volume action after IRT * VOLUME_CHANGE_TIMEOUT_MAX_COUNT just stop volume + // action. + if (++mTimeoutCount == VOLUME_CHANGE_TIMEOUT_MAX_COUNT) { + finishWithVolumeChangeRelease(); + return; + } + + sendVolumeChange(mIsVolumeUp); + addTimer(mState, IRT_MS); + } +}