diff options
13 files changed, 646 insertions, 48 deletions
diff --git a/core/java/android/hardware/hdmi/HdmiControlManager.java b/core/java/android/hardware/hdmi/HdmiControlManager.java index b520d2c14edc..f5d288e6a233 100644 --- a/core/java/android/hardware/hdmi/HdmiControlManager.java +++ b/core/java/android/hardware/hdmi/HdmiControlManager.java @@ -16,6 +16,8 @@ package android.hardware.hdmi; +import static com.android.internal.os.RoSystemProperties.PROPERTY_HDMI_IS_DEVICE_HDMI_CEC_SWITCH; + import android.annotation.Nullable; import android.annotation.RequiresFeature; import android.annotation.RequiresPermission; @@ -27,6 +29,7 @@ import android.annotation.SystemService; import android.content.Context; import android.content.pm.PackageManager; import android.os.RemoteException; +import android.os.SystemProperties; import android.util.ArrayMap; import android.util.Log; @@ -264,6 +267,10 @@ public final class HdmiControlManager { private final boolean mHasTvDevice; // True if we have a logical device of type audio system hosted in the system. private final boolean mHasAudioSystemDevice; + // True if we have a logical device of type audio system hosted in the system. + private final boolean mHasSwitchDevice; + // True if it's a switch device. + private final boolean mIsSwitchDevice; /** * {@hide} - hide this constructor because it has a parameter of type IHdmiControlService, @@ -283,6 +290,9 @@ public final class HdmiControlManager { mHasTvDevice = hasDeviceType(types, HdmiDeviceInfo.DEVICE_TV); mHasPlaybackDevice = hasDeviceType(types, HdmiDeviceInfo.DEVICE_PLAYBACK); mHasAudioSystemDevice = hasDeviceType(types, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM); + mHasSwitchDevice = hasDeviceType(types, HdmiDeviceInfo.DEVICE_PURE_CEC_SWITCH); + mIsSwitchDevice = SystemProperties.getBoolean( + PROPERTY_HDMI_IS_DEVICE_HDMI_CEC_SWITCH, false); } private static boolean hasDeviceType(int[] types, int type) { @@ -319,6 +329,9 @@ public final class HdmiControlManager { return mHasPlaybackDevice ? new HdmiPlaybackClient(mService) : null; case HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM: return mHasAudioSystemDevice ? new HdmiAudioSystemClient(mService) : null; + case HdmiDeviceInfo.DEVICE_PURE_CEC_SWITCH: + return (mHasSwitchDevice || mIsSwitchDevice) + ? new HdmiSwitchClient(mService) : null; default: return null; } @@ -373,6 +386,24 @@ public final class HdmiControlManager { } /** + * Gets an object that represents an HDMI-CEC logical device of type switch on the system. + * + * <p>Used to send HDMI control messages to other devices like TV through HDMI bus. It is also + * possible to communicate with other logical devices hosted in the same system if the system is + * configured to host more than one type of HDMI-CEC logical devices. + * + * @return {@link HdmiSwitchClient} instance. {@code null} on failure. + * + * TODO(b/110094868): unhide for Q + * @hide + */ + @Nullable + @SuppressLint("Doclava125") + public HdmiSwitchClient getSwitchClient() { + return (HdmiSwitchClient) getClient(HdmiDeviceInfo.DEVICE_PURE_CEC_SWITCH); + } + + /** * Controls standby mode of the system. It will also try to turn on/off the connected devices if * necessary. * diff --git a/core/java/android/hardware/hdmi/HdmiSwitchClient.java b/core/java/android/hardware/hdmi/HdmiSwitchClient.java new file mode 100644 index 000000000000..1ac29736f964 --- /dev/null +++ b/core/java/android/hardware/hdmi/HdmiSwitchClient.java @@ -0,0 +1,137 @@ +/* + * 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 android.hardware.hdmi; + +import android.annotation.NonNull; +import android.os.RemoteException; +import android.util.Log; + +import java.util.Collections; +import java.util.List; + +/** + * HdmiSwitchClient represents HDMI-CEC logical device of type Switch in the Android system which + * acts as switch. + * + * <p>HdmiSwitchClient has a CEC device type of HdmiDeviceInfo.DEVICE_PURE_CEC_SWITCH, + * but it is used by all Android TV devices that have multiple HDMI inputs, + * even if it is not a "pure" swicth and has another device type like TV or Player. + * + * @hide + * TODO(b/110094868): unhide and add @SystemApi for Q + */ +public class HdmiSwitchClient extends HdmiClient { + + private static final String TAG = "HdmiSwitchClient"; + + /* package */ HdmiSwitchClient(IHdmiControlService service) { + super(service); + } + + private static IHdmiControlCallback getCallbackWrapper(final SelectCallback callback) { + return new IHdmiControlCallback.Stub() { + @Override + public void onComplete(int result) { + callback.onComplete(result); + } + }; + } + + /** @hide */ + // TODO(b/110094868): unhide for Q + @Override + public int getDeviceType() { + return HdmiDeviceInfo.DEVICE_PURE_CEC_SWITCH; + } + + /** + * Selects a CEC logical device to be a new active source. + * + * @param logicalAddress logical address of the device to select + * @param callback callback to get the result with + * @throws {@link IllegalArgumentException} if the {@code callback} is null + * + * @hide + * TODO(b/110094868): unhide and add @SystemApi for Q + */ + public void deviceSelect(int logicalAddress, @NonNull SelectCallback callback) { + if (callback == null) { + throw new IllegalArgumentException("callback must not be null."); + } + try { + mService.deviceSelect(logicalAddress, getCallbackWrapper(callback)); + } catch (RemoteException e) { + Log.e(TAG, "failed to select device: ", e); + } + } + + /** + * Selects a HDMI port to be a new route path. + * + * @param portId HDMI port to select + * @param callback callback to get the result with + * @throws {@link IllegalArgumentException} if the {@code callback} is null + * + * @hide + * TODO(b/110094868): unhide and add @SystemApi for Q + */ + public void portSelect(int portId, @NonNull SelectCallback callback) { + if (callback == null) { + throw new IllegalArgumentException("Callback must not be null"); + } + try { + mService.portSelect(portId, getCallbackWrapper(callback)); + } catch (RemoteException e) { + Log.e(TAG, "failed to select port: ", e); + } + } + + /** + * Returns all the CEC devices connected to the device. + * + * <p>This only applies to device with multiple HDMI inputs + * + * @return list of {@link HdmiDeviceInfo} for connected CEC devices. Empty list is returned if + * there is none. + * + * @hide + * TODO(b/110094868): unhide and add @SystemApi for Q + */ + public List<HdmiDeviceInfo> getDeviceList() { + try { + return mService.getDeviceList(); + } catch (RemoteException e) { + Log.e("TAG", "Failed to call getDeviceList():", e); + return Collections.<HdmiDeviceInfo>emptyList(); + } + } + + /** + * Callback interface used to get the result of {@link #deviceSelect} or {@link #portSelect}. + * + * @hide + * TODO(b/110094868): unhide and add @SystemApi for Q + */ + public interface SelectCallback { + + /** + * Called when the operation is finished. + * + * @param result the result value of {@link #deviceSelect} or {@link #portSelect}. + */ + void onComplete(int result); + } +} diff --git a/core/java/com/android/internal/os/RoSystemProperties.java b/core/java/com/android/internal/os/RoSystemProperties.java index a319d83b077d..b529bbe51e93 100644 --- a/core/java/com/android/internal/os/RoSystemProperties.java +++ b/core/java/com/android/internal/os/RoSystemProperties.java @@ -30,6 +30,7 @@ public class RoSystemProperties { public static final String CONTROL_PRIVAPP_PERMISSIONS = SystemProperties.get("ro.control_privapp_permissions"); + // ------ ro.hdmi.* -------- // /** * Property to indicate if a CEC audio device should forward volume keys when system audio * mode is off. @@ -38,6 +39,14 @@ public class RoSystemProperties { SystemProperties.getBoolean( "ro.hdmi.cec_audio_device_forward_volume_keys_system_audio_mode_off", false); + /** + * Property to indicate if the current device is a cec switch device. + * + * <p> Default is false. + */ + public static final String PROPERTY_HDMI_IS_DEVICE_HDMI_CEC_SWITCH = + "ro.hdmi.property_is_device_hdmi_cec_switch"; + // ------ ro.config.* -------- // public static final boolean CONFIG_AVOID_GFX_ACCEL = SystemProperties.getBoolean("ro.config.avoid_gfx_accel", false); diff --git a/services/core/java/com/android/server/hdmi/Constants.java b/services/core/java/com/android/server/hdmi/Constants.java index 6f5a19612895..ff029c194f12 100644 --- a/services/core/java/com/android/server/hdmi/Constants.java +++ b/services/core/java/com/android/server/hdmi/Constants.java @@ -343,14 +343,6 @@ final class Constants { static final String PROPERTY_HDMI_CEC_NEVER_ASSIGN_LOGICAL_ADDRESSES = "ro.hdmi.property_hdmi_cec_never_assign_logical_addresses"; - /** - * Property to indicate if the current device is a cec switch device. - * - * <p> Default is false. - */ - static final String PROPERTY_HDMI_IS_DEVICE_HDMI_CEC_SWITCH = - "ro.hdmi.property_is_device_hdmi_cec_switch"; - // Set to false to allow playback device to go to suspend mode even // when it's an active source. True by default. static final String PROPERTY_KEEP_AWAKE = "persist.sys.hdmi.keep_awake"; diff --git a/services/core/java/com/android/server/hdmi/DeviceDiscoveryAction.java b/services/core/java/com/android/server/hdmi/DeviceDiscoveryAction.java index b75e75feabb3..ba21b7882afd 100755 --- a/services/core/java/com/android/server/hdmi/DeviceDiscoveryAction.java +++ b/services/core/java/com/android/server/hdmi/DeviceDiscoveryAction.java @@ -28,7 +28,7 @@ import java.util.List; /** * Feature action that handles device discovery sequences. - * Device discovery is launched when TV device is woken from "Standby" state + * Device discovery is launched when device is woken from "Standby" state * or enabled "Control for Hdmi" from disabled state. * * <p>Device discovery goes through the following steps. @@ -89,6 +89,7 @@ final class DeviceDiscoveryAction extends HdmiCecFeatureAction { private final DeviceDiscoveryCallback mCallback; private int mProcessedDeviceCount = 0; private int mTimeoutRetry = 0; + private boolean mIsTvDevice = source().mService.isTvDevice(); /** * Constructor. @@ -266,15 +267,19 @@ final class DeviceDiscoveryAction extends HdmiCecFeatureAction { current.mPortId = getPortId(current.mPhysicalAddress); current.mDeviceType = params[2] & 0xFF; - tv().updateCecSwitchInfo(current.mLogicalAddress, current.mDeviceType, + // TODO(amyjojo): check if non-TV device needs to update cec switch info. + // This is to manager CEC device separately in case they don't have address. + if (mIsTvDevice) { + tv().updateCecSwitchInfo(current.mLogicalAddress, current.mDeviceType, current.mPhysicalAddress); - + } increaseProcessedDeviceCount(); checkAndProceedStage(); } private int getPortId(int physicalAddress) { - return tv().getPortId(physicalAddress); + return mIsTvDevice ? tv().getPortId(physicalAddress) + : source().getPortId(physicalAddress); } private void handleSetOsdName(HdmiCecMessage cmd) { @@ -345,7 +350,9 @@ final class DeviceDiscoveryAction extends HdmiCecFeatureAction { mCallback.onDeviceDiscoveryDone(result); finish(); // Process any commands buffered while device discovery action was in progress. - tv().processAllDelayedMessages(); + if (mIsTvDevice) { + tv().processAllDelayedMessages(); + } } private void checkAndProceedStage() { diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java index 6e1b0181adbf..32dc02613512 100755 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java @@ -916,6 +916,11 @@ abstract class HdmiCecLocalDevice { setActivePath(mService.portIdToPath(portId)); } + // Returns the id of the port that the target device is connected to. + int getPortId(int physicalAddress) { + return mService.pathToPortId(physicalAddress); + } + @ServiceThreadOnly HdmiCecMessageCache getCecMessageCache() { assertRunOnServiceThread(); diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java index 8cfe47f3d75e..0e4e3342451c 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java @@ -23,19 +23,25 @@ import android.annotation.Nullable; import android.content.Intent; 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.AudioManager; import android.media.AudioSystem; import android.media.tv.TvContract; import android.os.SystemProperties; +import android.util.Slog; +import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.server.hdmi.Constants.AudioCodec; +import com.android.server.hdmi.DeviceDiscoveryAction.DeviceDiscoveryCallback; import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly; +import java.io.UnsupportedEncodingException; import java.util.HashMap; +import java.util.List; /** * Represent a logical device of type {@link HdmiDeviceInfo#DEVICE_AUDIO_SYSTEM} residing in Android @@ -71,6 +77,10 @@ public class HdmiCecLocalDeviceAudioSystem extends HdmiCecLocalDeviceSource { // processing. private final HashMap<Integer, String> mTvInputs = new HashMap<>(); + // Map-like container of all cec devices. + // device id is used as key of container. + private final SparseArray<HdmiDeviceInfo> mDeviceInfos = new SparseArray<>(); + protected HdmiCecLocalDeviceAudioSystem(HdmiControlService service) { super(service, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM); mSystemAudioControlFeatureEnabled = true; @@ -86,6 +96,132 @@ public class HdmiCecLocalDeviceAudioSystem extends HdmiCecLocalDeviceSource { "com.droidlogic.tvinput/.services.Hdmi3InputService/HW7"); } + /** + * Called when a device is newly added or a new device is detected or + * an existing device is updated. + * + * @param info device info of a new device. + */ + @ServiceThreadOnly + final void addCecDevice(HdmiDeviceInfo info) { + assertRunOnServiceThread(); + HdmiDeviceInfo old = addDeviceInfo(info); + if (info.getPhysicalAddress() == mService.getPhysicalAddress()) { + // The addition of the device itself should not be notified. + // Note that different logical address could still be the same local device. + return; + } + if (old == null) { + invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_ADD_DEVICE); + } else if (!old.equals(info)) { + invokeDeviceEventListener(old, HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE); + invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_ADD_DEVICE); + } + } + + /** + * Called when a device is removed or removal of device is detected. + * + * @param address a logical address of a device to be removed + */ + @ServiceThreadOnly + final void removeCecDevice(int address) { + assertRunOnServiceThread(); + HdmiDeviceInfo info = removeDeviceInfo(HdmiDeviceInfo.idForCecDevice(address)); + + mCecMessageCache.flushMessagesFrom(address); + invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE); + } + + /** + * Called when a device is updated. + * + * @param info device info of the updating device. + */ + @ServiceThreadOnly + final void updateCecDevice(HdmiDeviceInfo info) { + assertRunOnServiceThread(); + HdmiDeviceInfo old = addDeviceInfo(info); + + if (old == null) { + invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_ADD_DEVICE); + } else if (!old.equals(info)) { + invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_UPDATE_DEVICE); + } + } + + /** + * Add a new {@link HdmiDeviceInfo}. It returns old device info which has the same + * logical address as new device info's. + * + * @param deviceInfo a new {@link HdmiDeviceInfo} to be added. + * @return {@code null} if it is new device. Otherwise, returns old {@HdmiDeviceInfo} + * that has the same logical address as new one has. + */ + @ServiceThreadOnly + @VisibleForTesting + protected HdmiDeviceInfo addDeviceInfo(HdmiDeviceInfo deviceInfo) { + assertRunOnServiceThread(); + HdmiDeviceInfo oldDeviceInfo = getCecDeviceInfo(deviceInfo.getLogicalAddress()); + if (oldDeviceInfo != null) { + removeDeviceInfo(deviceInfo.getId()); + } + mDeviceInfos.append(deviceInfo.getId(), deviceInfo); + return oldDeviceInfo; + } + + /** + * Remove a device info corresponding to the given {@code logicalAddress}. + * It returns removed {@link HdmiDeviceInfo} if exists. + * + * @param id id of device to be removed + * @return removed {@link HdmiDeviceInfo} it exists. Otherwise, returns {@code null} + */ + @ServiceThreadOnly + private HdmiDeviceInfo removeDeviceInfo(int id) { + assertRunOnServiceThread(); + HdmiDeviceInfo deviceInfo = mDeviceInfos.get(id); + if (deviceInfo != null) { + mDeviceInfos.remove(id); + } + return deviceInfo; + } + + /** + * Return a {@link HdmiDeviceInfo} corresponding to the given {@code logicalAddress}. + * + * @param logicalAddress logical address of the device to be retrieved + * @return {@link HdmiDeviceInfo} matched with the given {@code logicalAddress}. + * Returns null if no logical address matched + */ + @ServiceThreadOnly + HdmiDeviceInfo getCecDeviceInfo(int logicalAddress) { + assertRunOnServiceThread(); + return mDeviceInfos.get(HdmiDeviceInfo.idForCecDevice(logicalAddress)); + } + + private void invokeDeviceEventListener(HdmiDeviceInfo info, int status) { + mService.invokeDeviceEventListeners(info, status); + } + + @Override + @ServiceThreadOnly + void onHotplug(int portId, boolean connected) { + assertRunOnServiceThread(); + if (connected) { + mService.wakeUp(); + } + if (mService.getPortInfo(portId).getType() == HdmiPortInfo.PORT_OUTPUT) { + mCecMessageCache.flushAll(); + } else { + if (connected) { + launchDeviceDiscovery(); + } else { + // TODO(amyjojo): remove device from mDeviceInfo + } + } + } + @Override @ServiceThreadOnly protected void onStandby(boolean initiatedByCec, int standbyAction) { @@ -116,6 +252,8 @@ public class HdmiCecLocalDeviceAudioSystem extends HdmiCecLocalDeviceSource { boolean lastSystemAudioControlStatus = SystemProperties.getBoolean(Constants.PROPERTY_LAST_SYSTEM_AUDIO_CONTROL, true); systemAudioControlOnPowerOn(systemAudioControlOnPowerOnProp, lastSystemAudioControlStatus); + clearDeviceInfoList(); + launchDeviceDiscovery(); startQueuedActions(); } @@ -152,6 +290,78 @@ public class HdmiCecLocalDeviceAudioSystem extends HdmiCecLocalDeviceSource { @Override @ServiceThreadOnly + protected boolean handleReportPhysicalAddress(HdmiCecMessage message) { + assertRunOnServiceThread(); + int path = HdmiUtils.twoBytesToInt(message.getParams()); + int address = message.getSource(); + int type = message.getParams()[2]; + + // Ignore if [Device Discovery Action] is going on. + if (hasAction(DeviceDiscoveryAction.class)) { + Slog.i(TAG, "Ignored while Device Discovery Action is in progress: " + message); + return true; + } + + // Update the device info with TIF, note that the same device info could have added in + // device discovery and we do not want to override it with default OSD name. Therefore we + // need the following check to skip redundant device info updating. + HdmiDeviceInfo oldDevice = getCecDeviceInfo(address); + if (oldDevice == null || oldDevice.getPhysicalAddress() != path) { + addCecDevice(new HdmiDeviceInfo( + address, path, mService.pathToPortId(path), type, + Constants.UNKNOWN_VENDOR_ID, HdmiUtils.getDefaultDeviceName(address))); + // if we are adding a new device info, send out a give osd name command + // to update the name of the device in TIF + mService.sendCecCommand( + HdmiCecMessageBuilder.buildGiveOsdNameCommand(mAddress, address)); + return true; + } + + Slog.w(TAG, "Device info exists. Not updating on Physical Address."); + return true; + } + + @Override + protected boolean handleReportPowerStatus(HdmiCecMessage command) { + int newStatus = command.getParams()[0] & 0xFF; + updateDevicePowerStatus(command.getSource(), newStatus); + return true; + } + + @Override + @ServiceThreadOnly + protected boolean handleSetOsdName(HdmiCecMessage message) { + int source = message.getSource(); + String osdName; + HdmiDeviceInfo deviceInfo = getCecDeviceInfo(source); + // If the device is not in device list, ignore it. + if (deviceInfo == null) { + Slog.i(TAG, "No source device info for <Set Osd Name>." + message); + return true; + } + try { + osdName = new String(message.getParams(), "US-ASCII"); + } catch (UnsupportedEncodingException e) { + Slog.e(TAG, "Invalid <Set Osd Name> request:" + message, e); + return true; + } + + if (deviceInfo.getDisplayName().equals(osdName)) { + Slog.d(TAG, "Ignore incoming <Set Osd Name> having same osd name:" + message); + return true; + } + + Slog.d(TAG, "Updating device OSD name from " + + deviceInfo.getDisplayName() + + " to " + osdName); + updateCecDevice(new HdmiDeviceInfo(deviceInfo.getLogicalAddress(), + deviceInfo.getPhysicalAddress(), deviceInfo.getPortId(), + deviceInfo.getDeviceType(), deviceInfo.getVendorId(), osdName)); + return true; + } + + @Override + @ServiceThreadOnly protected boolean handleReportAudioStatus(HdmiCecMessage message) { assertRunOnServiceThread(); // TODO(amyjojo): implement report audio status handler @@ -407,8 +617,8 @@ public class HdmiCecLocalDeviceAudioSystem extends HdmiCecLocalDeviceSource { 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()) { + // Wake up device if System Audio Control is turned on + if (newSystemAudioMode) { mService.wakeUp(); } setSystemAudioMode(newSystemAudioMode); @@ -494,19 +704,20 @@ public class HdmiCecLocalDeviceAudioSystem extends HdmiCecLocalDeviceSource { invokeCallback(callback, HdmiControlManager.RESULT_SUCCESS); return; } - getActiveSource().invalidate(); if (!mService.isControlEnabled()) { + setRoutingPort(portId); setLocalActivePort(portId); invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE); return; } - int oldPath = getLocalActivePort() != Constants.CEC_SWITCH_HOME - ? getActivePathOnSwitchFromActivePortId(getLocalActivePort()) + int oldPath = getRoutingPort() != Constants.CEC_SWITCH_HOME + ? mService.portIdToPath(getRoutingPort()) : getDeviceInfo().getPhysicalAddress(); - int newPath = getActivePathOnSwitchFromActivePortId(portId); + int newPath = mService.portIdToPath(portId); if (oldPath == newPath) { return; } + setRoutingPort(portId); setLocalActivePort(portId); HdmiCecMessage routingChange = HdmiCecMessageBuilder.buildRoutingChange(mAddress, oldPath, newPath); @@ -575,10 +786,8 @@ public class HdmiCecLocalDeviceAudioSystem extends HdmiCecLocalDeviceSource { mService.maySendFeatureAbortCommand(message, Constants.ABORT_REFUSED); return; } - // Wake up device if it is still on standby - if (mService.isPowerStandbyOrTransient()) { - mService.wakeUp(); - } + // Wake up device + mService.wakeUp(); // Check if TV supports System Audio Control. // Handle broadcasting setSystemAudioMode on or aborting message on callback. queryTvSystemAudioModeSupport(new TvSystemAudioModeSupportedCallback() { @@ -635,9 +844,7 @@ public class HdmiCecLocalDeviceAudioSystem extends HdmiCecLocalDeviceSource { return; } // Wake up if the current device if ready to route. - if (mService.isPowerStandbyOrTransient()) { - mService.wakeUp(); - } + mService.wakeUp(); if (portId == Constants.CEC_SWITCH_HOME && mService.isPlaybackDevice()) { switchToHomeTvInput(); } else if (portId == Constants.CEC_SWITCH_ARC) { @@ -725,4 +932,50 @@ public class HdmiCecLocalDeviceAudioSystem extends HdmiCecLocalDeviceSource { mAddress, routingInformationPath)); routeToInputFromPortId(getRoutingPort()); } + + protected void updateDevicePowerStatus(int logicalAddress, int newPowerStatus) { + HdmiDeviceInfo info = getCecDeviceInfo(logicalAddress); + if (info == null) { + Slog.w(TAG, "Can not update power status of non-existing device:" + logicalAddress); + return; + } + + if (info.getDevicePowerStatus() == newPowerStatus) { + return; + } + + HdmiDeviceInfo newInfo = HdmiUtils.cloneHdmiDeviceInfo(info, newPowerStatus); + // addDeviceInfo replaces old device info with new one if exists. + addDeviceInfo(newInfo); + + invokeDeviceEventListener(newInfo, HdmiControlManager.DEVICE_EVENT_UPDATE_DEVICE); + } + + @ServiceThreadOnly + private void launchDeviceDiscovery() { + assertRunOnServiceThread(); + DeviceDiscoveryAction action = new DeviceDiscoveryAction(this, + new DeviceDiscoveryCallback() { + @Override + public void onDeviceDiscoveryDone(List<HdmiDeviceInfo> deviceInfos) { + for (HdmiDeviceInfo info : deviceInfos) { + addCecDevice(info); + } + } + }); + addAndStartAction(action); + } + + // Clear all device info. + @ServiceThreadOnly + private void clearDeviceInfoList() { + assertRunOnServiceThread(); + for (HdmiDeviceInfo info : HdmiUtils.sparseArrayToList(mDeviceInfos)) { + if (info.getPhysicalAddress() == mService.getPhysicalAddress()) { + continue; + } + invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE); + } + mDeviceInfos.clear(); + } } diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceSource.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceSource.java index 6532e16cffe5..a95f7f1f3f3d 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceSource.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceSource.java @@ -16,7 +16,10 @@ package com.android.server.hdmi; +import static com.android.internal.os.RoSystemProperties.PROPERTY_HDMI_IS_DEVICE_HDMI_CEC_SWITCH; + import android.hardware.hdmi.HdmiControlManager; +import android.hardware.hdmi.HdmiPortInfo; import android.hardware.hdmi.IHdmiControlCallback; import android.os.SystemProperties; import android.util.Slog; @@ -42,7 +45,7 @@ abstract class HdmiCecLocalDeviceSource extends HdmiCecLocalDevice { // Device has cec switch functionality or not. // Default is false. protected boolean mIsSwitchDevice = SystemProperties.getBoolean( - Constants.PROPERTY_HDMI_IS_DEVICE_HDMI_CEC_SWITCH, false); + PROPERTY_HDMI_IS_DEVICE_HDMI_CEC_SWITCH, false); // Routing port number used for Routing Control. // This records the default routing port or the previous valid routing port. @@ -71,9 +74,11 @@ abstract class HdmiCecLocalDeviceSource extends HdmiCecLocalDevice { @ServiceThreadOnly void onHotplug(int portId, boolean connected) { assertRunOnServiceThread(); - mCecMessageCache.flushAll(); + if (mService.getPortInfo(portId).getType() == HdmiPortInfo.PORT_OUTPUT) { + mCecMessageCache.flushAll(); + } // We'll not clear mIsActiveSource on the hotplug event to pass CETC 11.2.2-2 ~ 3. - if (mService.isPowerStandbyOrTransient()) { + if (connected) { mService.wakeUp(); } } @@ -117,6 +122,7 @@ abstract class HdmiCecLocalDeviceSource extends HdmiCecLocalDevice { setActiveSource(activeSource); } setIsActiveSource(physicalAddress == mService.getPhysicalAddress()); + updateDevicePowerStatus(logicalAddress, HdmiControlManager.POWER_STATUS_ON); switchInputOnReceivingNewActivePath(physicalAddress); return true; } @@ -185,6 +191,13 @@ abstract class HdmiCecLocalDeviceSource extends HdmiCecLocalDevice { // do nothing } + // Update the power status of the devices connected to the current device. + // This only works if the current device is a switch and keeps tracking the device info + // of the device connected to it. + protected void updateDevicePowerStatus(int logicalAddress, int newPowerStatus) { + // do nothing + } + // Active source claiming needs to be handled in Service // since service can decide who will be the active source when the device supports // multiple device types in this method. @@ -204,10 +217,8 @@ abstract class HdmiCecLocalDeviceSource extends HdmiCecLocalDevice { if (!mIsActiveSource) { return; } - // Wake up the device if the power is in standby mode - if (mService.isPowerStandbyOrTransient()) { - mService.wakeUp(); - } + // Wake up the device + mService.wakeUp(); return; } diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java index b91d8c637c79..a8c435086e8e 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java @@ -346,10 +346,6 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { } } - int getPortId(int physicalAddress) { - return mService.pathToPortId(physicalAddress); - } - /** * Returns the previous port id kept to handle input switching on <Inactive Source>. */ diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index 833091df5f1c..2d6e76294e4a 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -19,6 +19,7 @@ package com.android.server.hdmi; import static android.hardware.hdmi.HdmiControlManager.DEVICE_EVENT_ADD_DEVICE; import static android.hardware.hdmi.HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE; +import static com.android.internal.os.RoSystemProperties.PROPERTY_HDMI_IS_DEVICE_HDMI_CEC_SWITCH; import static com.android.server.hdmi.Constants.DISABLED; import static com.android.server.hdmi.Constants.ENABLED; import static com.android.server.hdmi.Constants.OPTION_MHL_ENABLE; @@ -754,6 +755,9 @@ public class HdmiControlService extends SystemService { mPortInfoMap = new UnmodifiableSparseArray<>(portInfoMap); mPortDeviceMap = new UnmodifiableSparseArray<>(portDeviceMap); + if (mMhlController == null) { + return; + } HdmiPortInfo[] mhlPortInfo = mMhlController.getPortInfos(); ArraySet<Integer> mhlSupportedPorts = new ArraySet<Integer>(mhlPortInfo.length); for (HdmiPortInfo info : mhlPortInfo) { @@ -808,13 +812,31 @@ public class HdmiControlService extends SystemService { } /** - * Returns the id of HDMI port located at the top of the hierarchy of - * the specified routing path. For the routing path 0x1220 (1.2.2.0), for instance, - * the port id to be returned is the ID associated with the port address - * 0x1000 (1.0.0.0) which is the topmost path of the given routing path. + * Returns the id of HDMI port located at the current device that runs this method. + * + * For TV with physical address 0x0000, target device 0x1120, we want port physical address + * 0x1000 to get the correct port id from {@link #mPortIdMap}. For device with Physical Address + * 0x2000, target device 0x2420, we want port address 0x24000 to get the port id. + * + * <p>Return {@link Constants#INVALID_PORT_ID} if target device does not connect to. + * + * @param path the target device's physical address. + * @return the id of the port that the target device eventually connects to + * on the current device. */ int pathToPortId(int path) { - int portAddress = path & Constants.ROUTING_PATH_TOP_MASK; + int mask = 0xF000; + int finalMask = 0xF000; + int physicalAddress = getPhysicalAddress(); + int maskedAddress = physicalAddress; + + while (maskedAddress != 0) { + maskedAddress = physicalAddress & mask; + finalMask |= mask; + mask >>= 4; + } + + int portAddress = path & finalMask; return mPortIdMap.get(portAddress, Constants.INVALID_PORT_ID); } @@ -1007,8 +1029,9 @@ public class HdmiControlService extends SystemService { void onHotplug(int portId, boolean connected) { assertRunOnServiceThread(); - if (connected && !isTvDevice()) { - if (getPortInfo(portId).getType() == HdmiPortInfo.PORT_OUTPUT && isSwitchDevice()) { + if (connected && !isTvDevice() + && getPortInfo(portId).getType() == HdmiPortInfo.PORT_OUTPUT) { + if (isSwitchDevice()) { initPortInfo(); HdmiLogger.debug("initPortInfo for switch device when onHotplug from tx."); } @@ -2130,7 +2153,7 @@ public class HdmiControlService extends SystemService { boolean isSwitchDevice() { return SystemProperties.getBoolean( - Constants.PROPERTY_HDMI_IS_DEVICE_HDMI_CEC_SWITCH, false); + PROPERTY_HDMI_IS_DEVICE_HDMI_CEC_SWITCH, false); } boolean isTvDeviceEnabled() { diff --git a/services/tests/servicestests/src/com/android/server/hdmi/FakeNativeWrapper.java b/services/tests/servicestests/src/com/android/server/hdmi/FakeNativeWrapper.java index 4255e37aac2d..8607ec66a5ba 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/FakeNativeWrapper.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/FakeNativeWrapper.java @@ -50,6 +50,7 @@ final class FakeNativeWrapper implements NativeWrapper { private final List<HdmiCecMessage> mResultMessages = new ArrayList<>(); private int mMyPhysicalAddress = 0; + private HdmiPortInfo[] mHdmiPortInfo = null; @Override public long nativeInit(HdmiCecController handler, MessageQueue messageQueue) { @@ -92,9 +93,11 @@ final class FakeNativeWrapper implements NativeWrapper { @Override public HdmiPortInfo[] nativeGetPortInfos(long controllerPtr) { - HdmiPortInfo[] hdmiPortInfo = new HdmiPortInfo[1]; - hdmiPortInfo[0] = new HdmiPortInfo(1, 1, 0x1000, true, true, true); - return hdmiPortInfo; + if (mHdmiPortInfo == null) { + mHdmiPortInfo = new HdmiPortInfo[1]; + mHdmiPortInfo[0] = new HdmiPortInfo(1, 1, 0x1000, true, true, true); + } + return mHdmiPortInfo; } @Override @@ -131,4 +134,10 @@ final class FakeNativeWrapper implements NativeWrapper { protected void setPhysicalAddress(int physicalAddress) { mMyPhysicalAddress = physicalAddress; } + + @VisibleForTesting + protected void setPortInfo(HdmiPortInfo[] hdmiPortInfo) { + mHdmiPortInfo = new HdmiPortInfo[hdmiPortInfo.length]; + System.arraycopy(hdmiPortInfo, 0, mHdmiPortInfo, 0, hdmiPortInfo.length); + } } diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java index b47f269bc721..3b51a2a70f83 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java @@ -15,6 +15,9 @@ */ package com.android.server.hdmi; +import static android.hardware.hdmi.HdmiControlManager.DEVICE_EVENT_ADD_DEVICE; +import static android.hardware.hdmi.HdmiControlManager.DEVICE_EVENT_UPDATE_DEVICE; + import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM; import static com.android.server.hdmi.Constants.ADDR_BROADCAST; import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_1; @@ -25,6 +28,7 @@ import static com.android.server.hdmi.HdmiControlService.STANDBY_SCREEN_OFF; import static com.google.common.truth.Truth.assertThat; +import android.hardware.hdmi.HdmiDeviceInfo; import android.media.AudioManager; import android.os.Looper; import android.os.SystemProperties; @@ -64,6 +68,8 @@ public class HdmiCecLocalDeviceAudioSystemTest { private int mMusicMaxVolume; private boolean mMusicMute; private int mAvrPhysicalAddress; + private int mInvokeDeviceEventState; + private HdmiDeviceInfo mDeviceInfo; @Before public void setUp() { @@ -127,6 +133,18 @@ public class HdmiCecLocalDeviceAudioSystemTest { @Override void wakeUp() {} + + @Override + void invokeDeviceEventListeners(HdmiDeviceInfo device, int status) { + mDeviceInfo = device; + mInvokeDeviceEventState = status; + } + + @Override + int pathToPortId(int path) { + // port id is not useful for the test right now + return 1; + } }; mMyLooper = mTestLooper.getLooper(); @@ -157,6 +175,8 @@ public class HdmiCecLocalDeviceAudioSystemTest { mNativeWrapper.setPhysicalAddress(mAvrPhysicalAddress); SystemProperties.set(Constants.PROPERTY_ARC_SUPPORT, "true"); SystemProperties.set(Constants.PROPERTY_SYSTEM_AUDIO_MODE_MUTING_ENABLE, "true"); + mInvokeDeviceEventState = 0; + mDeviceInfo = null; } @Test @@ -611,4 +631,73 @@ public class HdmiCecLocalDeviceAudioSystemTest { mTestLooper.dispatchAll(); assertThat(mNativeWrapper.getResultMessages()).contains(expectedMessage); } + + @Test + public void updateCecDevice_deviceNotExists_addDevice() { + assertThat(mInvokeDeviceEventState).isNotEqualTo(DEVICE_EVENT_ADD_DEVICE); + HdmiDeviceInfo newDevice = new HdmiDeviceInfo( + ADDR_PLAYBACK_1, 0x2100, 2, HdmiDeviceInfo.DEVICE_PLAYBACK, + Constants.UNKNOWN_VENDOR_ID, HdmiUtils.getDefaultDeviceName(ADDR_PLAYBACK_1)); + + mHdmiCecLocalDeviceAudioSystem.updateCecDevice(newDevice); + assertThat(mDeviceInfo).isEqualTo(newDevice); + assertThat(mHdmiCecLocalDeviceAudioSystem + .getCecDeviceInfo(newDevice.getLogicalAddress())).isEqualTo(newDevice); + assertThat(mInvokeDeviceEventState).isEqualTo(DEVICE_EVENT_ADD_DEVICE); + } + + @Test + public void updateCecDevice_deviceExists_doNothing() { + mInvokeDeviceEventState = 0; + HdmiDeviceInfo oldDevice = new HdmiDeviceInfo( + ADDR_PLAYBACK_1, 0x2100, 2, HdmiDeviceInfo.DEVICE_PLAYBACK, + Constants.UNKNOWN_VENDOR_ID, HdmiUtils.getDefaultDeviceName(ADDR_PLAYBACK_1)); + mHdmiCecLocalDeviceAudioSystem.addDeviceInfo(oldDevice); + + mHdmiCecLocalDeviceAudioSystem.updateCecDevice(oldDevice); + assertThat(mInvokeDeviceEventState).isEqualTo(0); + } + + @Test + public void updateCecDevice_deviceInfoDifferent_updateDevice() { + assertThat(mInvokeDeviceEventState).isNotEqualTo(DEVICE_EVENT_UPDATE_DEVICE); + HdmiDeviceInfo oldDevice = new HdmiDeviceInfo( + ADDR_PLAYBACK_1, 0x2100, 2, HdmiDeviceInfo.DEVICE_PLAYBACK, + Constants.UNKNOWN_VENDOR_ID, HdmiUtils.getDefaultDeviceName(ADDR_PLAYBACK_1)); + mHdmiCecLocalDeviceAudioSystem.addDeviceInfo(oldDevice); + + HdmiDeviceInfo differentDevice = new HdmiDeviceInfo( + ADDR_PLAYBACK_1, 0x2100, 4, HdmiDeviceInfo.DEVICE_PLAYBACK, + Constants.UNKNOWN_VENDOR_ID, HdmiUtils.getDefaultDeviceName(ADDR_PLAYBACK_1)); + + mHdmiCecLocalDeviceAudioSystem.updateCecDevice(differentDevice); + assertThat(mDeviceInfo).isEqualTo(differentDevice); + assertThat(mHdmiCecLocalDeviceAudioSystem + .getCecDeviceInfo(differentDevice.getLogicalAddress())).isEqualTo(differentDevice); + assertThat(mInvokeDeviceEventState).isEqualTo(DEVICE_EVENT_UPDATE_DEVICE); + } + + @Test + public void handleReportPhysicalAddress_differentPath_addDevice() { + assertThat(mInvokeDeviceEventState).isNotEqualTo(DEVICE_EVENT_ADD_DEVICE); + HdmiDeviceInfo oldDevice = new HdmiDeviceInfo( + ADDR_PLAYBACK_1, 0x2100, 2, HdmiDeviceInfo.DEVICE_PLAYBACK, + Constants.UNKNOWN_VENDOR_ID, HdmiUtils.getDefaultDeviceName(ADDR_PLAYBACK_1)); + mHdmiCecLocalDeviceAudioSystem.addDeviceInfo(oldDevice); + + HdmiDeviceInfo differentDevice = new HdmiDeviceInfo( + ADDR_PLAYBACK_1, 0x2200, 1, HdmiDeviceInfo.DEVICE_PLAYBACK, + Constants.UNKNOWN_VENDOR_ID, HdmiUtils.getDefaultDeviceName(ADDR_PLAYBACK_1)); + HdmiCecMessage reportPhysicalAddress = HdmiCecMessageBuilder + .buildReportPhysicalAddressCommand( + ADDR_PLAYBACK_1, 0x2200, HdmiDeviceInfo.DEVICE_PLAYBACK); + mHdmiCecLocalDeviceAudioSystem.handleReportPhysicalAddress(reportPhysicalAddress); + + mHdmiCecLocalDeviceAudioSystem.addDeviceInfo(oldDevice); + mTestLooper.dispatchAll(); + assertThat(mDeviceInfo).isEqualTo(differentDevice); + assertThat(mHdmiCecLocalDeviceAudioSystem + .getCecDeviceInfo(differentDevice.getLogicalAddress())).isEqualTo(differentDevice); + assertThat(mInvokeDeviceEventState).isEqualTo(DEVICE_EVENT_ADD_DEVICE); + } } diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java index 18c9a653dc7d..67ce13fdef72 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java @@ -20,13 +20,18 @@ import static android.hardware.hdmi.HdmiDeviceInfo.DEVICE_PLAYBACK; import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC; +import static com.google.common.truth.Truth.assertThat; + import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; +import android.hardware.hdmi.HdmiPortInfo; import android.os.Looper; import android.os.test.TestLooper; + import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -102,6 +107,7 @@ public class HdmiControlServiceTest { private TestLooper mTestLooper = new TestLooper(); private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>(); private boolean mStandbyMessageReceived; + private HdmiPortInfo[] mHdmiPortInfo; @Before public void SetUp() { @@ -131,6 +137,16 @@ public class HdmiControlServiceTest { mLocalDevices.add(mMyAudioSystemDevice); mLocalDevices.add(mMyPlaybackDevice); + mHdmiPortInfo = new HdmiPortInfo[4]; + mHdmiPortInfo[0] = + new HdmiPortInfo(1, HdmiPortInfo.PORT_INPUT, 0x2100, true, false, false); + mHdmiPortInfo[1] = + new HdmiPortInfo(2, HdmiPortInfo.PORT_INPUT, 0x2200, true, false, false); + mHdmiPortInfo[2] = + new HdmiPortInfo(3, HdmiPortInfo.PORT_INPUT, 0x2000, true, false, false); + mHdmiPortInfo[3] = + new HdmiPortInfo(4, HdmiPortInfo.PORT_INPUT, 0x3000, true, false, false); + mNativeWrapper.setPortInfo(mHdmiPortInfo); mHdmiControlService.initPortInfo(); mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC); @@ -159,4 +175,24 @@ public class HdmiControlServiceTest { assertTrue(mMyPlaybackDevice.isDisabled()); assertTrue(mMyAudioSystemDevice.isDisabled()); } + + @Test + public void pathToPort_pathExists_weAreNonTv() { + mNativeWrapper.setPhysicalAddress(0x2000); + assertThat(mHdmiControlService.pathToPortId(0x2120)).isEqualTo(1); + assertThat(mHdmiControlService.pathToPortId(0x2234)).isEqualTo(2); + } + + @Test + public void pathToPort_pathExists_weAreTv() { + mNativeWrapper.setPhysicalAddress(0x0000); + assertThat(mHdmiControlService.pathToPortId(0x2120)).isEqualTo(3); + assertThat(mHdmiControlService.pathToPortId(0x3234)).isEqualTo(4); + } + + @Test + public void pathToPort_pathInvalid() { + mNativeWrapper.setPhysicalAddress(0x2000); + assertThat(mHdmiControlService.pathToPortId(0x1000)).isEqualTo(Constants.INVALID_PORT_ID); + } } |