All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.com.android.server.hdmi.HdmiCecController Maven / Gradle / Ivy

/*
 * 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 android.annotation.NonNull;
import android.annotation.Nullable;
import android.hardware.hdmi.HdmiPortInfo;
import android.hardware.tv.cec.V1_0.CecMessage;
import android.hardware.tv.cec.V1_0.HotplugEvent;
import android.hardware.tv.cec.V1_0.IHdmiCec;
import android.hardware.tv.cec.V1_0.IHdmiCec.getPhysicalAddressCallback;
import android.hardware.tv.cec.V1_0.IHdmiCecCallback;
import android.hardware.tv.cec.V1_0.Result;
import android.hardware.tv.cec.V1_0.SendMessageResult;
import android.icu.util.IllformedLocaleException;
import android.icu.util.ULocale;
import android.os.Binder;
import android.os.Handler;
import android.os.IHwBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.stats.hdmi.HdmiStatsEnums;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.hdmi.HdmiAnnotations.IoThreadOnly;
import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly;
import com.android.server.hdmi.HdmiControlService.DevicePollingCallback;

import libcore.util.EmptyArray;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.function.Predicate;

/**
 * Manages HDMI-CEC command and behaviors. It converts user's command into CEC command
 * and pass it to CEC HAL so that it sends message to other device. For incoming
 * message it translates the message and delegates it to proper module.
 *
 * 

It should be careful to access member variables on IO thread because * it can be accessed from system thread as well. * *

It can be created only by {@link HdmiCecController#create} * *

Declared as package-private, accessed by {@link HdmiControlService} only. */ final class HdmiCecController { private static final String TAG = "HdmiCecController"; /** * Interface to report allocated logical address. */ interface AllocateAddressCallback { /** * Called when a new logical address is allocated. * * @param deviceType requested device type to allocate logical address * @param logicalAddress allocated logical address. If it is * {@link Constants#ADDR_UNREGISTERED}, it means that * it failed to allocate logical address for the given device type */ void onAllocated(int deviceType, int logicalAddress); } private static final byte[] EMPTY_BODY = EmptyArray.BYTE; private static final int NUM_LOGICAL_ADDRESS = 16; private static final int MAX_DEDICATED_ADDRESS = 11; private static final int MAX_HDMI_MESSAGE_HISTORY = 250; private static final int INVALID_PHYSICAL_ADDRESS = 0xFFFF; /** Cookie for matching the right end point. */ protected static final int HDMI_CEC_HAL_DEATH_COOKIE = 353; // Predicate for whether the given logical address is remote device's one or not. private final Predicate mRemoteDeviceAddressPredicate = new Predicate() { @Override public boolean test(Integer address) { return !mService.getHdmiCecNetwork().isAllocatedLocalDeviceAddress(address); } }; // Predicate whether the given logical address is system audio's one or not private final Predicate mSystemAudioAddressPredicate = new Predicate() { @Override public boolean test(Integer address) { return HdmiUtils.isEligibleAddressForDevice(Constants.ADDR_AUDIO_SYSTEM, address); } }; // Handler instance to process synchronous I/O (mainly send) message. private Handler mIoHandler; // Handler instance to process various messages coming from other CEC // device or issued by internal state change. private Handler mControlHandler; private final HdmiControlService mService; // Stores recent CEC messages and HDMI Hotplug event history for debugging purpose. private final ArrayBlockingQueue mMessageHistory = new ArrayBlockingQueue<>(MAX_HDMI_MESSAGE_HISTORY); private final NativeWrapper mNativeWrapperImpl; private final HdmiCecAtomWriter mHdmiCecAtomWriter; // Private constructor. Use HdmiCecController.create(). private HdmiCecController( HdmiControlService service, NativeWrapper nativeWrapper, HdmiCecAtomWriter atomWriter) { mService = service; mNativeWrapperImpl = nativeWrapper; mHdmiCecAtomWriter = atomWriter; } /** * A factory method to get {@link HdmiCecController}. If it fails to initialize * inner device or has no device it will return {@code null}. * *

Declared as package-private, accessed by {@link HdmiControlService} only. * @param service {@link HdmiControlService} instance used to create internal handler * and to pass callback for incoming message or event. * @param atomWriter {@link HdmiCecAtomWriter} instance for writing atoms for metrics. * @return {@link HdmiCecController} if device is initialized successfully. Otherwise, * returns {@code null}. */ static HdmiCecController create(HdmiControlService service, HdmiCecAtomWriter atomWriter) { HdmiCecController controller = createWithNativeWrapper(service, new NativeWrapperImpl11(), atomWriter); if (controller != null) { return controller; } HdmiLogger.warning("Unable to use [email protected]"); return createWithNativeWrapper(service, new NativeWrapperImpl(), atomWriter); } /** * A factory method with injection of native methods for testing. */ static HdmiCecController createWithNativeWrapper( HdmiControlService service, NativeWrapper nativeWrapper, HdmiCecAtomWriter atomWriter) { HdmiCecController controller = new HdmiCecController(service, nativeWrapper, atomWriter); String nativePtr = nativeWrapper.nativeInit(); if (nativePtr == null) { HdmiLogger.warning("Couldn't get tv.cec service."); return null; } controller.init(nativeWrapper); return controller; } private void init(NativeWrapper nativeWrapper) { mIoHandler = new Handler(mService.getIoLooper()); mControlHandler = new Handler(mService.getServiceLooper()); nativeWrapper.setCallback(new HdmiCecCallback()); } /** * Allocate a new logical address of the given device type. Allocated * address will be reported through {@link AllocateAddressCallback}. * *

Declared as package-private, accessed by {@link HdmiControlService} only. * * @param deviceType type of device to used to determine logical address * @param preferredAddress a logical address preferred to be allocated. * If sets {@link Constants#ADDR_UNREGISTERED}, scans * the smallest logical address matched with the given device type. * Otherwise, scan address will start from {@code preferredAddress} * @param callback callback interface to report allocated logical address to caller */ @ServiceThreadOnly void allocateLogicalAddress(final int deviceType, final int preferredAddress, final AllocateAddressCallback callback) { assertRunOnServiceThread(); runOnIoThread(new Runnable() { @Override public void run() { handleAllocateLogicalAddress(deviceType, preferredAddress, callback); } }); } /** * Address allocation will check the following addresses (in order): *

    *
  • Given preferred logical address (if the address is valid for the given device * type)
  • *
  • All dedicated logical addresses for the given device type
  • *
  • Backup addresses, if valid for the given device type
  • *
*/ @IoThreadOnly private void handleAllocateLogicalAddress(final int deviceType, int preferredAddress, final AllocateAddressCallback callback) { assertRunOnIoThread(); List logicalAddressesToPoll = new ArrayList<>(); if (HdmiUtils.isEligibleAddressForDevice(deviceType, preferredAddress)) { logicalAddressesToPoll.add(preferredAddress); } for (int i = 0; i < NUM_LOGICAL_ADDRESS; ++i) { if (!logicalAddressesToPoll.contains(i) && HdmiUtils.isEligibleAddressForDevice( deviceType, i) && HdmiUtils.isEligibleAddressForCecVersion( mService.getCecVersion(), i)) { logicalAddressesToPoll.add(i); } } int logicalAddress = Constants.ADDR_UNREGISTERED; for (Integer logicalAddressToPoll : logicalAddressesToPoll) { boolean acked = false; for (int j = 0; j < HdmiConfig.ADDRESS_ALLOCATION_RETRY; ++j) { if (sendPollMessage(logicalAddressToPoll, logicalAddressToPoll, 1)) { acked = true; break; } } // If sending failed, it becomes new logical address for the // device because no device uses it as logical address of the device. if (!acked) { logicalAddress = logicalAddressToPoll; break; } } final int assignedAddress = logicalAddress; HdmiLogger.debug("New logical address for device [%d]: [preferred:%d, assigned:%d]", deviceType, preferredAddress, assignedAddress); if (callback != null) { runOnServiceThread(new Runnable() { @Override public void run() { callback.onAllocated(deviceType, assignedAddress); } }); } } private static byte[] buildBody(int opcode, byte[] params) { byte[] body = new byte[params.length + 1]; body[0] = (byte) opcode; System.arraycopy(params, 0, body, 1, params.length); return body; } HdmiPortInfo[] getPortInfos() { return mNativeWrapperImpl.nativeGetPortInfos(); } /** * Add a new logical address to the device. Device's HW should be notified * when a new logical address is assigned to a device, so that it can accept * a command having available destinations. * *

Declared as package-private. accessed by {@link HdmiControlService} only. * * @param newLogicalAddress a logical address to be added * @return 0 on success. Otherwise, returns negative value */ @ServiceThreadOnly int addLogicalAddress(int newLogicalAddress) { assertRunOnServiceThread(); if (HdmiUtils.isValidAddress(newLogicalAddress)) { return mNativeWrapperImpl.nativeAddLogicalAddress(newLogicalAddress); } else { return Result.FAILURE_INVALID_ARGS; } } /** * Clear all logical addresses registered in the device. * *

Declared as package-private. accessed by {@link HdmiControlService} only. */ @ServiceThreadOnly void clearLogicalAddress() { assertRunOnServiceThread(); mNativeWrapperImpl.nativeClearLogicalAddress(); } /** * Return the physical address of the device. * *

Declared as package-private. accessed by {@link HdmiControlService} only. * * @return CEC physical address of the device. The range of success address * is between 0x0000 and 0xFFFF. If failed it returns -1 */ @ServiceThreadOnly int getPhysicalAddress() { assertRunOnServiceThread(); return mNativeWrapperImpl.nativeGetPhysicalAddress(); } /** * Return highest CEC version supported by this device. * *

Declared as package-private. accessed by {@link HdmiControlService} only. */ @ServiceThreadOnly int getVersion() { assertRunOnServiceThread(); return mNativeWrapperImpl.nativeGetVersion(); } /** * Return vendor id of the device. * *

Declared as package-private. accessed by {@link HdmiControlService} only. */ @ServiceThreadOnly int getVendorId() { assertRunOnServiceThread(); return mNativeWrapperImpl.nativeGetVendorId(); } /** * Set an option to CEC HAL. * * @param flag key of option * @param enabled whether to enable/disable the given option. */ @ServiceThreadOnly void setOption(int flag, boolean enabled) { assertRunOnServiceThread(); HdmiLogger.debug("setOption: [flag:%d, enabled:%b]", flag, enabled); mNativeWrapperImpl.nativeSetOption(flag, enabled); } /** * Informs CEC HAL about the current system language. * * @param language Three-letter code defined in ISO/FDIS 639-2. Must be lowercase letters. */ @ServiceThreadOnly void setLanguage(String language) { assertRunOnServiceThread(); if (!isLanguage(language)) { return; } mNativeWrapperImpl.nativeSetLanguage(language); } /** * Returns true if the language code is well-formed. */ @VisibleForTesting static boolean isLanguage(String language) { // Handle null and empty string because because ULocale.Builder#setLanguage accepts them. if (language == null || language.isEmpty()) { return false; } ULocale.Builder builder = new ULocale.Builder(); try { builder.setLanguage(language); return true; } catch (IllformedLocaleException e) { return false; } } /** * Configure ARC circuit in the hardware logic to start or stop the feature. * * @param port ID of HDMI port to which AVR is connected * @param enabled whether to enable/disable ARC */ @ServiceThreadOnly void enableAudioReturnChannel(int port, boolean enabled) { assertRunOnServiceThread(); mNativeWrapperImpl.nativeEnableAudioReturnChannel(port, enabled); } /** * Return the connection status of the specified port * * @param port port number to check connection status * @return true if connected; otherwise, return false */ @ServiceThreadOnly boolean isConnected(int port) { assertRunOnServiceThread(); return mNativeWrapperImpl.nativeIsConnected(port); } /** * Poll all remote devices. It sends <Polling Message> to all remote * devices. * *

Declared as package-private. accessed by {@link HdmiControlService} only. * * @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 */ @ServiceThreadOnly void pollDevices(DevicePollingCallback callback, int sourceAddress, int pickStrategy, int retryCount) { assertRunOnServiceThread(); // Extract polling candidates. No need to poll against local devices. List pollingCandidates = pickPollCandidates(pickStrategy); ArrayList allocated = new ArrayList<>(); runDevicePolling(sourceAddress, pollingCandidates, retryCount, callback, allocated); } private List pickPollCandidates(int pickStrategy) { int strategy = pickStrategy & Constants.POLL_STRATEGY_MASK; Predicate pickPredicate = null; switch (strategy) { case Constants.POLL_STRATEGY_SYSTEM_AUDIO: pickPredicate = mSystemAudioAddressPredicate; break; case Constants.POLL_STRATEGY_REMOTES_DEVICES: default: // The default is POLL_STRATEGY_REMOTES_DEVICES. pickPredicate = mRemoteDeviceAddressPredicate; break; } int iterationStrategy = pickStrategy & Constants.POLL_ITERATION_STRATEGY_MASK; LinkedList pollingCandidates = new LinkedList<>(); switch (iterationStrategy) { case Constants.POLL_ITERATION_IN_ORDER: for (int i = Constants.ADDR_TV; i <= Constants.ADDR_SPECIFIC_USE; ++i) { if (pickPredicate.test(i)) { pollingCandidates.add(i); } } break; case Constants.POLL_ITERATION_REVERSE_ORDER: default: // The default is reverse order. for (int i = Constants.ADDR_SPECIFIC_USE; i >= Constants.ADDR_TV; --i) { if (pickPredicate.test(i)) { pollingCandidates.add(i); } } break; } return pollingCandidates; } @ServiceThreadOnly private void runDevicePolling(final int sourceAddress, final List candidates, final int retryCount, final DevicePollingCallback callback, final List allocated) { assertRunOnServiceThread(); if (candidates.isEmpty()) { if (callback != null) { HdmiLogger.debug("[P]:AllocatedAddress=%s", allocated.toString()); callback.onPollingFinished(allocated); } return; } final Integer candidate = candidates.remove(0); // Proceed polling action for the next address once polling action for the // previous address is done. runOnIoThread(new Runnable() { @Override public void run() { if (sendPollMessage(sourceAddress, candidate, retryCount)) { allocated.add(candidate); } runOnServiceThread(new Runnable() { @Override public void run() { runDevicePolling(sourceAddress, candidates, retryCount, callback, allocated); } }); } }); } @IoThreadOnly private boolean sendPollMessage(int sourceAddress, int destinationAddress, int retryCount) { assertRunOnIoThread(); for (int i = 0; i < retryCount; ++i) { // is a message which has empty body. int ret = mNativeWrapperImpl.nativeSendCecCommand( sourceAddress, destinationAddress, EMPTY_BODY); if (ret == SendMessageResult.SUCCESS) { return true; } else if (ret != SendMessageResult.NACK) { // Unusual failure HdmiLogger.warning("Failed to send a polling message(%d->%d) with return code %d", sourceAddress, destinationAddress, ret); } } return false; } private void assertRunOnIoThread() { if (Looper.myLooper() != mIoHandler.getLooper()) { throw new IllegalStateException("Should run on io thread."); } } private void assertRunOnServiceThread() { if (Looper.myLooper() != mControlHandler.getLooper()) { throw new IllegalStateException("Should run on service thread."); } } // Run a Runnable on IO thread. // It should be careful to access member variables on IO thread because // it can be accessed from system thread as well. @VisibleForTesting void runOnIoThread(Runnable runnable) { mIoHandler.post(new WorkSourceUidPreservingRunnable(runnable)); } @VisibleForTesting void runOnServiceThread(Runnable runnable) { mControlHandler.post(new WorkSourceUidPreservingRunnable(runnable)); } @ServiceThreadOnly void flush(final Runnable runnable) { assertRunOnServiceThread(); runOnIoThread(new Runnable() { @Override public void run() { // This ensures the runnable for cleanup is performed after all the pending // commands are processed by IO thread. runOnServiceThread(runnable); } }); } private boolean isAcceptableAddress(int address) { // Can access command targeting devices available in local device or broadcast command. if (address == Constants.ADDR_BROADCAST) { return true; } return mService.getHdmiCecNetwork().isAllocatedLocalDeviceAddress(address); } @ServiceThreadOnly @VisibleForTesting void onReceiveCommand(HdmiCecMessage message) { assertRunOnServiceThread(); if (mService.isAddressAllocated() && !isAcceptableAddress(message.getDestination())) { return; } @Constants.HandleMessageResult int messageState = mService.handleCecCommand(message); if (messageState == Constants.NOT_HANDLED) { // Message was not handled maySendFeatureAbortCommand(message, Constants.ABORT_UNRECOGNIZED_OPCODE); } else if (messageState != Constants.HANDLED) { // Message handler wants to send a feature abort maySendFeatureAbortCommand(message, messageState); } } @ServiceThreadOnly void maySendFeatureAbortCommand(HdmiCecMessage message, @Constants.AbortReason int reason) { assertRunOnServiceThread(); // Swap the source and the destination. int src = message.getDestination(); int dest = message.getSource(); if (src == Constants.ADDR_BROADCAST || dest == Constants.ADDR_UNREGISTERED) { // Don't reply from the unregistered devices or for the broadcasted // messages. See CEC 12.2 Protocol General Rules for detail. return; } int originalOpcode = message.getOpcode(); if (originalOpcode == Constants.MESSAGE_FEATURE_ABORT) { return; } sendCommand( HdmiCecMessageBuilder.buildFeatureAbortCommand(src, dest, originalOpcode, reason)); } @ServiceThreadOnly void sendCommand(HdmiCecMessage cecMessage) { assertRunOnServiceThread(); sendCommand(cecMessage, null); } /** * Returns the calling UID of the original Binder call that triggered this code. * If this code was not triggered by a Binder call, returns the UID of this process. */ private int getCallingUid() { int workSourceUid = Binder.getCallingWorkSourceUid(); if (workSourceUid == -1) { return Binder.getCallingUid(); } return workSourceUid; } @ServiceThreadOnly void sendCommand(final HdmiCecMessage cecMessage, final HdmiControlService.SendMessageCallback callback) { assertRunOnServiceThread(); addCecMessageToHistory(false /* isReceived */, cecMessage); runOnIoThread(new Runnable() { @Override public void run() { HdmiLogger.debug("[S]:" + cecMessage); byte[] body = buildBody(cecMessage.getOpcode(), cecMessage.getParams()); int retransmissionCount = 0; int errorCode = SendMessageResult.SUCCESS; do { errorCode = mNativeWrapperImpl.nativeSendCecCommand( cecMessage.getSource(), cecMessage.getDestination(), body); if (errorCode == SendMessageResult.SUCCESS) { break; } } while (retransmissionCount++ < HdmiConfig.RETRANSMISSION_COUNT); final int finalError = errorCode; if (finalError != SendMessageResult.SUCCESS) { Slog.w(TAG, "Failed to send " + cecMessage + " with errorCode=" + finalError); } runOnServiceThread(new Runnable() { @Override public void run() { mHdmiCecAtomWriter.messageReported( cecMessage, FrameworkStatsLog.HDMI_CEC_MESSAGE_REPORTED__DIRECTION__OUTGOING, getCallingUid(), finalError ); if (callback != null) { callback.onSendCompleted(finalError); } } }); } }); } /** * Called when incoming CEC message arrived. */ @ServiceThreadOnly private void handleIncomingCecCommand(int srcAddress, int dstAddress, byte[] body) { assertRunOnServiceThread(); HdmiCecMessage command = HdmiCecMessageBuilder.of(srcAddress, dstAddress, body); HdmiLogger.debug("[R]:" + command); addCecMessageToHistory(true /* isReceived */, command); mHdmiCecAtomWriter.messageReported(command, incomingMessageDirection(srcAddress, dstAddress), getCallingUid()); onReceiveCommand(command); } /** * Computes the direction of an incoming message, as implied by the source and * destination addresses. This will usually return INCOMING; if not, it can indicate a bug. */ private int incomingMessageDirection(int srcAddress, int dstAddress) { boolean sourceIsLocal = false; boolean destinationIsLocal = dstAddress == Constants.ADDR_BROADCAST; for (HdmiCecLocalDevice localDevice : mService.getHdmiCecNetwork().getLocalDeviceList()) { int logicalAddress = localDevice.getDeviceInfo().getLogicalAddress(); if (logicalAddress == srcAddress) { sourceIsLocal = true; } if (logicalAddress == dstAddress) { destinationIsLocal = true; } } if (!sourceIsLocal && destinationIsLocal) { return HdmiStatsEnums.INCOMING; } else if (sourceIsLocal && destinationIsLocal) { return HdmiStatsEnums.TO_SELF; } return HdmiStatsEnums.MESSAGE_DIRECTION_OTHER; } /** * Called when a hotplug event issues. */ @ServiceThreadOnly private void handleHotplug(int port, boolean connected) { assertRunOnServiceThread(); HdmiLogger.debug("Hotplug event:[port:%d, connected:%b]", port, connected); addHotplugEventToHistory(port, connected); mService.onHotplug(port, connected); } @ServiceThreadOnly private void addHotplugEventToHistory(int port, boolean connected) { assertRunOnServiceThread(); addEventToHistory(new HotplugHistoryRecord(port, connected)); } @ServiceThreadOnly private void addCecMessageToHistory(boolean isReceived, HdmiCecMessage message) { assertRunOnServiceThread(); addEventToHistory(new MessageHistoryRecord(isReceived, message)); } private void addEventToHistory(Dumpable event) { if (!mMessageHistory.offer(event)) { mMessageHistory.poll(); mMessageHistory.offer(event); } } void dump(final IndentingPrintWriter pw) { pw.println("CEC message history:"); pw.increaseIndent(); final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); for (Dumpable record : mMessageHistory) { record.dump(pw, sdf); } pw.decreaseIndent(); } protected interface NativeWrapper { String nativeInit(); void setCallback(HdmiCecCallback callback); int nativeSendCecCommand(int srcAddress, int dstAddress, byte[] body); int nativeAddLogicalAddress(int logicalAddress); void nativeClearLogicalAddress(); int nativeGetPhysicalAddress(); int nativeGetVersion(); int nativeGetVendorId(); HdmiPortInfo[] nativeGetPortInfos(); void nativeSetOption(int flag, boolean enabled); void nativeSetLanguage(String language); void nativeEnableAudioReturnChannel(int port, boolean flag); boolean nativeIsConnected(int port); } private static final class NativeWrapperImpl11 implements NativeWrapper, IHwBinder.DeathRecipient, getPhysicalAddressCallback { private android.hardware.tv.cec.V1_1.IHdmiCec mHdmiCec; @Nullable private HdmiCecCallback mCallback; private final Object mLock = new Object(); private int mPhysicalAddress = INVALID_PHYSICAL_ADDRESS; @Override public String nativeInit() { return (connectToHal() ? mHdmiCec.toString() : null); } boolean connectToHal() { try { mHdmiCec = android.hardware.tv.cec.V1_1.IHdmiCec.getService(true); try { mHdmiCec.linkToDeath(this, HDMI_CEC_HAL_DEATH_COOKIE); } catch (RemoteException e) { HdmiLogger.error("Couldn't link to death : ", e); } } catch (RemoteException | NoSuchElementException e) { HdmiLogger.error("Couldn't connect to [email protected]", e); return false; } return true; } @Override public void onValues(int result, short addr) { if (result == Result.SUCCESS) { synchronized (mLock) { mPhysicalAddress = new Short(addr).intValue(); } } } @Override public void serviceDied(long cookie) { if (cookie == HDMI_CEC_HAL_DEATH_COOKIE) { HdmiLogger.error("Service died cookie : " + cookie + "; reconnecting"); connectToHal(); // Reconnect the callback if (mCallback != null) { setCallback(mCallback); } } } @Override public void setCallback(HdmiCecCallback callback) { mCallback = callback; try { mHdmiCec.setCallback_1_1(new HdmiCecCallback11(callback)); } catch (RemoteException e) { HdmiLogger.error("Couldn't initialise tv.cec callback : ", e); } } @Override public int nativeSendCecCommand(int srcAddress, int dstAddress, byte[] body) { android.hardware.tv.cec.V1_1.CecMessage message = new android.hardware.tv.cec.V1_1.CecMessage(); message.initiator = srcAddress; message.destination = dstAddress; message.body = new ArrayList<>(body.length); for (byte b : body) { message.body.add(b); } try { return mHdmiCec.sendMessage_1_1(message); } catch (RemoteException e) { HdmiLogger.error("Failed to send CEC message : ", e); return SendMessageResult.FAIL; } } @Override public int nativeAddLogicalAddress(int logicalAddress) { try { return mHdmiCec.addLogicalAddress_1_1(logicalAddress); } catch (RemoteException e) { HdmiLogger.error("Failed to add a logical address : ", e); return Result.FAILURE_INVALID_ARGS; } } @Override public void nativeClearLogicalAddress() { try { mHdmiCec.clearLogicalAddress(); } catch (RemoteException e) { HdmiLogger.error("Failed to clear logical address : ", e); } } @Override public int nativeGetPhysicalAddress() { try { mHdmiCec.getPhysicalAddress(this); return mPhysicalAddress; } catch (RemoteException e) { HdmiLogger.error("Failed to get physical address : ", e); return INVALID_PHYSICAL_ADDRESS; } } @Override public int nativeGetVersion() { try { return mHdmiCec.getCecVersion(); } catch (RemoteException e) { HdmiLogger.error("Failed to get cec version : ", e); return Result.FAILURE_UNKNOWN; } } @Override public int nativeGetVendorId() { try { return mHdmiCec.getVendorId(); } catch (RemoteException e) { HdmiLogger.error("Failed to get vendor id : ", e); return Result.FAILURE_UNKNOWN; } } @Override public HdmiPortInfo[] nativeGetPortInfos() { try { ArrayList hdmiPortInfos = mHdmiCec.getPortInfo(); HdmiPortInfo[] hdmiPortInfo = new HdmiPortInfo[hdmiPortInfos.size()]; int i = 0; for (android.hardware.tv.cec.V1_0.HdmiPortInfo portInfo : hdmiPortInfos) { hdmiPortInfo[i] = new HdmiPortInfo(portInfo.portId, portInfo.type, portInfo.physicalAddress, portInfo.cecSupported, false, portInfo.arcSupported); i++; } return hdmiPortInfo; } catch (RemoteException e) { HdmiLogger.error("Failed to get port information : ", e); return null; } } @Override public void nativeSetOption(int flag, boolean enabled) { try { mHdmiCec.setOption(flag, enabled); } catch (RemoteException e) { HdmiLogger.error("Failed to set option : ", e); } } @Override public void nativeSetLanguage(String language) { try { mHdmiCec.setLanguage(language); } catch (RemoteException e) { HdmiLogger.error("Failed to set language : ", e); } } @Override public void nativeEnableAudioReturnChannel(int port, boolean flag) { try { mHdmiCec.enableAudioReturnChannel(port, flag); } catch (RemoteException e) { HdmiLogger.error("Failed to enable/disable ARC : ", e); } } @Override public boolean nativeIsConnected(int port) { try { return mHdmiCec.isConnected(port); } catch (RemoteException e) { HdmiLogger.error("Failed to get connection info : ", e); return false; } } } private static final class NativeWrapperImpl implements NativeWrapper, IHwBinder.DeathRecipient, getPhysicalAddressCallback { private android.hardware.tv.cec.V1_0.IHdmiCec mHdmiCec; @Nullable private HdmiCecCallback mCallback; private final Object mLock = new Object(); private int mPhysicalAddress = INVALID_PHYSICAL_ADDRESS; @Override public String nativeInit() { return (connectToHal() ? mHdmiCec.toString() : null); } boolean connectToHal() { try { mHdmiCec = IHdmiCec.getService(true); try { mHdmiCec.linkToDeath(this, HDMI_CEC_HAL_DEATH_COOKIE); } catch (RemoteException e) { HdmiLogger.error("Couldn't link to death : ", e); } } catch (RemoteException | NoSuchElementException e) { HdmiLogger.error("Couldn't connect to [email protected]", e); return false; } return true; } @Override public void setCallback(@NonNull HdmiCecCallback callback) { mCallback = callback; try { mHdmiCec.setCallback(new HdmiCecCallback10(callback)); } catch (RemoteException e) { HdmiLogger.error("Couldn't initialise tv.cec callback : ", e); } } @Override public int nativeSendCecCommand(int srcAddress, int dstAddress, byte[] body) { CecMessage message = new CecMessage(); message.initiator = srcAddress; message.destination = dstAddress; message.body = new ArrayList<>(body.length); for (byte b : body) { message.body.add(b); } try { return mHdmiCec.sendMessage(message); } catch (RemoteException e) { HdmiLogger.error("Failed to send CEC message : ", e); return SendMessageResult.FAIL; } } @Override public int nativeAddLogicalAddress(int logicalAddress) { try { return mHdmiCec.addLogicalAddress(logicalAddress); } catch (RemoteException e) { HdmiLogger.error("Failed to add a logical address : ", e); return Result.FAILURE_INVALID_ARGS; } } @Override public void nativeClearLogicalAddress() { try { mHdmiCec.clearLogicalAddress(); } catch (RemoteException e) { HdmiLogger.error("Failed to clear logical address : ", e); } } @Override public int nativeGetPhysicalAddress() { try { mHdmiCec.getPhysicalAddress(this); return mPhysicalAddress; } catch (RemoteException e) { HdmiLogger.error("Failed to get physical address : ", e); return INVALID_PHYSICAL_ADDRESS; } } @Override public int nativeGetVersion() { try { return mHdmiCec.getCecVersion(); } catch (RemoteException e) { HdmiLogger.error("Failed to get cec version : ", e); return Result.FAILURE_UNKNOWN; } } @Override public int nativeGetVendorId() { try { return mHdmiCec.getVendorId(); } catch (RemoteException e) { HdmiLogger.error("Failed to get vendor id : ", e); return Result.FAILURE_UNKNOWN; } } @Override public HdmiPortInfo[] nativeGetPortInfos() { try { ArrayList hdmiPortInfos = mHdmiCec.getPortInfo(); HdmiPortInfo[] hdmiPortInfo = new HdmiPortInfo[hdmiPortInfos.size()]; int i = 0; for (android.hardware.tv.cec.V1_0.HdmiPortInfo portInfo : hdmiPortInfos) { hdmiPortInfo[i] = new HdmiPortInfo(portInfo.portId, portInfo.type, portInfo.physicalAddress, portInfo.cecSupported, false, portInfo.arcSupported); i++; } return hdmiPortInfo; } catch (RemoteException e) { HdmiLogger.error("Failed to get port information : ", e); return null; } } @Override public void nativeSetOption(int flag, boolean enabled) { try { mHdmiCec.setOption(flag, enabled); } catch (RemoteException e) { HdmiLogger.error("Failed to set option : ", e); } } @Override public void nativeSetLanguage(String language) { try { mHdmiCec.setLanguage(language); } catch (RemoteException e) { HdmiLogger.error("Failed to set language : ", e); } } @Override public void nativeEnableAudioReturnChannel(int port, boolean flag) { try { mHdmiCec.enableAudioReturnChannel(port, flag); } catch (RemoteException e) { HdmiLogger.error("Failed to enable/disable ARC : ", e); } } @Override public boolean nativeIsConnected(int port) { try { return mHdmiCec.isConnected(port); } catch (RemoteException e) { HdmiLogger.error("Failed to get connection info : ", e); return false; } } @Override public void serviceDied(long cookie) { if (cookie == HDMI_CEC_HAL_DEATH_COOKIE) { HdmiLogger.error("Service died cookie : " + cookie + "; reconnecting"); connectToHal(); // Reconnect the callback if (mCallback != null) { setCallback(mCallback); } } } @Override public void onValues(int result, short addr) { if (result == Result.SUCCESS) { synchronized (mLock) { mPhysicalAddress = new Short(addr).intValue(); } } } } final class HdmiCecCallback { public void onCecMessage(int initiator, int destination, byte[] body) { runOnServiceThread( () -> handleIncomingCecCommand(initiator, destination, body)); } public void onHotplugEvent(int portId, boolean connected) { runOnServiceThread(() -> handleHotplug(portId, connected)); } } private static final class HdmiCecCallback10 extends IHdmiCecCallback.Stub { private final HdmiCecCallback mHdmiCecCallback; HdmiCecCallback10(HdmiCecCallback hdmiCecCallback) { mHdmiCecCallback = hdmiCecCallback; } @Override public void onCecMessage(CecMessage message) throws RemoteException { byte[] body = new byte[message.body.size()]; for (int i = 0; i < message.body.size(); i++) { body[i] = message.body.get(i); } mHdmiCecCallback.onCecMessage(message.initiator, message.destination, body); } @Override public void onHotplugEvent(HotplugEvent event) throws RemoteException { mHdmiCecCallback.onHotplugEvent(event.portId, event.connected); } } private static final class HdmiCecCallback11 extends android.hardware.tv.cec.V1_1.IHdmiCecCallback.Stub { private final HdmiCecCallback mHdmiCecCallback; HdmiCecCallback11(HdmiCecCallback hdmiCecCallback) { mHdmiCecCallback = hdmiCecCallback; } @Override public void onCecMessage_1_1(android.hardware.tv.cec.V1_1.CecMessage message) throws RemoteException { byte[] body = new byte[message.body.size()]; for (int i = 0; i < message.body.size(); i++) { body[i] = message.body.get(i); } mHdmiCecCallback.onCecMessage(message.initiator, message.destination, body); } @Override public void onCecMessage(CecMessage message) throws RemoteException { byte[] body = new byte[message.body.size()]; for (int i = 0; i < message.body.size(); i++) { body[i] = message.body.get(i); } mHdmiCecCallback.onCecMessage(message.initiator, message.destination, body); } @Override public void onHotplugEvent(HotplugEvent event) throws RemoteException { mHdmiCecCallback.onHotplugEvent(event.portId, event.connected); } } public abstract static class Dumpable { protected final long mTime; Dumpable() { mTime = System.currentTimeMillis(); } abstract void dump(IndentingPrintWriter pw, SimpleDateFormat sdf); } private static final class MessageHistoryRecord extends Dumpable { private final boolean mIsReceived; // true if received message and false if sent message private final HdmiCecMessage mMessage; MessageHistoryRecord(boolean isReceived, HdmiCecMessage message) { super(); mIsReceived = isReceived; mMessage = message; } @Override void dump(final IndentingPrintWriter pw, SimpleDateFormat sdf) { pw.print(mIsReceived ? "[R]" : "[S]"); pw.print(" time="); pw.print(sdf.format(new Date(mTime))); pw.print(" message="); pw.println(mMessage); } } private static final class HotplugHistoryRecord extends Dumpable { private final int mPort; private final boolean mConnected; HotplugHistoryRecord(int port, boolean connected) { super(); mPort = port; mConnected = connected; } @Override void dump(final IndentingPrintWriter pw, SimpleDateFormat sdf) { pw.print("[H]"); pw.print(" time="); pw.print(sdf.format(new Date(mTime))); pw.print(" hotplug port="); pw.print(mPort); pw.print(" connected="); pw.println(mConnected); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy