Home | History | Annotate | Download | only in hdmi
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.server.hdmi;
     18 
     19 import android.hardware.hdmi.HdmiPortInfo;
     20 import android.hardware.tv.cec.V1_0.Result;
     21 import android.hardware.tv.cec.V1_0.SendMessageResult;
     22 import android.os.Handler;
     23 import android.os.Looper;
     24 import android.os.MessageQueue;
     25 import android.util.Slog;
     26 import android.util.SparseArray;
     27 import com.android.internal.util.IndentingPrintWriter;
     28 import com.android.server.hdmi.HdmiAnnotations.IoThreadOnly;
     29 import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly;
     30 import com.android.server.hdmi.HdmiControlService.DevicePollingCallback;
     31 import java.text.SimpleDateFormat;
     32 import java.util.ArrayList;
     33 import java.util.Date;
     34 import java.util.LinkedList;
     35 import java.util.List;
     36 import java.util.function.Predicate;
     37 import java.util.concurrent.ArrayBlockingQueue;
     38 import libcore.util.EmptyArray;
     39 import sun.util.locale.LanguageTag;
     40 
     41 /**
     42  * Manages HDMI-CEC command and behaviors. It converts user's command into CEC command
     43  * and pass it to CEC HAL so that it sends message to other device. For incoming
     44  * message it translates the message and delegates it to proper module.
     45  *
     46  * <p>It should be careful to access member variables on IO thread because
     47  * it can be accessed from system thread as well.
     48  *
     49  * <p>It can be created only by {@link HdmiCecController#create}
     50  *
     51  * <p>Declared as package-private, accessed by {@link HdmiControlService} only.
     52  */
     53 final class HdmiCecController {
     54     private static final String TAG = "HdmiCecController";
     55 
     56     /**
     57      * Interface to report allocated logical address.
     58      */
     59     interface AllocateAddressCallback {
     60         /**
     61          * Called when a new logical address is allocated.
     62          *
     63          * @param deviceType requested device type to allocate logical address
     64          * @param logicalAddress allocated logical address. If it is
     65          *                       {@link Constants#ADDR_UNREGISTERED}, it means that
     66          *                       it failed to allocate logical address for the given device type
     67          */
     68         void onAllocated(int deviceType, int logicalAddress);
     69     }
     70 
     71     private static final byte[] EMPTY_BODY = EmptyArray.BYTE;
     72 
     73     private static final int NUM_LOGICAL_ADDRESS = 16;
     74 
     75     private static final int MAX_CEC_MESSAGE_HISTORY = 20;
     76 
     77     // Predicate for whether the given logical address is remote device's one or not.
     78     private final Predicate<Integer> mRemoteDeviceAddressPredicate = new Predicate<Integer>() {
     79         @Override
     80         public boolean test(Integer address) {
     81             return !isAllocatedLocalDeviceAddress(address);
     82         }
     83     };
     84 
     85     // Predicate whether the given logical address is system audio's one or not
     86     private final Predicate<Integer> mSystemAudioAddressPredicate = new Predicate<Integer>() {
     87         @Override
     88         public boolean test(Integer address) {
     89             return HdmiUtils.getTypeFromAddress(address) == Constants.ADDR_AUDIO_SYSTEM;
     90         }
     91     };
     92 
     93     // Handler instance to process synchronous I/O (mainly send) message.
     94     private Handler mIoHandler;
     95 
     96     // Handler instance to process various messages coming from other CEC
     97     // device or issued by internal state change.
     98     private Handler mControlHandler;
     99 
    100     // Stores the pointer to the native implementation of the service that
    101     // interacts with HAL.
    102     private volatile long mNativePtr;
    103 
    104     private final HdmiControlService mService;
    105 
    106     // Stores the local CEC devices in the system. Device type is used for key.
    107     private final SparseArray<HdmiCecLocalDevice> mLocalDevices = new SparseArray<>();
    108 
    109     // Stores recent CEC messages history for debugging purpose.
    110     private final ArrayBlockingQueue<MessageHistoryRecord> mMessageHistory =
    111             new ArrayBlockingQueue<>(MAX_CEC_MESSAGE_HISTORY);
    112 
    113     // Private constructor.  Use HdmiCecController.create().
    114     private HdmiCecController(HdmiControlService service) {
    115         mService = service;
    116     }
    117 
    118     /**
    119      * A factory method to get {@link HdmiCecController}. If it fails to initialize
    120      * inner device or has no device it will return {@code null}.
    121      *
    122      * <p>Declared as package-private, accessed by {@link HdmiControlService} only.
    123      * @param service {@link HdmiControlService} instance used to create internal handler
    124      *                and to pass callback for incoming message or event.
    125      * @return {@link HdmiCecController} if device is initialized successfully. Otherwise,
    126      *         returns {@code null}.
    127      */
    128     static HdmiCecController create(HdmiControlService service) {
    129         HdmiCecController controller = new HdmiCecController(service);
    130         long nativePtr = nativeInit(controller, service.getServiceLooper().getQueue());
    131         if (nativePtr == 0L) {
    132             controller = null;
    133             return null;
    134         }
    135 
    136         controller.init(nativePtr);
    137         return controller;
    138     }
    139 
    140     private void init(long nativePtr) {
    141         mIoHandler = new Handler(mService.getIoLooper());
    142         mControlHandler = new Handler(mService.getServiceLooper());
    143         mNativePtr = nativePtr;
    144     }
    145 
    146     @ServiceThreadOnly
    147     void addLocalDevice(int deviceType, HdmiCecLocalDevice device) {
    148         assertRunOnServiceThread();
    149         mLocalDevices.put(deviceType, device);
    150     }
    151 
    152     /**
    153      * Allocate a new logical address of the given device type. Allocated
    154      * address will be reported through {@link AllocateAddressCallback}.
    155      *
    156      * <p> Declared as package-private, accessed by {@link HdmiControlService} only.
    157      *
    158      * @param deviceType type of device to used to determine logical address
    159      * @param preferredAddress a logical address preferred to be allocated.
    160      *                         If sets {@link Constants#ADDR_UNREGISTERED}, scans
    161      *                         the smallest logical address matched with the given device type.
    162      *                         Otherwise, scan address will start from {@code preferredAddress}
    163      * @param callback callback interface to report allocated logical address to caller
    164      */
    165     @ServiceThreadOnly
    166     void allocateLogicalAddress(final int deviceType, final int preferredAddress,
    167             final AllocateAddressCallback callback) {
    168         assertRunOnServiceThread();
    169 
    170         runOnIoThread(new Runnable() {
    171             @Override
    172             public void run() {
    173                 handleAllocateLogicalAddress(deviceType, preferredAddress, callback);
    174             }
    175         });
    176     }
    177 
    178     @IoThreadOnly
    179     private void handleAllocateLogicalAddress(final int deviceType, int preferredAddress,
    180             final AllocateAddressCallback callback) {
    181         assertRunOnIoThread();
    182         int startAddress = preferredAddress;
    183         // If preferred address is "unregistered", start address will be the smallest
    184         // address matched with the given device type.
    185         if (preferredAddress == Constants.ADDR_UNREGISTERED) {
    186             for (int i = 0; i < NUM_LOGICAL_ADDRESS; ++i) {
    187                 if (deviceType == HdmiUtils.getTypeFromAddress(i)) {
    188                     startAddress = i;
    189                     break;
    190                 }
    191             }
    192         }
    193 
    194         int logicalAddress = Constants.ADDR_UNREGISTERED;
    195         // Iterates all possible addresses which has the same device type.
    196         for (int i = 0; i < NUM_LOGICAL_ADDRESS; ++i) {
    197             int curAddress = (startAddress + i) % NUM_LOGICAL_ADDRESS;
    198             if (curAddress != Constants.ADDR_UNREGISTERED
    199                     && deviceType == HdmiUtils.getTypeFromAddress(curAddress)) {
    200                 boolean acked = false;
    201                 for (int j = 0; j < HdmiConfig.ADDRESS_ALLOCATION_RETRY; ++j) {
    202                     if (sendPollMessage(curAddress, curAddress, 1)) {
    203                         acked = true;
    204                         break;
    205                     }
    206                 }
    207                 // If sending <Polling Message> failed, it becomes new logical address for the
    208                 // device because no device uses it as logical address of the device.
    209                 if (!acked) {
    210                     logicalAddress = curAddress;
    211                     break;
    212                 }
    213             }
    214         }
    215 
    216         final int assignedAddress = logicalAddress;
    217         HdmiLogger.debug("New logical address for device [%d]: [preferred:%d, assigned:%d]",
    218                         deviceType, preferredAddress, assignedAddress);
    219         if (callback != null) {
    220             runOnServiceThread(new Runnable() {
    221                 @Override
    222                 public void run() {
    223                     callback.onAllocated(deviceType, assignedAddress);
    224                 }
    225             });
    226         }
    227     }
    228 
    229     private static byte[] buildBody(int opcode, byte[] params) {
    230         byte[] body = new byte[params.length + 1];
    231         body[0] = (byte) opcode;
    232         System.arraycopy(params, 0, body, 1, params.length);
    233         return body;
    234     }
    235 
    236 
    237     HdmiPortInfo[] getPortInfos() {
    238         return nativeGetPortInfos(mNativePtr);
    239     }
    240 
    241     /**
    242      * Return the locally hosted logical device of a given type.
    243      *
    244      * @param deviceType logical device type
    245      * @return {@link HdmiCecLocalDevice} instance if the instance of the type is available;
    246      *          otherwise null.
    247      */
    248     HdmiCecLocalDevice getLocalDevice(int deviceType) {
    249         return mLocalDevices.get(deviceType);
    250     }
    251 
    252     /**
    253      * Add a new logical address to the device. Device's HW should be notified
    254      * when a new logical address is assigned to a device, so that it can accept
    255      * a command having available destinations.
    256      *
    257      * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
    258      *
    259      * @param newLogicalAddress a logical address to be added
    260      * @return 0 on success. Otherwise, returns negative value
    261      */
    262     @ServiceThreadOnly
    263     int addLogicalAddress(int newLogicalAddress) {
    264         assertRunOnServiceThread();
    265         if (HdmiUtils.isValidAddress(newLogicalAddress)) {
    266             return nativeAddLogicalAddress(mNativePtr, newLogicalAddress);
    267         } else {
    268             return Result.FAILURE_INVALID_ARGS;
    269         }
    270     }
    271 
    272     /**
    273      * Clear all logical addresses registered in the device.
    274      *
    275      * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
    276      */
    277     @ServiceThreadOnly
    278     void clearLogicalAddress() {
    279         assertRunOnServiceThread();
    280         for (int i = 0; i < mLocalDevices.size(); ++i) {
    281             mLocalDevices.valueAt(i).clearAddress();
    282         }
    283         nativeClearLogicalAddress(mNativePtr);
    284     }
    285 
    286     @ServiceThreadOnly
    287     void clearLocalDevices() {
    288         assertRunOnServiceThread();
    289         mLocalDevices.clear();
    290     }
    291 
    292     /**
    293      * Return the physical address of the device.
    294      *
    295      * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
    296      *
    297      * @return CEC physical address of the device. The range of success address
    298      *         is between 0x0000 and 0xFFFF. If failed it returns -1
    299      */
    300     @ServiceThreadOnly
    301     int getPhysicalAddress() {
    302         assertRunOnServiceThread();
    303         return nativeGetPhysicalAddress(mNativePtr);
    304     }
    305 
    306     /**
    307      * Return CEC version of the device.
    308      *
    309      * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
    310      */
    311     @ServiceThreadOnly
    312     int getVersion() {
    313         assertRunOnServiceThread();
    314         return nativeGetVersion(mNativePtr);
    315     }
    316 
    317     /**
    318      * Return vendor id of the device.
    319      *
    320      * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
    321      */
    322     @ServiceThreadOnly
    323     int getVendorId() {
    324         assertRunOnServiceThread();
    325         return nativeGetVendorId(mNativePtr);
    326     }
    327 
    328     /**
    329      * Set an option to CEC HAL.
    330      *
    331      * @param flag key of option
    332      * @param enabled whether to enable/disable the given option.
    333      */
    334     @ServiceThreadOnly
    335     void setOption(int flag, boolean enabled) {
    336         assertRunOnServiceThread();
    337         HdmiLogger.debug("setOption: [flag:%d, enabled:%b]", flag, enabled);
    338         nativeSetOption(mNativePtr, flag, enabled);
    339     }
    340 
    341     /**
    342      * Informs CEC HAL about the current system language.
    343      *
    344      * @param language Three-letter code defined in ISO/FDIS 639-2. Must be lowercase letters.
    345      */
    346     @ServiceThreadOnly
    347     void setLanguage(String language) {
    348         assertRunOnServiceThread();
    349         if (!LanguageTag.isLanguage(language)) {
    350             return;
    351         }
    352         nativeSetLanguage(mNativePtr, language);
    353     }
    354 
    355     /**
    356      * Configure ARC circuit in the hardware logic to start or stop the feature.
    357      *
    358      * @param port ID of HDMI port to which AVR is connected
    359      * @param enabled whether to enable/disable ARC
    360      */
    361     @ServiceThreadOnly
    362     void enableAudioReturnChannel(int port, boolean enabled) {
    363         assertRunOnServiceThread();
    364         nativeEnableAudioReturnChannel(mNativePtr, port, enabled);
    365     }
    366 
    367     /**
    368      * Return the connection status of the specified port
    369      *
    370      * @param port port number to check connection status
    371      * @return true if connected; otherwise, return false
    372      */
    373     @ServiceThreadOnly
    374     boolean isConnected(int port) {
    375         assertRunOnServiceThread();
    376         return nativeIsConnected(mNativePtr, port);
    377     }
    378 
    379     /**
    380      * Poll all remote devices. It sends &lt;Polling Message&gt; to all remote
    381      * devices.
    382      *
    383      * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
    384      *
    385      * @param callback an interface used to get a list of all remote devices' address
    386      * @param sourceAddress a logical address of source device where sends polling message
    387      * @param pickStrategy strategy how to pick polling candidates
    388      * @param retryCount the number of retry used to send polling message to remote devices
    389      */
    390     @ServiceThreadOnly
    391     void pollDevices(DevicePollingCallback callback, int sourceAddress, int pickStrategy,
    392             int retryCount) {
    393         assertRunOnServiceThread();
    394 
    395         // Extract polling candidates. No need to poll against local devices.
    396         List<Integer> pollingCandidates = pickPollCandidates(pickStrategy);
    397         ArrayList<Integer> allocated = new ArrayList<>();
    398         runDevicePolling(sourceAddress, pollingCandidates, retryCount, callback, allocated);
    399     }
    400 
    401     /**
    402      * Return a list of all {@link HdmiCecLocalDevice}s.
    403      *
    404      * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
    405      */
    406     @ServiceThreadOnly
    407     List<HdmiCecLocalDevice> getLocalDeviceList() {
    408         assertRunOnServiceThread();
    409         return HdmiUtils.sparseArrayToList(mLocalDevices);
    410     }
    411 
    412     private List<Integer> pickPollCandidates(int pickStrategy) {
    413         int strategy = pickStrategy & Constants.POLL_STRATEGY_MASK;
    414         Predicate<Integer> pickPredicate = null;
    415         switch (strategy) {
    416             case Constants.POLL_STRATEGY_SYSTEM_AUDIO:
    417                 pickPredicate = mSystemAudioAddressPredicate;
    418                 break;
    419             case Constants.POLL_STRATEGY_REMOTES_DEVICES:
    420             default:  // The default is POLL_STRATEGY_REMOTES_DEVICES.
    421                 pickPredicate = mRemoteDeviceAddressPredicate;
    422                 break;
    423         }
    424 
    425         int iterationStrategy = pickStrategy & Constants.POLL_ITERATION_STRATEGY_MASK;
    426         LinkedList<Integer> pollingCandidates = new LinkedList<>();
    427         switch (iterationStrategy) {
    428             case Constants.POLL_ITERATION_IN_ORDER:
    429                 for (int i = Constants.ADDR_TV; i <= Constants.ADDR_SPECIFIC_USE; ++i) {
    430                     if (pickPredicate.test(i)) {
    431                         pollingCandidates.add(i);
    432                     }
    433                 }
    434                 break;
    435             case Constants.POLL_ITERATION_REVERSE_ORDER:
    436             default:  // The default is reverse order.
    437                 for (int i = Constants.ADDR_SPECIFIC_USE; i >= Constants.ADDR_TV; --i) {
    438                     if (pickPredicate.test(i)) {
    439                         pollingCandidates.add(i);
    440                     }
    441                 }
    442                 break;
    443         }
    444         return pollingCandidates;
    445     }
    446 
    447     @ServiceThreadOnly
    448     private boolean isAllocatedLocalDeviceAddress(int address) {
    449         assertRunOnServiceThread();
    450         for (int i = 0; i < mLocalDevices.size(); ++i) {
    451             if (mLocalDevices.valueAt(i).isAddressOf(address)) {
    452                 return true;
    453             }
    454         }
    455         return false;
    456     }
    457 
    458     @ServiceThreadOnly
    459     private void runDevicePolling(final int sourceAddress,
    460             final List<Integer> candidates, final int retryCount,
    461             final DevicePollingCallback callback, final List<Integer> allocated) {
    462         assertRunOnServiceThread();
    463         if (candidates.isEmpty()) {
    464             if (callback != null) {
    465                 HdmiLogger.debug("[P]:AllocatedAddress=%s", allocated.toString());
    466                 callback.onPollingFinished(allocated);
    467             }
    468             return;
    469         }
    470 
    471         final Integer candidate = candidates.remove(0);
    472         // Proceed polling action for the next address once polling action for the
    473         // previous address is done.
    474         runOnIoThread(new Runnable() {
    475             @Override
    476             public void run() {
    477                 if (sendPollMessage(sourceAddress, candidate, retryCount)) {
    478                     allocated.add(candidate);
    479                 }
    480                 runOnServiceThread(new Runnable() {
    481                     @Override
    482                     public void run() {
    483                         runDevicePolling(sourceAddress, candidates, retryCount, callback,
    484                                 allocated);
    485                     }
    486                 });
    487             }
    488         });
    489     }
    490 
    491     @IoThreadOnly
    492     private boolean sendPollMessage(int sourceAddress, int destinationAddress, int retryCount) {
    493         assertRunOnIoThread();
    494         for (int i = 0; i < retryCount; ++i) {
    495             // <Polling Message> is a message which has empty body.
    496             int ret =
    497                     nativeSendCecCommand(mNativePtr, sourceAddress, destinationAddress, EMPTY_BODY);
    498             if (ret == SendMessageResult.SUCCESS) {
    499                 return true;
    500             } else if (ret != SendMessageResult.NACK) {
    501                 // Unusual failure
    502                 HdmiLogger.warning("Failed to send a polling message(%d->%d) with return code %d",
    503                         sourceAddress, destinationAddress, ret);
    504             }
    505         }
    506         return false;
    507     }
    508 
    509     private void assertRunOnIoThread() {
    510         if (Looper.myLooper() != mIoHandler.getLooper()) {
    511             throw new IllegalStateException("Should run on io thread.");
    512         }
    513     }
    514 
    515     private void assertRunOnServiceThread() {
    516         if (Looper.myLooper() != mControlHandler.getLooper()) {
    517             throw new IllegalStateException("Should run on service thread.");
    518         }
    519     }
    520 
    521     // Run a Runnable on IO thread.
    522     // It should be careful to access member variables on IO thread because
    523     // it can be accessed from system thread as well.
    524     private void runOnIoThread(Runnable runnable) {
    525         mIoHandler.post(runnable);
    526     }
    527 
    528     private void runOnServiceThread(Runnable runnable) {
    529         mControlHandler.post(runnable);
    530     }
    531 
    532     @ServiceThreadOnly
    533     void flush(final Runnable runnable) {
    534         assertRunOnServiceThread();
    535         runOnIoThread(new Runnable() {
    536             @Override
    537             public void run() {
    538                 // This ensures the runnable for cleanup is performed after all the pending
    539                 // commands are processed by IO thread.
    540                 runOnServiceThread(runnable);
    541             }
    542         });
    543     }
    544 
    545     private boolean isAcceptableAddress(int address) {
    546         // Can access command targeting devices available in local device or broadcast command.
    547         if (address == Constants.ADDR_BROADCAST) {
    548             return true;
    549         }
    550         return isAllocatedLocalDeviceAddress(address);
    551     }
    552 
    553     @ServiceThreadOnly
    554     private void onReceiveCommand(HdmiCecMessage message) {
    555         assertRunOnServiceThread();
    556         if (isAcceptableAddress(message.getDestination()) && mService.handleCecCommand(message)) {
    557             return;
    558         }
    559         // Not handled message, so we will reply it with <Feature Abort>.
    560         maySendFeatureAbortCommand(message, Constants.ABORT_UNRECOGNIZED_OPCODE);
    561     }
    562 
    563     @ServiceThreadOnly
    564     void maySendFeatureAbortCommand(HdmiCecMessage message, int reason) {
    565         assertRunOnServiceThread();
    566         // Swap the source and the destination.
    567         int src = message.getDestination();
    568         int dest = message.getSource();
    569         if (src == Constants.ADDR_BROADCAST || dest == Constants.ADDR_UNREGISTERED) {
    570             // Don't reply <Feature Abort> from the unregistered devices or for the broadcasted
    571             // messages. See CEC 12.2 Protocol General Rules for detail.
    572             return;
    573         }
    574         int originalOpcode = message.getOpcode();
    575         if (originalOpcode == Constants.MESSAGE_FEATURE_ABORT) {
    576             return;
    577         }
    578         sendCommand(
    579                 HdmiCecMessageBuilder.buildFeatureAbortCommand(src, dest, originalOpcode, reason));
    580     }
    581 
    582     @ServiceThreadOnly
    583     void sendCommand(HdmiCecMessage cecMessage) {
    584         assertRunOnServiceThread();
    585         sendCommand(cecMessage, null);
    586     }
    587 
    588     @ServiceThreadOnly
    589     void sendCommand(final HdmiCecMessage cecMessage,
    590             final HdmiControlService.SendMessageCallback callback) {
    591         assertRunOnServiceThread();
    592         addMessageToHistory(false /* isReceived */, cecMessage);
    593         runOnIoThread(new Runnable() {
    594             @Override
    595             public void run() {
    596                 HdmiLogger.debug("[S]:" + cecMessage);
    597                 byte[] body = buildBody(cecMessage.getOpcode(), cecMessage.getParams());
    598                 int i = 0;
    599                 int errorCode = SendMessageResult.SUCCESS;
    600                 do {
    601                     errorCode = nativeSendCecCommand(mNativePtr, cecMessage.getSource(),
    602                             cecMessage.getDestination(), body);
    603                     if (errorCode == SendMessageResult.SUCCESS) {
    604                         break;
    605                     }
    606                 } while (i++ < HdmiConfig.RETRANSMISSION_COUNT);
    607 
    608                 final int finalError = errorCode;
    609                 if (finalError != SendMessageResult.SUCCESS) {
    610                     Slog.w(TAG, "Failed to send " + cecMessage + " with errorCode=" + finalError);
    611                 }
    612                 if (callback != null) {
    613                     runOnServiceThread(new Runnable() {
    614                         @Override
    615                         public void run() {
    616                             callback.onSendCompleted(finalError);
    617                         }
    618                     });
    619                 }
    620             }
    621         });
    622     }
    623 
    624     /**
    625      * Called by native when incoming CEC message arrived.
    626      */
    627     @ServiceThreadOnly
    628     private void handleIncomingCecCommand(int srcAddress, int dstAddress, byte[] body) {
    629         assertRunOnServiceThread();
    630         HdmiCecMessage command = HdmiCecMessageBuilder.of(srcAddress, dstAddress, body);
    631         HdmiLogger.debug("[R]:" + command);
    632         addMessageToHistory(true /* isReceived */, command);
    633         onReceiveCommand(command);
    634     }
    635 
    636     /**
    637      * Called by native when a hotplug event issues.
    638      */
    639     @ServiceThreadOnly
    640     private void handleHotplug(int port, boolean connected) {
    641         assertRunOnServiceThread();
    642         HdmiLogger.debug("Hotplug event:[port:%d, connected:%b]", port, connected);
    643         mService.onHotplug(port, connected);
    644     }
    645 
    646     @ServiceThreadOnly
    647     private void addMessageToHistory(boolean isReceived, HdmiCecMessage message) {
    648         assertRunOnServiceThread();
    649         MessageHistoryRecord record = new MessageHistoryRecord(isReceived, message);
    650         if (!mMessageHistory.offer(record)) {
    651             mMessageHistory.poll();
    652             mMessageHistory.offer(record);
    653         }
    654     }
    655 
    656     void dump(final IndentingPrintWriter pw) {
    657         for (int i = 0; i < mLocalDevices.size(); ++i) {
    658             pw.println("HdmiCecLocalDevice #" + i + ":");
    659             pw.increaseIndent();
    660             mLocalDevices.valueAt(i).dump(pw);
    661             pw.decreaseIndent();
    662         }
    663         pw.println("CEC message history:");
    664         pw.increaseIndent();
    665         final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    666         for (MessageHistoryRecord record : mMessageHistory) {
    667             record.dump(pw, sdf);
    668         }
    669         pw.decreaseIndent();
    670     }
    671 
    672     private static native long nativeInit(HdmiCecController handler, MessageQueue messageQueue);
    673     private static native int nativeSendCecCommand(long controllerPtr, int srcAddress,
    674             int dstAddress, byte[] body);
    675     private static native int nativeAddLogicalAddress(long controllerPtr, int logicalAddress);
    676     private static native void nativeClearLogicalAddress(long controllerPtr);
    677     private static native int nativeGetPhysicalAddress(long controllerPtr);
    678     private static native int nativeGetVersion(long controllerPtr);
    679     private static native int nativeGetVendorId(long controllerPtr);
    680     private static native HdmiPortInfo[] nativeGetPortInfos(long controllerPtr);
    681     private static native void nativeSetOption(long controllerPtr, int flag, boolean enabled);
    682     private static native void nativeSetLanguage(long controllerPtr, String language);
    683     private static native void nativeEnableAudioReturnChannel(long controllerPtr, int port, boolean flag);
    684     private static native boolean nativeIsConnected(long controllerPtr, int port);
    685 
    686     private final class MessageHistoryRecord {
    687         private final long mTime;
    688         private final boolean mIsReceived; // true if received message and false if sent message
    689         private final HdmiCecMessage mMessage;
    690 
    691         public MessageHistoryRecord(boolean isReceived, HdmiCecMessage message) {
    692             mTime = System.currentTimeMillis();
    693             mIsReceived = isReceived;
    694             mMessage = message;
    695         }
    696 
    697         void dump(final IndentingPrintWriter pw, SimpleDateFormat sdf) {
    698             pw.print(mIsReceived ? "[R]" : "[S]");
    699             pw.print(" time=");
    700             pw.print(sdf.format(new Date(mTime)));
    701             pw.print(" message=");
    702             pw.println(mMessage);
    703         }
    704     }
    705 }
    706