/* * 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 android.hardware.hdmi.HdmiControlManager.DEVICE_EVENT_ADD_DEVICE; import static android.hardware.hdmi.HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE; import static com.android.server.hdmi.Constants.DISABLED; import static com.android.server.hdmi.Constants.ENABLED; import static com.android.server.hdmi.Constants.OPTION_CEC_AUTO_WAKEUP; import static com.android.server.hdmi.Constants.OPTION_CEC_ENABLE; import static com.android.server.hdmi.Constants.OPTION_CEC_SERVICE_CONTROL; import static com.android.server.hdmi.Constants.OPTION_CEC_SET_LANGUAGE; import static com.android.server.hdmi.Constants.OPTION_MHL_ENABLE; import static com.android.server.hdmi.Constants.OPTION_MHL_INPUT_SWITCHING; import static com.android.server.hdmi.Constants.OPTION_MHL_POWER_CHARGE; import static com.android.server.hdmi.Constants.OPTION_MHL_SERVICE_CONTROL; import android.annotation.Nullable; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.database.ContentObserver; import android.hardware.hdmi.HdmiControlManager; import android.hardware.hdmi.HdmiDeviceInfo; import android.hardware.hdmi.HdmiHotplugEvent; import android.hardware.hdmi.HdmiPortInfo; import android.hardware.hdmi.IHdmiControlCallback; import android.hardware.hdmi.IHdmiControlService; import android.hardware.hdmi.IHdmiDeviceEventListener; import android.hardware.hdmi.IHdmiHotplugEventListener; import android.hardware.hdmi.IHdmiInputChangeListener; import android.hardware.hdmi.IHdmiMhlVendorCommandListener; import android.hardware.hdmi.IHdmiRecordListener; import android.hardware.hdmi.IHdmiSystemAudioModeChangeListener; import android.hardware.hdmi.IHdmiVendorCommandListener; import android.media.AudioManager; import android.media.tv.TvInputManager; import android.media.tv.TvInputManager.TvInputCallback; import android.net.Uri; import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.PowerManager; import android.os.RemoteException; import android.os.SystemClock; import android.os.SystemProperties; import android.os.UserHandle; import android.provider.Settings.Global; import android.text.TextUtils; import android.util.ArraySet; import android.util.Slog; import android.util.SparseArray; import android.util.SparseIntArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.IndentingPrintWriter; import com.android.server.SystemService; import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly; import com.android.server.hdmi.HdmiCecController.AllocateAddressCallback; import com.android.server.hdmi.HdmiCecLocalDevice.ActiveSource; import com.android.server.hdmi.HdmiCecLocalDevice.PendingActionClearedCallback; import libcore.util.EmptyArray; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; /** * Provides a service for sending and processing HDMI control messages, * HDMI-CEC and MHL control command, and providing the information on both standard. */ public final class HdmiControlService extends SystemService { private static final String TAG = "HdmiControlService"; private final Locale HONG_KONG = new Locale("zh", "HK"); private final Locale MACAU = new Locale("zh", "MO"); static final String PERMISSION = "android.permission.HDMI_CEC"; // The reason code to initiate intializeCec(). static final int INITIATED_BY_ENABLE_CEC = 0; static final int INITIATED_BY_BOOT_UP = 1; static final int INITIATED_BY_SCREEN_ON = 2; static final int INITIATED_BY_WAKE_UP_MESSAGE = 3; static final int INITIATED_BY_HOTPLUG = 4; // The reason code representing the intent action that drives the standby // procedure. The procedure starts either by Intent.ACTION_SCREEN_OFF or // Intent.ACTION_SHUTDOWN. static final int STANDBY_SCREEN_OFF = 0; static final int STANDBY_SHUTDOWN = 1; /** * Interface to report send result. */ interface SendMessageCallback { /** * Called when {@link HdmiControlService#sendCecCommand} is completed. * * @param error result of send request. * */ void onSendCompleted(int error); } /** * Interface to get a list of available logical devices. */ interface DevicePollingCallback { /** * Called when device polling is finished. * * @param ackedAddress a list of logical addresses of available devices */ void onPollingFinished(List ackedAddress); } private class HdmiControlBroadcastReceiver extends BroadcastReceiver { @ServiceThreadOnly @Override public void onReceive(Context context, Intent intent) { assertRunOnServiceThread(); switch (intent.getAction()) { case Intent.ACTION_SCREEN_OFF: if (isPowerOnOrTransient()) { onStandby(STANDBY_SCREEN_OFF); } break; case Intent.ACTION_SCREEN_ON: if (isPowerStandbyOrTransient()) { onWakeUp(); } break; case Intent.ACTION_CONFIGURATION_CHANGED: String language = getMenuLanguage(); if (!mLanguage.equals(language)) { onLanguageChanged(language); } break; case Intent.ACTION_SHUTDOWN: if (isPowerOnOrTransient()) { onStandby(STANDBY_SHUTDOWN); } break; } } private String getMenuLanguage() { Locale locale = Locale.getDefault(); if (locale.equals(Locale.TAIWAN) || locale.equals(HONG_KONG) || locale.equals(MACAU)) { // Android always returns "zho" for all Chinese variants. // Use "bibliographic" code defined in CEC639-2 for traditional // Chinese used in Taiwan/Hong Kong/Macau. return "chi"; } else { return locale.getISO3Language(); } } } // A thread to handle synchronous IO of CEC and MHL control service. // Since all of CEC and MHL HAL interfaces processed in short time (< 200ms) // and sparse call it shares a thread to handle IO operations. private final HandlerThread mIoThread = new HandlerThread("Hdmi Control Io Thread"); // Used to synchronize the access to the service. private final Object mLock = new Object(); // Type of logical devices hosted in the system. Stored in the unmodifiable list. private final List mLocalDevices; // List of records for hotplug event listener to handle the the caller killed in action. @GuardedBy("mLock") private final ArrayList mHotplugEventListenerRecords = new ArrayList<>(); // List of records for device event listener to handle the caller killed in action. @GuardedBy("mLock") private final ArrayList mDeviceEventListenerRecords = new ArrayList<>(); // List of records for vendor command listener to handle the caller killed in action. @GuardedBy("mLock") private final ArrayList mVendorCommandListenerRecords = new ArrayList<>(); @GuardedBy("mLock") private InputChangeListenerRecord mInputChangeListenerRecord; @GuardedBy("mLock") private HdmiRecordListenerRecord mRecordListenerRecord; // Set to true while HDMI control is enabled. If set to false, HDMI-CEC/MHL protocol // handling will be disabled and no request will be handled. @GuardedBy("mLock") private boolean mHdmiControlEnabled; // Set to true while the service is in normal mode. While set to false, no input change is // allowed. Used for situations where input change can confuse users such as channel auto-scan, // system upgrade, etc., a.k.a. "prohibit mode". @GuardedBy("mLock") private boolean mProhibitMode; // List of records for system audio mode change to handle the the caller killed in action. private final ArrayList mSystemAudioModeChangeListenerRecords = new ArrayList<>(); // Handler used to run a task in service thread. private final Handler mHandler = new Handler(); private final SettingsObserver mSettingsObserver; private final HdmiControlBroadcastReceiver mHdmiControlBroadcastReceiver = new HdmiControlBroadcastReceiver(); @Nullable private HdmiCecController mCecController; // HDMI port information. Stored in the unmodifiable list to keep the static information // from being modified. private List mPortInfo; // Map from path(physical address) to port ID. private UnmodifiableSparseIntArray mPortIdMap; // Map from port ID to HdmiPortInfo. private UnmodifiableSparseArray mPortInfoMap; // Map from port ID to HdmiDeviceInfo. private UnmodifiableSparseArray mPortDeviceMap; private HdmiCecMessageValidator mMessageValidator; @ServiceThreadOnly private int mPowerStatus = HdmiControlManager.POWER_STATUS_STANDBY; @ServiceThreadOnly private String mLanguage = Locale.getDefault().getISO3Language(); @ServiceThreadOnly private boolean mStandbyMessageReceived = false; @ServiceThreadOnly private boolean mWakeUpMessageReceived = false; @ServiceThreadOnly private int mActivePortId = Constants.INVALID_PORT_ID; // Set to true while the input change by MHL is allowed. @GuardedBy("mLock") private boolean mMhlInputChangeEnabled; // List of records for MHL Vendor command listener to handle the caller killed in action. @GuardedBy("mLock") private final ArrayList mMhlVendorCommandListenerRecords = new ArrayList<>(); @GuardedBy("mLock") private List mMhlDevices; @Nullable private HdmiMhlControllerStub mMhlController; @Nullable private TvInputManager mTvInputManager; @Nullable private PowerManager mPowerManager; // Last input port before switching to the MHL port. Should switch back to this port // when the mobile device sends the request one touch play with off. // Gets invalidated if we go to other port/input. @ServiceThreadOnly private int mLastInputMhl = Constants.INVALID_PORT_ID; // Set to true if the logical address allocation is completed. private boolean mAddressAllocated = false; // Buffer for processing the incoming cec messages while allocating logical addresses. private final class CecMessageBuffer { private List mBuffer = new ArrayList<>(); public void bufferMessage(HdmiCecMessage message) { switch (message.getOpcode()) { case Constants.MESSAGE_ACTIVE_SOURCE: bufferActiveSource(message); break; case Constants.MESSAGE_IMAGE_VIEW_ON: case Constants.MESSAGE_TEXT_VIEW_ON: bufferImageOrTextViewOn(message); break; // Add here if new message that needs to buffer default: // Do not need to buffer messages other than above break; } } public void processMessages() { for (final HdmiCecMessage message : mBuffer) { runOnServiceThread(new Runnable() { @Override public void run() { handleCecCommand(message); } }); } mBuffer.clear(); } private void bufferActiveSource(HdmiCecMessage message) { if (!replaceMessageIfBuffered(message, Constants.MESSAGE_ACTIVE_SOURCE)) { mBuffer.add(message); } } private void bufferImageOrTextViewOn(HdmiCecMessage message) { if (!replaceMessageIfBuffered(message, Constants.MESSAGE_IMAGE_VIEW_ON) && !replaceMessageIfBuffered(message, Constants.MESSAGE_TEXT_VIEW_ON)) { mBuffer.add(message); } } // Returns true if the message is replaced private boolean replaceMessageIfBuffered(HdmiCecMessage message, int opcode) { for (int i = 0; i < mBuffer.size(); i++) { HdmiCecMessage bufferedMessage = mBuffer.get(i); if (bufferedMessage.getOpcode() == opcode) { mBuffer.set(i, message); return true; } } return false; } } private CecMessageBuffer mCecMessageBuffer = new CecMessageBuffer(); public HdmiControlService(Context context) { super(context); mLocalDevices = getIntList(SystemProperties.get(Constants.PROPERTY_DEVICE_TYPE)); mSettingsObserver = new SettingsObserver(mHandler); } private static List getIntList(String string) { ArrayList list = new ArrayList<>(); TextUtils.SimpleStringSplitter splitter = new TextUtils.SimpleStringSplitter(','); splitter.setString(string); for (String item : splitter) { try { list.add(Integer.parseInt(item)); } catch (NumberFormatException e) { Slog.w(TAG, "Can't parseInt: " + item); } } return Collections.unmodifiableList(list); } @Override public void onStart() { mIoThread.start(); mPowerStatus = HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON; mProhibitMode = false; mHdmiControlEnabled = readBooleanSetting(Global.HDMI_CONTROL_ENABLED, true); mMhlInputChangeEnabled = readBooleanSetting(Global.MHL_INPUT_SWITCHING_ENABLED, true); mCecController = HdmiCecController.create(this); if (mCecController != null) { if (mHdmiControlEnabled) { initializeCec(INITIATED_BY_BOOT_UP); } } else { Slog.i(TAG, "Device does not support HDMI-CEC."); return; } mMhlController = HdmiMhlControllerStub.create(this); if (!mMhlController.isReady()) { Slog.i(TAG, "Device does not support MHL-control."); } mMhlDevices = Collections.emptyList(); initPortInfo(); mMessageValidator = new HdmiCecMessageValidator(this); publishBinderService(Context.HDMI_CONTROL_SERVICE, new BinderService()); if (mCecController != null) { // Register broadcast receiver for power state change. IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_SCREEN_OFF); filter.addAction(Intent.ACTION_SCREEN_ON); filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); getContext().registerReceiver(mHdmiControlBroadcastReceiver, filter); // Register ContentObserver to monitor the settings change. registerContentObserver(); } mMhlController.setOption(OPTION_MHL_SERVICE_CONTROL, ENABLED); } @Override public void onBootPhase(int phase) { if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) { mTvInputManager = (TvInputManager) getContext().getSystemService( Context.TV_INPUT_SERVICE); mPowerManager = (PowerManager) getContext().getSystemService(Context.POWER_SERVICE); } } TvInputManager getTvInputManager() { return mTvInputManager; } void registerTvInputCallback(TvInputCallback callback) { if (mTvInputManager == null) return; mTvInputManager.registerCallback(callback, mHandler); } void unregisterTvInputCallback(TvInputCallback callback) { if (mTvInputManager == null) return; mTvInputManager.unregisterCallback(callback); } PowerManager getPowerManager() { return mPowerManager; } /** * Called when the initialization of local devices is complete. */ private void onInitializeCecComplete(int initiatedBy) { if (mPowerStatus == HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON) { mPowerStatus = HdmiControlManager.POWER_STATUS_ON; } mWakeUpMessageReceived = false; if (isTvDeviceEnabled()) { mCecController.setOption(OPTION_CEC_AUTO_WAKEUP, toInt(tv().getAutoWakeup())); } int reason = -1; switch (initiatedBy) { case INITIATED_BY_BOOT_UP: reason = HdmiControlManager.CONTROL_STATE_CHANGED_REASON_START; break; case INITIATED_BY_ENABLE_CEC: reason = HdmiControlManager.CONTROL_STATE_CHANGED_REASON_SETTING; break; case INITIATED_BY_SCREEN_ON: case INITIATED_BY_WAKE_UP_MESSAGE: reason = HdmiControlManager.CONTROL_STATE_CHANGED_REASON_WAKEUP; break; } if (reason != -1) { invokeVendorCommandListenersOnControlStateChanged(true, reason); } } private void registerContentObserver() { ContentResolver resolver = getContext().getContentResolver(); String[] settings = new String[] { Global.HDMI_CONTROL_ENABLED, Global.HDMI_CONTROL_AUTO_WAKEUP_ENABLED, Global.HDMI_CONTROL_AUTO_DEVICE_OFF_ENABLED, Global.MHL_INPUT_SWITCHING_ENABLED, Global.MHL_POWER_CHARGE_ENABLED }; for (String s : settings) { resolver.registerContentObserver(Global.getUriFor(s), false, mSettingsObserver, UserHandle.USER_ALL); } } private class SettingsObserver extends ContentObserver { public SettingsObserver(Handler handler) { super(handler); } // onChange is set up to run in service thread. @Override public void onChange(boolean selfChange, Uri uri) { String option = uri.getLastPathSegment(); boolean enabled = readBooleanSetting(option, true); switch (option) { case Global.HDMI_CONTROL_ENABLED: setControlEnabled(enabled); break; case Global.HDMI_CONTROL_AUTO_WAKEUP_ENABLED: if (isTvDeviceEnabled()) { tv().setAutoWakeup(enabled); } setCecOption(OPTION_CEC_AUTO_WAKEUP, toInt(enabled)); break; case Global.HDMI_CONTROL_AUTO_DEVICE_OFF_ENABLED: for (int type : mLocalDevices) { HdmiCecLocalDevice localDevice = mCecController.getLocalDevice(type); localDevice.setAutoDeviceOff(enabled); } // No need to propagate to HAL. break; case Global.MHL_INPUT_SWITCHING_ENABLED: setMhlInputChangeEnabled(enabled); break; case Global.MHL_POWER_CHARGE_ENABLED: mMhlController.setOption(OPTION_MHL_POWER_CHARGE, toInt(enabled)); break; } } } private static int toInt(boolean enabled) { return enabled ? ENABLED : DISABLED; } boolean readBooleanSetting(String key, boolean defVal) { ContentResolver cr = getContext().getContentResolver(); return Global.getInt(cr, key, toInt(defVal)) == ENABLED; } void writeBooleanSetting(String key, boolean value) { ContentResolver cr = getContext().getContentResolver(); Global.putInt(cr, key, toInt(value)); } private void initializeCec(int initiatedBy) { mAddressAllocated = false; mCecController.setOption(OPTION_CEC_SERVICE_CONTROL, ENABLED); mCecController.setOption(OPTION_CEC_SET_LANGUAGE, HdmiUtils.languageToInt(mLanguage)); initializeLocalDevices(initiatedBy); } @ServiceThreadOnly private void initializeLocalDevices(final int initiatedBy) { assertRunOnServiceThread(); // A container for [Device type, Local device info]. ArrayList localDevices = new ArrayList<>(); for (int type : mLocalDevices) { HdmiCecLocalDevice localDevice = mCecController.getLocalDevice(type); if (localDevice == null) { localDevice = HdmiCecLocalDevice.create(this, type); } localDevice.init(); localDevices.add(localDevice); } // It's now safe to flush existing local devices from mCecController since they were // already moved to 'localDevices'. clearLocalDevices(); allocateLogicalAddress(localDevices, initiatedBy); } @ServiceThreadOnly private void allocateLogicalAddress(final ArrayList allocatingDevices, final int initiatedBy) { assertRunOnServiceThread(); mCecController.clearLogicalAddress(); final ArrayList allocatedDevices = new ArrayList<>(); final int[] finished = new int[1]; mAddressAllocated = allocatingDevices.isEmpty(); for (final HdmiCecLocalDevice localDevice : allocatingDevices) { mCecController.allocateLogicalAddress(localDevice.getType(), localDevice.getPreferredAddress(), new AllocateAddressCallback() { @Override public void onAllocated(int deviceType, int logicalAddress) { if (logicalAddress == Constants.ADDR_UNREGISTERED) { Slog.e(TAG, "Failed to allocate address:[device_type:" + deviceType + "]"); } else { // Set POWER_STATUS_ON to all local devices because they share lifetime // with system. HdmiDeviceInfo deviceInfo = createDeviceInfo(logicalAddress, deviceType, HdmiControlManager.POWER_STATUS_ON); localDevice.setDeviceInfo(deviceInfo); mCecController.addLocalDevice(deviceType, localDevice); mCecController.addLogicalAddress(logicalAddress); allocatedDevices.add(localDevice); } // Address allocation completed for all devices. Notify each device. if (allocatingDevices.size() == ++finished[0]) { mAddressAllocated = true; if (initiatedBy != INITIATED_BY_HOTPLUG) { // In case of the hotplug we don't call onInitializeCecComplete() // since we reallocate the logical address only. onInitializeCecComplete(initiatedBy); } notifyAddressAllocated(allocatedDevices, initiatedBy); mCecMessageBuffer.processMessages(); } } }); } } @ServiceThreadOnly private void notifyAddressAllocated(ArrayList devices, int initiatedBy) { assertRunOnServiceThread(); for (HdmiCecLocalDevice device : devices) { int address = device.getDeviceInfo().getLogicalAddress(); device.handleAddressAllocated(address, initiatedBy); } } // Initialize HDMI port information. Combine the information from CEC and MHL HAL and // keep them in one place. @ServiceThreadOnly private void initPortInfo() { assertRunOnServiceThread(); HdmiPortInfo[] cecPortInfo = null; // CEC HAL provides majority of the info while MHL does only MHL support flag for // each port. Return empty array if CEC HAL didn't provide the info. if (mCecController != null) { cecPortInfo = mCecController.getPortInfos(); } if (cecPortInfo == null) { return; } SparseArray portInfoMap = new SparseArray<>(); SparseIntArray portIdMap = new SparseIntArray(); SparseArray portDeviceMap = new SparseArray<>(); for (HdmiPortInfo info : cecPortInfo) { portIdMap.put(info.getAddress(), info.getId()); portInfoMap.put(info.getId(), info); portDeviceMap.put(info.getId(), new HdmiDeviceInfo(info.getAddress(), info.getId())); } mPortIdMap = new UnmodifiableSparseIntArray(portIdMap); mPortInfoMap = new UnmodifiableSparseArray<>(portInfoMap); mPortDeviceMap = new UnmodifiableSparseArray<>(portDeviceMap); HdmiPortInfo[] mhlPortInfo = mMhlController.getPortInfos(); ArraySet mhlSupportedPorts = new ArraySet(mhlPortInfo.length); for (HdmiPortInfo info : mhlPortInfo) { if (info.isMhlSupported()) { mhlSupportedPorts.add(info.getId()); } } // Build HDMI port info list with CEC port info plus MHL supported flag. We can just use // cec port info if we do not have have port that supports MHL. if (mhlSupportedPorts.isEmpty()) { mPortInfo = Collections.unmodifiableList(Arrays.asList(cecPortInfo)); return; } ArrayList result = new ArrayList<>(cecPortInfo.length); for (HdmiPortInfo info : cecPortInfo) { if (mhlSupportedPorts.contains(info.getId())) { result.add(new HdmiPortInfo(info.getId(), info.getType(), info.getAddress(), info.isCecSupported(), true, info.isArcSupported())); } else { result.add(info); } } mPortInfo = Collections.unmodifiableList(result); } List getPortInfo() { return mPortInfo; } /** * Returns HDMI port information for the given port id. * * @param portId HDMI port id * @return {@link HdmiPortInfo} for the given port */ HdmiPortInfo getPortInfo(int portId) { return mPortInfoMap.get(portId, null); } /** * Returns the routing path (physical address) of the HDMI port for the given * port id. */ int portIdToPath(int portId) { HdmiPortInfo portInfo = getPortInfo(portId); if (portInfo == null) { Slog.e(TAG, "Cannot find the port info: " + portId); return Constants.INVALID_PHYSICAL_ADDRESS; } return portInfo.getAddress(); } /** * 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. */ int pathToPortId(int path) { int portAddress = path & Constants.ROUTING_PATH_TOP_MASK; return mPortIdMap.get(portAddress, Constants.INVALID_PORT_ID); } boolean isValidPortId(int portId) { return getPortInfo(portId) != null; } /** * Returns {@link Looper} for IO operation. * *

Declared as package-private. */ Looper getIoLooper() { return mIoThread.getLooper(); } /** * Returns {@link Looper} of main thread. Use this {@link Looper} instance * for tasks that are running on main service thread. * *

Declared as package-private. */ Looper getServiceLooper() { return mHandler.getLooper(); } /** * Returns physical address of the device. */ int getPhysicalAddress() { return mCecController.getPhysicalAddress(); } /** * Returns vendor id of CEC service. */ int getVendorId() { return mCecController.getVendorId(); } @ServiceThreadOnly HdmiDeviceInfo getDeviceInfo(int logicalAddress) { assertRunOnServiceThread(); return tv() == null ? null : tv().getCecDeviceInfo(logicalAddress); } @ServiceThreadOnly HdmiDeviceInfo getDeviceInfoByPort(int port) { assertRunOnServiceThread(); HdmiMhlLocalDeviceStub info = mMhlController.getLocalDevice(port); if (info != null) { return info.getInfo(); } return null; } /** * Returns version of CEC. */ int getCecVersion() { return mCecController.getVersion(); } /** * Whether a device of the specified physical address is connected to ARC enabled port. */ boolean isConnectedToArcPort(int physicalAddress) { int portId = pathToPortId(physicalAddress); if (portId != Constants.INVALID_PORT_ID) { return mPortInfoMap.get(portId).isArcSupported(); } return false; } @ServiceThreadOnly boolean isConnected(int portId) { assertRunOnServiceThread(); return mCecController.isConnected(portId); } void runOnServiceThread(Runnable runnable) { mHandler.post(runnable); } void runOnServiceThreadAtFrontOfQueue(Runnable runnable) { mHandler.postAtFrontOfQueue(runnable); } private void assertRunOnServiceThread() { if (Looper.myLooper() != mHandler.getLooper()) { throw new IllegalStateException("Should run on service thread."); } } /** * Transmit a CEC command to CEC bus. * * @param command CEC command to send out * @param callback interface used to the result of send command */ @ServiceThreadOnly void sendCecCommand(HdmiCecMessage command, @Nullable SendMessageCallback callback) { assertRunOnServiceThread(); if (mMessageValidator.isValid(command) == HdmiCecMessageValidator.OK) { mCecController.sendCommand(command, callback); } else { HdmiLogger.error("Invalid message type:" + command); if (callback != null) { callback.onSendCompleted(Constants.SEND_RESULT_FAILURE); } } } @ServiceThreadOnly void sendCecCommand(HdmiCecMessage command) { assertRunOnServiceThread(); sendCecCommand(command, null); } /** * Send command on the given CEC message if possible. * If the aborted message is invalid, then it wont send the message. * @param command original command to be aborted * @param reason reason of feature abort */ @ServiceThreadOnly void maySendFeatureAbortCommand(HdmiCecMessage command, int reason) { assertRunOnServiceThread(); mCecController.maySendFeatureAbortCommand(command, reason); } @ServiceThreadOnly boolean handleCecCommand(HdmiCecMessage message) { assertRunOnServiceThread(); if (!mAddressAllocated) { mCecMessageBuffer.bufferMessage(message); return true; } int errorCode = mMessageValidator.isValid(message); if (errorCode != HdmiCecMessageValidator.OK) { // We'll not response on the messages with the invalid source or destination // or with parameter length shorter than specified in the standard. if (errorCode == HdmiCecMessageValidator.ERROR_PARAMETER) { maySendFeatureAbortCommand(message, Constants.ABORT_INVALID_OPERAND); } return true; } return dispatchMessageToLocalDevice(message); } void setAudioReturnChannel(int portId, boolean enabled) { mCecController.setAudioReturnChannel(portId, enabled); } @ServiceThreadOnly private boolean dispatchMessageToLocalDevice(HdmiCecMessage message) { assertRunOnServiceThread(); for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) { if (device.dispatchMessage(message) && message.getDestination() != Constants.ADDR_BROADCAST) { return true; } } if (message.getDestination() != Constants.ADDR_BROADCAST) { HdmiLogger.warning("Unhandled cec command:" + message); } return false; } /** * Called when a new hotplug event is issued. * * @param portId hdmi port number where hot plug event issued. * @param connected whether to be plugged in or not */ @ServiceThreadOnly void onHotplug(int portId, boolean connected) { assertRunOnServiceThread(); if (connected && !isTvDevice()) { ArrayList localDevices = new ArrayList<>(); for (int type : mLocalDevices) { HdmiCecLocalDevice localDevice = mCecController.getLocalDevice(type); if (localDevice == null) { localDevice = HdmiCecLocalDevice.create(this, type); localDevice.init(); } localDevices.add(localDevice); } allocateLogicalAddress(localDevices, INITIATED_BY_HOTPLUG); } for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) { device.onHotplug(portId, connected); } announceHotplugEvent(portId, connected); } /** * Poll all remote devices. It sends <Polling Message> to all remote * devices. * * @param callback an interface used to get a list of all remote devices' address * @param sourceAddress a logical address of source device where sends polling message * @param pickStrategy strategy how to pick polling candidates * @param retryCount the number of retry used to send polling message to remote devices * @throw IllegalArgumentException if {@code pickStrategy} is invalid value */ @ServiceThreadOnly void pollDevices(DevicePollingCallback callback, int sourceAddress, int pickStrategy, int retryCount) { assertRunOnServiceThread(); mCecController.pollDevices(callback, sourceAddress, checkPollStrategy(pickStrategy), retryCount); } private int checkPollStrategy(int pickStrategy) { int strategy = pickStrategy & Constants.POLL_STRATEGY_MASK; if (strategy == 0) { throw new IllegalArgumentException("Invalid poll strategy:" + pickStrategy); } int iterationStrategy = pickStrategy & Constants.POLL_ITERATION_STRATEGY_MASK; if (iterationStrategy == 0) { throw new IllegalArgumentException("Invalid iteration strategy:" + pickStrategy); } return strategy | iterationStrategy; } List getAllLocalDevices() { assertRunOnServiceThread(); return mCecController.getLocalDeviceList(); } Object getServiceLock() { return mLock; } void setAudioStatus(boolean mute, int volume) { AudioManager audioManager = getAudioManager(); boolean muted = audioManager.isStreamMute(AudioManager.STREAM_MUSIC); if (mute) { if (!muted) { audioManager.setStreamMute(AudioManager.STREAM_MUSIC, true); } } else { if (muted) { audioManager.setStreamMute(AudioManager.STREAM_MUSIC, false); } // FLAG_HDMI_SYSTEM_AUDIO_VOLUME prevents audio manager from announcing // volume change notification back to hdmi control service. audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_HDMI_SYSTEM_AUDIO_VOLUME); } } void announceSystemAudioModeChange(boolean enabled) { synchronized (mLock) { for (SystemAudioModeChangeListenerRecord record : mSystemAudioModeChangeListenerRecords) { invokeSystemAudioModeChangeLocked(record.mListener, enabled); } } } private HdmiDeviceInfo createDeviceInfo(int logicalAddress, int deviceType, int powerStatus) { // TODO: find better name instead of model name. String displayName = Build.MODEL; return new HdmiDeviceInfo(logicalAddress, getPhysicalAddress(), pathToPortId(getPhysicalAddress()), deviceType, getVendorId(), displayName); } @ServiceThreadOnly void handleMhlHotplugEvent(int portId, boolean connected) { assertRunOnServiceThread(); // Hotplug event is used to add/remove MHL devices as TV input. if (connected) { HdmiMhlLocalDeviceStub newDevice = new HdmiMhlLocalDeviceStub(this, portId); HdmiMhlLocalDeviceStub oldDevice = mMhlController.addLocalDevice(newDevice); if (oldDevice != null) { oldDevice.onDeviceRemoved(); Slog.i(TAG, "Old device of port " + portId + " is removed"); } invokeDeviceEventListeners(newDevice.getInfo(), DEVICE_EVENT_ADD_DEVICE); updateSafeMhlInput(); } else { HdmiMhlLocalDeviceStub device = mMhlController.removeLocalDevice(portId); if (device != null) { device.onDeviceRemoved(); invokeDeviceEventListeners(device.getInfo(), DEVICE_EVENT_REMOVE_DEVICE); updateSafeMhlInput(); } else { Slog.w(TAG, "No device to remove:[portId=" + portId); } } announceHotplugEvent(portId, connected); } @ServiceThreadOnly void handleMhlBusModeChanged(int portId, int busmode) { assertRunOnServiceThread(); HdmiMhlLocalDeviceStub device = mMhlController.getLocalDevice(portId); if (device != null) { device.setBusMode(busmode); } else { Slog.w(TAG, "No mhl device exists for bus mode change[portId:" + portId + ", busmode:" + busmode + "]"); } } @ServiceThreadOnly void handleMhlBusOvercurrent(int portId, boolean on) { assertRunOnServiceThread(); HdmiMhlLocalDeviceStub device = mMhlController.getLocalDevice(portId); if (device != null) { device.onBusOvercurrentDetected(on); } else { Slog.w(TAG, "No mhl device exists for bus overcurrent event[portId:" + portId + "]"); } } @ServiceThreadOnly void handleMhlDeviceStatusChanged(int portId, int adopterId, int deviceId) { assertRunOnServiceThread(); HdmiMhlLocalDeviceStub device = mMhlController.getLocalDevice(portId); if (device != null) { device.setDeviceStatusChange(adopterId, deviceId); } else { Slog.w(TAG, "No mhl device exists for device status event[portId:" + portId + ", adopterId:" + adopterId + ", deviceId:" + deviceId + "]"); } } @ServiceThreadOnly private void updateSafeMhlInput() { assertRunOnServiceThread(); List inputs = Collections.emptyList(); SparseArray devices = mMhlController.getAllLocalDevices(); for (int i = 0; i < devices.size(); ++i) { HdmiMhlLocalDeviceStub device = devices.valueAt(i); HdmiDeviceInfo info = device.getInfo(); if (info != null) { if (inputs.isEmpty()) { inputs = new ArrayList<>(); } inputs.add(device.getInfo()); } } synchronized (mLock) { mMhlDevices = inputs; } } private List getMhlDevicesLocked() { return mMhlDevices; } private class HdmiMhlVendorCommandListenerRecord implements IBinder.DeathRecipient { private final IHdmiMhlVendorCommandListener mListener; public HdmiMhlVendorCommandListenerRecord(IHdmiMhlVendorCommandListener listener) { mListener = listener; } @Override public void binderDied() { mMhlVendorCommandListenerRecords.remove(this); } } // Record class that monitors the event of the caller of being killed. Used to clean up // the listener list and record list accordingly. private final class HotplugEventListenerRecord implements IBinder.DeathRecipient { private final IHdmiHotplugEventListener mListener; public HotplugEventListenerRecord(IHdmiHotplugEventListener listener) { mListener = listener; } @Override public void binderDied() { synchronized (mLock) { mHotplugEventListenerRecords.remove(this); } } @Override public boolean equals(Object obj) { if (!(obj instanceof HotplugEventListenerRecord)) return false; if (obj == this) return true; HotplugEventListenerRecord other = (HotplugEventListenerRecord) obj; return other.mListener == this.mListener; } @Override public int hashCode() { return mListener.hashCode(); } } private final class DeviceEventListenerRecord implements IBinder.DeathRecipient { private final IHdmiDeviceEventListener mListener; public DeviceEventListenerRecord(IHdmiDeviceEventListener listener) { mListener = listener; } @Override public void binderDied() { synchronized (mLock) { mDeviceEventListenerRecords.remove(this); } } } private final class SystemAudioModeChangeListenerRecord implements IBinder.DeathRecipient { private final IHdmiSystemAudioModeChangeListener mListener; public SystemAudioModeChangeListenerRecord(IHdmiSystemAudioModeChangeListener listener) { mListener = listener; } @Override public void binderDied() { synchronized (mLock) { mSystemAudioModeChangeListenerRecords.remove(this); } } } class VendorCommandListenerRecord implements IBinder.DeathRecipient { private final IHdmiVendorCommandListener mListener; private final int mDeviceType; public VendorCommandListenerRecord(IHdmiVendorCommandListener listener, int deviceType) { mListener = listener; mDeviceType = deviceType; } @Override public void binderDied() { synchronized (mLock) { mVendorCommandListenerRecords.remove(this); } } } private class HdmiRecordListenerRecord implements IBinder.DeathRecipient { private final IHdmiRecordListener mListener; public HdmiRecordListenerRecord(IHdmiRecordListener listener) { mListener = listener; } @Override public void binderDied() { synchronized (mLock) { mRecordListenerRecord = null; } } } private void enforceAccessPermission() { getContext().enforceCallingOrSelfPermission(PERMISSION, TAG); } private final class BinderService extends IHdmiControlService.Stub { @Override public int[] getSupportedTypes() { enforceAccessPermission(); // mLocalDevices is an unmodifiable list - no lock necesary. int[] localDevices = new int[mLocalDevices.size()]; for (int i = 0; i < localDevices.length; ++i) { localDevices[i] = mLocalDevices.get(i); } return localDevices; } @Override public HdmiDeviceInfo getActiveSource() { enforceAccessPermission(); HdmiCecLocalDeviceTv tv = tv(); if (tv == null) { Slog.w(TAG, "Local tv device not available"); return null; } ActiveSource activeSource = tv.getActiveSource(); if (activeSource.isValid()) { return new HdmiDeviceInfo(activeSource.logicalAddress, activeSource.physicalAddress, HdmiDeviceInfo.PORT_INVALID, HdmiDeviceInfo.DEVICE_INACTIVE, 0, ""); } int activePath = tv.getActivePath(); if (activePath != HdmiDeviceInfo.PATH_INVALID) { HdmiDeviceInfo info = tv.getSafeDeviceInfoByPath(activePath); return (info != null) ? info : new HdmiDeviceInfo(activePath, tv.getActivePortId()); } return null; } @Override public void deviceSelect(final int deviceId, final IHdmiControlCallback callback) { enforceAccessPermission(); runOnServiceThread(new Runnable() { @Override public void run() { if (callback == null) { Slog.e(TAG, "Callback cannot be null"); return; } HdmiCecLocalDeviceTv tv = tv(); if (tv == null) { Slog.w(TAG, "Local tv device not available"); invokeCallback(callback, HdmiControlManager.RESULT_SOURCE_NOT_AVAILABLE); return; } HdmiMhlLocalDeviceStub device = mMhlController.getLocalDeviceById(deviceId); if (device != null) { if (device.getPortId() == tv.getActivePortId()) { invokeCallback(callback, HdmiControlManager.RESULT_SUCCESS); return; } // Upon selecting MHL device, we send RAP[Content On] to wake up // the connected mobile device, start routing control to switch ports. // callback is handled by MHL action. device.turnOn(callback); tv.doManualPortSwitching(device.getPortId(), null); return; } tv.deviceSelect(deviceId, callback); } }); } @Override public void portSelect(final int portId, final IHdmiControlCallback callback) { enforceAccessPermission(); runOnServiceThread(new Runnable() { @Override public void run() { if (callback == null) { Slog.e(TAG, "Callback cannot be null"); return; } HdmiCecLocalDeviceTv tv = tv(); if (tv == null) { Slog.w(TAG, "Local tv device not available"); invokeCallback(callback, HdmiControlManager.RESULT_SOURCE_NOT_AVAILABLE); return; } tv.doManualPortSwitching(portId, callback); } }); } @Override public void sendKeyEvent(final int deviceType, final int keyCode, final boolean isPressed) { enforceAccessPermission(); runOnServiceThread(new Runnable() { @Override public void run() { HdmiMhlLocalDeviceStub device = mMhlController.getLocalDevice(mActivePortId); if (device != null) { device.sendKeyEvent(keyCode, isPressed); return; } if (mCecController != null) { HdmiCecLocalDevice localDevice = mCecController.getLocalDevice(deviceType); if (localDevice == null) { Slog.w(TAG, "Local device not available"); return; } localDevice.sendKeyEvent(keyCode, isPressed); } } }); } @Override public void oneTouchPlay(final IHdmiControlCallback callback) { enforceAccessPermission(); runOnServiceThread(new Runnable() { @Override public void run() { HdmiControlService.this.oneTouchPlay(callback); } }); } @Override public void queryDisplayStatus(final IHdmiControlCallback callback) { enforceAccessPermission(); runOnServiceThread(new Runnable() { @Override public void run() { HdmiControlService.this.queryDisplayStatus(callback); } }); } @Override public void addHotplugEventListener(final IHdmiHotplugEventListener listener) { enforceAccessPermission(); HdmiControlService.this.addHotplugEventListener(listener); } @Override public void removeHotplugEventListener(final IHdmiHotplugEventListener listener) { enforceAccessPermission(); HdmiControlService.this.removeHotplugEventListener(listener); } @Override public void addDeviceEventListener(final IHdmiDeviceEventListener listener) { enforceAccessPermission(); HdmiControlService.this.addDeviceEventListener(listener); } @Override public List getPortInfo() { enforceAccessPermission(); return HdmiControlService.this.getPortInfo(); } @Override public boolean canChangeSystemAudioMode() { enforceAccessPermission(); HdmiCecLocalDeviceTv tv = tv(); if (tv == null) { return false; } return tv.hasSystemAudioDevice(); } @Override public boolean getSystemAudioMode() { enforceAccessPermission(); HdmiCecLocalDeviceTv tv = tv(); if (tv == null) { return false; } return tv.isSystemAudioActivated(); } @Override public void setSystemAudioMode(final boolean enabled, final IHdmiControlCallback callback) { enforceAccessPermission(); runOnServiceThread(new Runnable() { @Override public void run() { HdmiCecLocalDeviceTv tv = tv(); if (tv == null) { Slog.w(TAG, "Local tv device not available"); invokeCallback(callback, HdmiControlManager.RESULT_SOURCE_NOT_AVAILABLE); return; } tv.changeSystemAudioMode(enabled, callback); } }); } @Override public void addSystemAudioModeChangeListener( final IHdmiSystemAudioModeChangeListener listener) { enforceAccessPermission(); HdmiControlService.this.addSystemAudioModeChangeListner(listener); } @Override public void removeSystemAudioModeChangeListener( final IHdmiSystemAudioModeChangeListener listener) { enforceAccessPermission(); HdmiControlService.this.removeSystemAudioModeChangeListener(listener); } @Override public void setInputChangeListener(final IHdmiInputChangeListener listener) { enforceAccessPermission(); HdmiControlService.this.setInputChangeListener(listener); } @Override public List getInputDevices() { enforceAccessPermission(); // No need to hold the lock for obtaining TV device as the local device instance // is preserved while the HDMI control is enabled. HdmiCecLocalDeviceTv tv = tv(); synchronized (mLock) { List cecDevices = (tv == null) ? Collections.emptyList() : tv.getSafeExternalInputsLocked(); return HdmiUtils.mergeToUnmodifiableList(cecDevices, getMhlDevicesLocked()); } } // Returns all the CEC devices on the bus including system audio, switch, // even those of reserved type. @Override public List getDeviceList() { enforceAccessPermission(); HdmiCecLocalDeviceTv tv = tv(); synchronized (mLock) { return (tv == null) ? Collections.emptyList() : tv.getSafeCecDevicesLocked(); } } @Override public void setSystemAudioVolume(final int oldIndex, final int newIndex, final int maxIndex) { enforceAccessPermission(); runOnServiceThread(new Runnable() { @Override public void run() { HdmiCecLocalDeviceTv tv = tv(); if (tv == null) { Slog.w(TAG, "Local tv device not available"); return; } tv.changeVolume(oldIndex, newIndex - oldIndex, maxIndex); } }); } @Override public void setSystemAudioMute(final boolean mute) { enforceAccessPermission(); runOnServiceThread(new Runnable() { @Override public void run() { HdmiCecLocalDeviceTv tv = tv(); if (tv == null) { Slog.w(TAG, "Local tv device not available"); return; } tv.changeMute(mute); } }); } @Override public void setArcMode(final boolean enabled) { enforceAccessPermission(); runOnServiceThread(new Runnable() { @Override public void run() { HdmiCecLocalDeviceTv tv = tv(); if (tv == null) { Slog.w(TAG, "Local tv device not available to change arc mode."); return; } } }); } @Override public void setProhibitMode(final boolean enabled) { enforceAccessPermission(); if (!isTvDevice()) { return; } HdmiControlService.this.setProhibitMode(enabled); } @Override public void addVendorCommandListener(final IHdmiVendorCommandListener listener, final int deviceType) { enforceAccessPermission(); HdmiControlService.this.addVendorCommandListener(listener, deviceType); } @Override public void sendVendorCommand(final int deviceType, final int targetAddress, final byte[] params, final boolean hasVendorId) { enforceAccessPermission(); runOnServiceThread(new Runnable() { @Override public void run() { HdmiCecLocalDevice device = mCecController.getLocalDevice(deviceType); if (device == null) { Slog.w(TAG, "Local device not available"); return; } if (hasVendorId) { sendCecCommand(HdmiCecMessageBuilder.buildVendorCommandWithId( device.getDeviceInfo().getLogicalAddress(), targetAddress, getVendorId(), params)); } else { sendCecCommand(HdmiCecMessageBuilder.buildVendorCommand( device.getDeviceInfo().getLogicalAddress(), targetAddress, params)); } } }); } @Override public void sendStandby(final int deviceType, final int deviceId) { enforceAccessPermission(); runOnServiceThread(new Runnable() { @Override public void run() { HdmiMhlLocalDeviceStub mhlDevice = mMhlController.getLocalDeviceById(deviceId); if (mhlDevice != null) { mhlDevice.sendStandby(); return; } HdmiCecLocalDevice device = mCecController.getLocalDevice(deviceType); if (device == null) { Slog.w(TAG, "Local device not available"); return; } device.sendStandby(deviceId); } }); } @Override public void setHdmiRecordListener(IHdmiRecordListener listener) { enforceAccessPermission(); HdmiControlService.this.setHdmiRecordListener(listener); } @Override public void startOneTouchRecord(final int recorderAddress, final byte[] recordSource) { enforceAccessPermission(); runOnServiceThread(new Runnable() { @Override public void run() { if (!isTvDeviceEnabled()) { Slog.w(TAG, "TV device is not enabled."); return; } tv().startOneTouchRecord(recorderAddress, recordSource); } }); } @Override public void stopOneTouchRecord(final int recorderAddress) { enforceAccessPermission(); runOnServiceThread(new Runnable() { @Override public void run() { if (!isTvDeviceEnabled()) { Slog.w(TAG, "TV device is not enabled."); return; } tv().stopOneTouchRecord(recorderAddress); } }); } @Override public void startTimerRecording(final int recorderAddress, final int sourceType, final byte[] recordSource) { enforceAccessPermission(); runOnServiceThread(new Runnable() { @Override public void run() { if (!isTvDeviceEnabled()) { Slog.w(TAG, "TV device is not enabled."); return; } tv().startTimerRecording(recorderAddress, sourceType, recordSource); } }); } @Override public void clearTimerRecording(final int recorderAddress, final int sourceType, final byte[] recordSource) { enforceAccessPermission(); runOnServiceThread(new Runnable() { @Override public void run() { if (!isTvDeviceEnabled()) { Slog.w(TAG, "TV device is not enabled."); return; } tv().clearTimerRecording(recorderAddress, sourceType, recordSource); } }); } @Override public void sendMhlVendorCommand(final int portId, final int offset, final int length, final byte[] data) { enforceAccessPermission(); runOnServiceThread(new Runnable() { @Override public void run() { if (!isControlEnabled()) { Slog.w(TAG, "Hdmi control is disabled."); return ; } HdmiMhlLocalDeviceStub device = mMhlController.getLocalDevice(portId); if (device == null) { Slog.w(TAG, "Invalid port id:" + portId); return; } mMhlController.sendVendorCommand(portId, offset, length, data); } }); } @Override public void addHdmiMhlVendorCommandListener( IHdmiMhlVendorCommandListener listener) { enforceAccessPermission(); HdmiControlService.this.addHdmiMhlVendorCommandListener(listener); } @Override protected void dump(FileDescriptor fd, final PrintWriter writer, String[] args) { getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG); final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " "); pw.println("mHdmiControlEnabled: " + mHdmiControlEnabled); pw.println("mProhibitMode: " + mProhibitMode); if (mCecController != null) { pw.println("mCecController: "); pw.increaseIndent(); mCecController.dump(pw); pw.decreaseIndent(); } pw.println("mMhlController: "); pw.increaseIndent(); mMhlController.dump(pw); pw.decreaseIndent(); pw.println("mPortInfo: "); pw.increaseIndent(); for (HdmiPortInfo hdmiPortInfo : mPortInfo) { pw.println("- " + hdmiPortInfo); } pw.decreaseIndent(); pw.println("mPowerStatus: " + mPowerStatus); } } @ServiceThreadOnly private void oneTouchPlay(final IHdmiControlCallback callback) { assertRunOnServiceThread(); HdmiCecLocalDevicePlayback source = playback(); if (source == null) { Slog.w(TAG, "Local playback device not available"); invokeCallback(callback, HdmiControlManager.RESULT_SOURCE_NOT_AVAILABLE); return; } source.oneTouchPlay(callback); } @ServiceThreadOnly private void queryDisplayStatus(final IHdmiControlCallback callback) { assertRunOnServiceThread(); HdmiCecLocalDevicePlayback source = playback(); if (source == null) { Slog.w(TAG, "Local playback device not available"); invokeCallback(callback, HdmiControlManager.RESULT_SOURCE_NOT_AVAILABLE); return; } source.queryDisplayStatus(callback); } private void addHotplugEventListener(final IHdmiHotplugEventListener listener) { final HotplugEventListenerRecord record = new HotplugEventListenerRecord(listener); try { listener.asBinder().linkToDeath(record, 0); } catch (RemoteException e) { Slog.w(TAG, "Listener already died"); return; } synchronized (mLock) { mHotplugEventListenerRecords.add(record); } // Inform the listener of the initial state of each HDMI port by generating // hotplug events. runOnServiceThread(new Runnable() { @Override public void run() { synchronized (mLock) { if (!mHotplugEventListenerRecords.contains(record)) return; } for (HdmiPortInfo port : mPortInfo) { HdmiHotplugEvent event = new HdmiHotplugEvent(port.getId(), mCecController.isConnected(port.getId())); synchronized (mLock) { invokeHotplugEventListenerLocked(listener, event); } } } }); } private void removeHotplugEventListener(IHdmiHotplugEventListener listener) { synchronized (mLock) { for (HotplugEventListenerRecord record : mHotplugEventListenerRecords) { if (record.mListener.asBinder() == listener.asBinder()) { listener.asBinder().unlinkToDeath(record, 0); mHotplugEventListenerRecords.remove(record); break; } } } } private void addDeviceEventListener(IHdmiDeviceEventListener listener) { DeviceEventListenerRecord record = new DeviceEventListenerRecord(listener); try { listener.asBinder().linkToDeath(record, 0); } catch (RemoteException e) { Slog.w(TAG, "Listener already died"); return; } synchronized (mLock) { mDeviceEventListenerRecords.add(record); } } void invokeDeviceEventListeners(HdmiDeviceInfo device, int status) { synchronized (mLock) { for (DeviceEventListenerRecord record : mDeviceEventListenerRecords) { try { record.mListener.onStatusChanged(device, status); } catch (RemoteException e) { Slog.e(TAG, "Failed to report device event:" + e); } } } } private void addSystemAudioModeChangeListner(IHdmiSystemAudioModeChangeListener listener) { SystemAudioModeChangeListenerRecord record = new SystemAudioModeChangeListenerRecord( listener); try { listener.asBinder().linkToDeath(record, 0); } catch (RemoteException e) { Slog.w(TAG, "Listener already died"); return; } synchronized (mLock) { mSystemAudioModeChangeListenerRecords.add(record); } } private void removeSystemAudioModeChangeListener(IHdmiSystemAudioModeChangeListener listener) { synchronized (mLock) { for (SystemAudioModeChangeListenerRecord record : mSystemAudioModeChangeListenerRecords) { if (record.mListener.asBinder() == listener) { listener.asBinder().unlinkToDeath(record, 0); mSystemAudioModeChangeListenerRecords.remove(record); break; } } } } private final class InputChangeListenerRecord implements IBinder.DeathRecipient { private final IHdmiInputChangeListener mListener; public InputChangeListenerRecord(IHdmiInputChangeListener listener) { mListener = listener; } @Override public void binderDied() { synchronized (mLock) { mInputChangeListenerRecord = null; } } } private void setInputChangeListener(IHdmiInputChangeListener listener) { synchronized (mLock) { mInputChangeListenerRecord = new InputChangeListenerRecord(listener); try { listener.asBinder().linkToDeath(mInputChangeListenerRecord, 0); } catch (RemoteException e) { Slog.w(TAG, "Listener already died"); return; } } } void invokeInputChangeListener(HdmiDeviceInfo info) { synchronized (mLock) { if (mInputChangeListenerRecord != null) { try { mInputChangeListenerRecord.mListener.onChanged(info); } catch (RemoteException e) { Slog.w(TAG, "Exception thrown by IHdmiInputChangeListener: " + e); } } } } private void setHdmiRecordListener(IHdmiRecordListener listener) { synchronized (mLock) { mRecordListenerRecord = new HdmiRecordListenerRecord(listener); try { listener.asBinder().linkToDeath(mRecordListenerRecord, 0); } catch (RemoteException e) { Slog.w(TAG, "Listener already died.", e); } } } byte[] invokeRecordRequestListener(int recorderAddress) { synchronized (mLock) { if (mRecordListenerRecord != null) { try { return mRecordListenerRecord.mListener.getOneTouchRecordSource(recorderAddress); } catch (RemoteException e) { Slog.w(TAG, "Failed to start record.", e); } } return EmptyArray.BYTE; } } void invokeOneTouchRecordResult(int recorderAddress, int result) { synchronized (mLock) { if (mRecordListenerRecord != null) { try { mRecordListenerRecord.mListener.onOneTouchRecordResult(recorderAddress, result); } catch (RemoteException e) { Slog.w(TAG, "Failed to call onOneTouchRecordResult.", e); } } } } void invokeTimerRecordingResult(int recorderAddress, int result) { synchronized (mLock) { if (mRecordListenerRecord != null) { try { mRecordListenerRecord.mListener.onTimerRecordingResult(recorderAddress, result); } catch (RemoteException e) { Slog.w(TAG, "Failed to call onTimerRecordingResult.", e); } } } } void invokeClearTimerRecordingResult(int recorderAddress, int result) { synchronized (mLock) { if (mRecordListenerRecord != null) { try { mRecordListenerRecord.mListener.onClearTimerRecordingResult(recorderAddress, result); } catch (RemoteException e) { Slog.w(TAG, "Failed to call onClearTimerRecordingResult.", e); } } } } private void invokeCallback(IHdmiControlCallback callback, int result) { try { callback.onComplete(result); } catch (RemoteException e) { Slog.e(TAG, "Invoking callback failed:" + e); } } private void invokeSystemAudioModeChangeLocked(IHdmiSystemAudioModeChangeListener listener, boolean enabled) { try { listener.onStatusChanged(enabled); } catch (RemoteException e) { Slog.e(TAG, "Invoking callback failed:" + e); } } private void announceHotplugEvent(int portId, boolean connected) { HdmiHotplugEvent event = new HdmiHotplugEvent(portId, connected); synchronized (mLock) { for (HotplugEventListenerRecord record : mHotplugEventListenerRecords) { invokeHotplugEventListenerLocked(record.mListener, event); } } } private void invokeHotplugEventListenerLocked(IHdmiHotplugEventListener listener, HdmiHotplugEvent event) { try { listener.onReceived(event); } catch (RemoteException e) { Slog.e(TAG, "Failed to report hotplug event:" + event.toString(), e); } } private HdmiCecLocalDeviceTv tv() { return (HdmiCecLocalDeviceTv) mCecController.getLocalDevice(HdmiDeviceInfo.DEVICE_TV); } boolean isTvDevice() { return mLocalDevices.contains(HdmiDeviceInfo.DEVICE_TV); } boolean isTvDeviceEnabled() { return isTvDevice() && tv() != null; } private HdmiCecLocalDevicePlayback playback() { return (HdmiCecLocalDevicePlayback) mCecController.getLocalDevice(HdmiDeviceInfo.DEVICE_PLAYBACK); } AudioManager getAudioManager() { return (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE); } boolean isControlEnabled() { synchronized (mLock) { return mHdmiControlEnabled; } } @ServiceThreadOnly int getPowerStatus() { assertRunOnServiceThread(); return mPowerStatus; } @ServiceThreadOnly boolean isPowerOnOrTransient() { assertRunOnServiceThread(); return mPowerStatus == HdmiControlManager.POWER_STATUS_ON || mPowerStatus == HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON; } @ServiceThreadOnly boolean isPowerStandbyOrTransient() { assertRunOnServiceThread(); return mPowerStatus == HdmiControlManager.POWER_STATUS_STANDBY || mPowerStatus == HdmiControlManager.POWER_STATUS_TRANSIENT_TO_STANDBY; } @ServiceThreadOnly boolean isPowerStandby() { assertRunOnServiceThread(); return mPowerStatus == HdmiControlManager.POWER_STATUS_STANDBY; } @ServiceThreadOnly void wakeUp() { assertRunOnServiceThread(); mWakeUpMessageReceived = true; mPowerManager.wakeUp(SystemClock.uptimeMillis(), "android.server.hdmi:WAKE"); // PowerManger will send the broadcast Intent.ACTION_SCREEN_ON and after this gets // the intent, the sequence will continue at onWakeUp(). } @ServiceThreadOnly void standby() { assertRunOnServiceThread(); mStandbyMessageReceived = true; mPowerManager.goToSleep(SystemClock.uptimeMillis(), PowerManager.GO_TO_SLEEP_REASON_HDMI, 0); // PowerManger will send the broadcast Intent.ACTION_SCREEN_OFF and after this gets // the intent, the sequence will continue at onStandby(). } @ServiceThreadOnly private void onWakeUp() { assertRunOnServiceThread(); mPowerStatus = HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON; if (mCecController != null) { if (mHdmiControlEnabled) { int startReason = INITIATED_BY_SCREEN_ON; if (mWakeUpMessageReceived) { startReason = INITIATED_BY_WAKE_UP_MESSAGE; } initializeCec(startReason); } } else { Slog.i(TAG, "Device does not support HDMI-CEC."); } // TODO: Initialize MHL local devices. } @ServiceThreadOnly private void onStandby(final int standbyAction) { assertRunOnServiceThread(); if (!canGoToStandby()) return; mPowerStatus = HdmiControlManager.POWER_STATUS_TRANSIENT_TO_STANDBY; invokeVendorCommandListenersOnControlStateChanged(false, HdmiControlManager.CONTROL_STATE_CHANGED_REASON_STANDBY); final List devices = getAllLocalDevices(); disableDevices(new PendingActionClearedCallback() { @Override public void onCleared(HdmiCecLocalDevice device) { Slog.v(TAG, "On standby-action cleared:" + device.mDeviceType); devices.remove(device); if (devices.isEmpty()) { onStandbyCompleted(standbyAction); // We will not clear local devices here, since some OEM/SOC will keep passing // the received packets until the application processor enters to the sleep // actually. } } }); } private boolean canGoToStandby() { for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) { if (!device.canGoToStandby()) return false; } return true; } @ServiceThreadOnly private void onLanguageChanged(String language) { assertRunOnServiceThread(); mLanguage = language; if (isTvDeviceEnabled()) { tv().broadcastMenuLanguage(language); mCecController.setOption(OPTION_CEC_SET_LANGUAGE, HdmiUtils.languageToInt(language)); } } @ServiceThreadOnly String getLanguage() { assertRunOnServiceThread(); return mLanguage; } private void disableDevices(PendingActionClearedCallback callback) { if (mCecController != null) { for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) { device.disableDevice(mStandbyMessageReceived, callback); } } mMhlController.clearAllLocalDevices(); } @ServiceThreadOnly private void clearLocalDevices() { assertRunOnServiceThread(); if (mCecController == null) { return; } mCecController.clearLogicalAddress(); mCecController.clearLocalDevices(); } @ServiceThreadOnly private void onStandbyCompleted(int standbyAction) { assertRunOnServiceThread(); Slog.v(TAG, "onStandbyCompleted"); if (mPowerStatus != HdmiControlManager.POWER_STATUS_TRANSIENT_TO_STANDBY) { return; } mPowerStatus = HdmiControlManager.POWER_STATUS_STANDBY; for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) { device.onStandby(mStandbyMessageReceived, standbyAction); } mStandbyMessageReceived = false; mAddressAllocated = false; mCecController.setOption(OPTION_CEC_SERVICE_CONTROL, DISABLED); mMhlController.setOption(OPTION_MHL_SERVICE_CONTROL, DISABLED); } private void addVendorCommandListener(IHdmiVendorCommandListener listener, int deviceType) { VendorCommandListenerRecord record = new VendorCommandListenerRecord(listener, deviceType); try { listener.asBinder().linkToDeath(record, 0); } catch (RemoteException e) { Slog.w(TAG, "Listener already died"); return; } synchronized (mLock) { mVendorCommandListenerRecords.add(record); } } boolean invokeVendorCommandListenersOnReceived(int deviceType, int srcAddress, int destAddress, byte[] params, boolean hasVendorId) { synchronized (mLock) { if (mVendorCommandListenerRecords.isEmpty()) { return false; } for (VendorCommandListenerRecord record : mVendorCommandListenerRecords) { if (record.mDeviceType != deviceType) { continue; } try { record.mListener.onReceived(srcAddress, destAddress, params, hasVendorId); } catch (RemoteException e) { Slog.e(TAG, "Failed to notify vendor command reception", e); } } return true; } } boolean invokeVendorCommandListenersOnControlStateChanged(boolean enabled, int reason) { synchronized (mLock) { if (mVendorCommandListenerRecords.isEmpty()) { return false; } for (VendorCommandListenerRecord record : mVendorCommandListenerRecords) { try { record.mListener.onControlStateChanged(enabled, reason); } catch (RemoteException e) { Slog.e(TAG, "Failed to notify control-state-changed to vendor handler", e); } } return true; } } private void addHdmiMhlVendorCommandListener(IHdmiMhlVendorCommandListener listener) { HdmiMhlVendorCommandListenerRecord record = new HdmiMhlVendorCommandListenerRecord(listener); try { listener.asBinder().linkToDeath(record, 0); } catch (RemoteException e) { Slog.w(TAG, "Listener already died."); return; } synchronized (mLock) { mMhlVendorCommandListenerRecords.add(record); } } void invokeMhlVendorCommandListeners(int portId, int offest, int length, byte[] data) { synchronized (mLock) { for (HdmiMhlVendorCommandListenerRecord record : mMhlVendorCommandListenerRecords) { try { record.mListener.onReceived(portId, offest, length, data); } catch (RemoteException e) { Slog.e(TAG, "Failed to notify MHL vendor command", e); } } } } boolean isProhibitMode() { synchronized (mLock) { return mProhibitMode; } } void setProhibitMode(boolean enabled) { synchronized (mLock) { mProhibitMode = enabled; } } @ServiceThreadOnly void setCecOption(int key, int value) { assertRunOnServiceThread(); mCecController.setOption(key, value); } @ServiceThreadOnly void setControlEnabled(boolean enabled) { assertRunOnServiceThread(); synchronized (mLock) { mHdmiControlEnabled = enabled; } if (enabled) { enableHdmiControlService(); return; } // Call the vendor handler before the service is disabled. invokeVendorCommandListenersOnControlStateChanged(false, HdmiControlManager.CONTROL_STATE_CHANGED_REASON_SETTING); // Post the remained tasks in the service thread again to give the vendor-issued-tasks // a chance to run. runOnServiceThread(new Runnable() { @Override public void run() { disableHdmiControlService(); } }); return; } @ServiceThreadOnly private void enableHdmiControlService() { mCecController.setOption(OPTION_CEC_ENABLE, ENABLED); mMhlController.setOption(OPTION_MHL_ENABLE, ENABLED); initializeCec(INITIATED_BY_ENABLE_CEC); } @ServiceThreadOnly private void disableHdmiControlService() { disableDevices(new PendingActionClearedCallback() { @Override public void onCleared(HdmiCecLocalDevice device) { assertRunOnServiceThread(); mCecController.flush(new Runnable() { @Override public void run() { mCecController.setOption(OPTION_CEC_ENABLE, DISABLED); mMhlController.setOption(OPTION_MHL_ENABLE, DISABLED); clearLocalDevices(); } }); } }); } @ServiceThreadOnly void setActivePortId(int portId) { assertRunOnServiceThread(); mActivePortId = portId; // Resets last input for MHL, which stays valid only after the MHL device was selected, // and no further switching is done. setLastInputForMhl(Constants.INVALID_PORT_ID); } @ServiceThreadOnly void setLastInputForMhl(int portId) { assertRunOnServiceThread(); mLastInputMhl = portId; } @ServiceThreadOnly int getLastInputForMhl() { assertRunOnServiceThread(); return mLastInputMhl; } /** * Performs input change, routing control for MHL device. * * @param portId MHL port, or the last port to go back to if {@code contentOn} is false * @param contentOn {@code true} if RAP data is content on; otherwise false */ @ServiceThreadOnly void changeInputForMhl(int portId, boolean contentOn) { assertRunOnServiceThread(); if (tv() == null) return; final int lastInput = contentOn ? tv().getActivePortId() : Constants.INVALID_PORT_ID; if (portId != Constants.INVALID_PORT_ID) { tv().doManualPortSwitching(portId, new IHdmiControlCallback.Stub() { @Override public void onComplete(int result) throws RemoteException { // Keep the last input to switch back later when RAP[ContentOff] is received. // This effectively sets the port to invalid one if the switching is for // RAP[ContentOff]. setLastInputForMhl(lastInput); } }); } // MHL device is always directly connected to the port. Update the active port ID to avoid // unnecessary post-routing control task. tv().setActivePortId(portId); // The port is either the MHL-enabled port where the mobile device is connected, or // the last port to go back to when turnoff command is received. Note that the last port // may not be the MHL-enabled one. In this case the device info to be passed to // input change listener should be the one describing the corresponding HDMI port. HdmiMhlLocalDeviceStub device = mMhlController.getLocalDevice(portId); HdmiDeviceInfo info = (device != null) ? device.getInfo() : mPortDeviceMap.get(portId, HdmiDeviceInfo.INACTIVE_DEVICE); invokeInputChangeListener(info); } void setMhlInputChangeEnabled(boolean enabled) { mMhlController.setOption(OPTION_MHL_INPUT_SWITCHING, toInt(enabled)); synchronized (mLock) { mMhlInputChangeEnabled = enabled; } } boolean isMhlInputChangeEnabled() { synchronized (mLock) { return mMhlInputChangeEnabled; } } @ServiceThreadOnly void displayOsd(int messageId) { assertRunOnServiceThread(); Intent intent = new Intent(HdmiControlManager.ACTION_OSD_MESSAGE); intent.putExtra(HdmiControlManager.EXTRA_MESSAGE_ID, messageId); getContext().sendBroadcastAsUser(intent, UserHandle.ALL, HdmiControlService.PERMISSION); } @ServiceThreadOnly void displayOsd(int messageId, int extra) { assertRunOnServiceThread(); Intent intent = new Intent(HdmiControlManager.ACTION_OSD_MESSAGE); intent.putExtra(HdmiControlManager.EXTRA_MESSAGE_ID, messageId); intent.putExtra(HdmiControlManager.EXTRA_MESSAGE_EXTRA_PARAM1, extra); getContext().sendBroadcastAsUser(intent, UserHandle.ALL, HdmiControlService.PERMISSION); } }