HdmiCecController.java revision d42a7a322b7adf532ae0b70cb9eb1df7e62a8f2d
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 17package com.android.server.hdmi; 18 19import android.hardware.hdmi.HdmiCec; 20import android.hardware.hdmi.HdmiCecDeviceInfo; 21import android.hardware.hdmi.HdmiCecMessage; 22import android.os.Handler; 23import android.os.Looper; 24import android.os.MessageQueue; 25import android.util.Slog; 26import android.util.SparseArray; 27 28import com.android.server.hdmi.HdmiControlService.DevicePollingCallback; 29 30import libcore.util.EmptyArray; 31 32import java.util.ArrayList; 33import java.util.List; 34 35/** 36 * Manages HDMI-CEC command and behaviors. It converts user's command into CEC command 37 * and pass it to CEC HAL so that it sends message to other device. For incoming 38 * message it translates the message and delegates it to proper module. 39 * 40 * <p>It should be careful to access member variables on IO thread because 41 * it can be accessed from system thread as well. 42 * 43 * <p>It can be created only by {@link HdmiCecController#create} 44 * 45 * <p>Declared as package-private, accessed by {@link HdmiControlService} only. 46 */ 47final class HdmiCecController { 48 private static final String TAG = "HdmiCecController"; 49 50 private static final byte[] EMPTY_BODY = EmptyArray.BYTE; 51 52 // A message to pass cec send command to IO looper. 53 private static final int MSG_SEND_CEC_COMMAND = 1; 54 // A message to delegate logical allocation to IO looper. 55 private static final int MSG_ALLOCATE_LOGICAL_ADDRESS = 2; 56 57 // Message types to handle incoming message in main service looper. 58 private final static int MSG_RECEIVE_CEC_COMMAND = 1; 59 // A message to report allocated logical address to main control looper. 60 private final static int MSG_REPORT_LOGICAL_ADDRESS = 2; 61 62 private static final int NUM_LOGICAL_ADDRESS = 16; 63 64 private static final int RETRY_COUNT_FOR_LOGICAL_ADDRESS_ALLOCATION = 3; 65 66 // Handler instance to process synchronous I/O (mainly send) message. 67 private Handler mIoHandler; 68 69 // Handler instance to process various messages coming from other CEC 70 // device or issued by internal state change. 71 private Handler mControlHandler; 72 73 // Stores the pointer to the native implementation of the service that 74 // interacts with HAL. 75 private volatile long mNativePtr; 76 77 private HdmiControlService mService; 78 79 // Map-like container of all cec devices including local ones. 80 // A logical address of device is used as key of container. 81 private final SparseArray<HdmiCecDeviceInfo> mDeviceInfos = new SparseArray<>(); 82 83 // Stores the local CEC devices in the system. Device type is used for key. 84 private final SparseArray<HdmiCecLocalDevice> mLocalDevices = new SparseArray<>(); 85 86 // Private constructor. Use HdmiCecController.create(). 87 private HdmiCecController() { 88 } 89 90 /** 91 * A factory method to get {@link HdmiCecController}. If it fails to initialize 92 * inner device or has no device it will return {@code null}. 93 * 94 * <p>Declared as package-private, accessed by {@link HdmiControlService} only. 95 * @param service {@link HdmiControlService} instance used to create internal handler 96 * and to pass callback for incoming message or event. 97 * @return {@link HdmiCecController} if device is initialized successfully. Otherwise, 98 * returns {@code null}. 99 */ 100 static HdmiCecController create(HdmiControlService service) { 101 HdmiCecController controller = new HdmiCecController(); 102 long nativePtr = nativeInit(controller, service.getServiceLooper().getQueue()); 103 if (nativePtr == 0L) { 104 controller = null; 105 return null; 106 } 107 108 controller.init(service, nativePtr); 109 return controller; 110 } 111 112 private void init(HdmiControlService service, long nativePtr) { 113 mService = service; 114 mIoHandler = new Handler(service.getServiceLooper()); 115 mControlHandler = new Handler(service.getServiceLooper()); 116 mNativePtr = nativePtr; 117 } 118 119 /** 120 * Perform initialization for each hosted device. 121 * 122 * @param deviceTypes array of device types 123 */ 124 void initializeLocalDevices(int[] deviceTypes) { 125 assertRunOnServiceThread(); 126 for (int type : deviceTypes) { 127 HdmiCecLocalDevice device = HdmiCecLocalDevice.create(this, type); 128 if (device == null) { 129 continue; 130 } 131 // TODO: Consider restoring the local device addresses from persistent storage 132 // to allocate the same addresses again if possible. 133 device.setPreferredAddress(HdmiCec.ADDR_UNREGISTERED); 134 mLocalDevices.put(type, device); 135 device.init(); 136 } 137 } 138 139 /** 140 * Interface to report allocated logical address. 141 */ 142 interface AllocateLogicalAddressCallback { 143 /** 144 * Called when a new logical address is allocated. 145 * 146 * @param deviceType requested device type to allocate logical address 147 * @param logicalAddress allocated logical address. If it is 148 * {@link HdmiCec#ADDR_UNREGISTERED}, it means that 149 * it failed to allocate logical address for the given device type 150 */ 151 void onAllocated(int deviceType, int logicalAddress); 152 } 153 154 /** 155 * Allocate a new logical address of the given device type. Allocated 156 * address will be reported through {@link AllocateLogicalAddressCallback}. 157 * 158 * <p> Declared as package-private, accessed by {@link HdmiControlService} only. 159 * 160 * @param deviceType type of device to used to determine logical address 161 * @param preferredAddress a logical address preferred to be allocated. 162 * If sets {@link HdmiCec#ADDR_UNREGISTERED}, scans 163 * the smallest logical address matched with the given device type. 164 * Otherwise, scan address will start from {@code preferredAddress} 165 * @param callback callback interface to report allocated logical address to caller 166 */ 167 void allocateLogicalAddress(final int deviceType, final int preferredAddress, 168 final AllocateLogicalAddressCallback callback) { 169 assertRunOnServiceThread(); 170 171 runOnIoThread(new Runnable() { 172 @Override 173 public void run() { 174 handleAllocateLogicalAddress(deviceType, preferredAddress, callback); 175 } 176 }); 177 } 178 179 private void handleAllocateLogicalAddress(final int deviceType, int preferredAddress, 180 final AllocateLogicalAddressCallback 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 == HdmiCec.ADDR_UNREGISTERED) { 186 for (int i = 0; i < NUM_LOGICAL_ADDRESS; ++i) { 187 if (deviceType == HdmiCec.getTypeFromAddress(i)) { 188 startAddress = i; 189 break; 190 } 191 } 192 } 193 194 int logicalAddress = HdmiCec.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 != HdmiCec.ADDR_UNREGISTERED 199 && deviceType == HdmiCec.getTypeFromAddress(curAddress)) { 200 if (!sendPollMessage(curAddress, RETRY_COUNT_FOR_LOGICAL_ADDRESS_ALLOCATION)) { 201 logicalAddress = curAddress; 202 break; 203 } 204 } 205 } 206 207 final int assignedAddress = logicalAddress; 208 if (callback != null) { 209 runOnServiceThread(new Runnable() { 210 @Override 211 public void run() { 212 callback.onAllocated(deviceType, assignedAddress); 213 } 214 }); 215 } 216 } 217 218 private static byte[] buildBody(int opcode, byte[] params) { 219 byte[] body = new byte[params.length + 1]; 220 body[0] = (byte) opcode; 221 System.arraycopy(params, 0, body, 1, params.length); 222 return body; 223 } 224 225 /** 226 * Add a new {@link HdmiCecDeviceInfo}. It returns old device info which has the same 227 * logical address as new device info's. 228 * 229 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 230 * 231 * @param deviceInfo a new {@link HdmiCecDeviceInfo} to be added. 232 * @return {@code null} if it is new device. Otherwise, returns old {@HdmiCecDeviceInfo} 233 * that has the same logical address as new one has. 234 */ 235 HdmiCecDeviceInfo addDeviceInfo(HdmiCecDeviceInfo deviceInfo) { 236 assertRunOnServiceThread(); 237 HdmiCecDeviceInfo oldDeviceInfo = getDeviceInfo(deviceInfo.getLogicalAddress()); 238 if (oldDeviceInfo != null) { 239 removeDeviceInfo(deviceInfo.getLogicalAddress()); 240 } 241 mDeviceInfos.append(deviceInfo.getLogicalAddress(), deviceInfo); 242 return oldDeviceInfo; 243 } 244 245 /** 246 * Remove a device info corresponding to the given {@code logicalAddress}. 247 * It returns removed {@link HdmiCecDeviceInfo} if exists. 248 * 249 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 250 * 251 * @param logicalAddress logical address of device to be removed 252 * @return removed {@link HdmiCecDeviceInfo} it exists. Otherwise, returns {@code null} 253 */ 254 HdmiCecDeviceInfo removeDeviceInfo(int logicalAddress) { 255 assertRunOnServiceThread(); 256 HdmiCecDeviceInfo deviceInfo = mDeviceInfos.get(logicalAddress); 257 if (deviceInfo != null) { 258 mDeviceInfos.remove(logicalAddress); 259 } 260 return deviceInfo; 261 } 262 263 /** 264 * Clear all device info. 265 * 266 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 267 */ 268 void clearDeviceInfoList() { 269 assertRunOnServiceThread(); 270 mDeviceInfos.clear(); 271 } 272 273 /** 274 * Return a list of all {@link HdmiCecDeviceInfo}. 275 * 276 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 277 */ 278 List<HdmiCecDeviceInfo> getDeviceInfoList() { 279 assertRunOnServiceThread(); 280 return sparseArrayToList(mDeviceInfos); 281 } 282 283 /** 284 * Return a {@link HdmiCecDeviceInfo} corresponding to the given {@code logicalAddress}. 285 * 286 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 287 * 288 * @param logicalAddress logical address to be retrieved 289 * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}. 290 * Returns null if no logical address matched 291 */ 292 HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) { 293 assertRunOnServiceThread(); 294 return mDeviceInfos.get(logicalAddress); 295 } 296 297 /** 298 * Return the locally hosted logical device of a given type. 299 * 300 * @param deviceType logical device type 301 * @return {@link HdmiCecLocalDevice} instance if the instance of the type is available; 302 * otherwise null. 303 */ 304 HdmiCecLocalDevice getLocalDevice(int deviceType) { 305 return mLocalDevices.get(deviceType); 306 } 307 308 /** 309 * Add a new logical address to the device. Device's HW should be notified 310 * when a new logical address is assigned to a device, so that it can accept 311 * a command having available destinations. 312 * 313 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 314 * 315 * @param newLogicalAddress a logical address to be added 316 * @return 0 on success. Otherwise, returns negative value 317 */ 318 int addLogicalAddress(int newLogicalAddress) { 319 assertRunOnServiceThread(); 320 if (HdmiCec.isValidAddress(newLogicalAddress)) { 321 return nativeAddLogicalAddress(mNativePtr, newLogicalAddress); 322 } else { 323 return -1; 324 } 325 } 326 327 /** 328 * Clear all logical addresses registered in the device. 329 * 330 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 331 */ 332 void clearLogicalAddress() { 333 assertRunOnServiceThread(); 334 // TODO: consider to backup logical address so that new logical address 335 // allocation can use it as preferred address. 336 for (int i = 0; i < mLocalDevices.size(); ++i) { 337 mLocalDevices.valueAt(i).clearAddress(); 338 } 339 nativeClearLogicalAddress(mNativePtr); 340 } 341 342 /** 343 * Return the physical address of the device. 344 * 345 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 346 * 347 * @return CEC physical address of the device. The range of success address 348 * is between 0x0000 and 0xFFFF. If failed it returns -1 349 */ 350 int getPhysicalAddress() { 351 assertRunOnServiceThread(); 352 return nativeGetPhysicalAddress(mNativePtr); 353 } 354 355 /** 356 * Return CEC version of the device. 357 * 358 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 359 */ 360 int getVersion() { 361 assertRunOnServiceThread(); 362 return nativeGetVersion(mNativePtr); 363 } 364 365 /** 366 * Return vendor id of the device. 367 * 368 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 369 */ 370 int getVendorId() { 371 assertRunOnServiceThread(); 372 return nativeGetVendorId(mNativePtr); 373 } 374 375 /** 376 * Poll all remote devices. It sends <Polling Message> to all remote 377 * devices. 378 * 379 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 380 * 381 * @param callback an interface used to get a list of all remote devices' address 382 * @param retryCount the number of retry used to send polling message to remote devices 383 */ 384 void pollDevices(DevicePollingCallback callback, int retryCount) { 385 assertRunOnServiceThread(); 386 // Extract polling candidates. No need to poll against local devices. 387 ArrayList<Integer> pollingCandidates = new ArrayList<>(); 388 for (int i = HdmiCec.ADDR_SPECIFIC_USE; i >= HdmiCec.ADDR_TV; --i) { 389 if (!isAllocatedLocalDeviceAddress(i)) { 390 pollingCandidates.add(i); 391 } 392 } 393 394 runDevicePolling(pollingCandidates, retryCount, callback); 395 } 396 397 /** 398 * Return a list of all {@link HdmiCecLocalDevice}s. 399 * 400 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 401 */ 402 List<HdmiCecLocalDevice> getLocalDeviceList() { 403 assertRunOnServiceThread(); 404 return sparseArrayToList(mLocalDevices); 405 } 406 407 private static <T> List<T> sparseArrayToList(SparseArray<T> array) { 408 ArrayList<T> list = new ArrayList<>(); 409 for (int i = 0; i < array.size(); ++i) { 410 list.add(array.valueAt(i)); 411 } 412 return list; 413 } 414 415 private boolean isAllocatedLocalDeviceAddress(int address) { 416 for (int i = 0; i < mLocalDevices.size(); ++i) { 417 if (mLocalDevices.valueAt(i).isAddressOf(address)) { 418 return true; 419 } 420 } 421 return false; 422 } 423 424 private void runDevicePolling(final List<Integer> candidates, final int retryCount, 425 final DevicePollingCallback callback) { 426 assertRunOnServiceThread(); 427 runOnIoThread(new Runnable() { 428 @Override 429 public void run() { 430 final ArrayList<Integer> allocated = new ArrayList<>(); 431 for (Integer address : candidates) { 432 if (sendPollMessage(address, retryCount)) { 433 allocated.add(address); 434 } 435 } 436 if (callback != null) { 437 runOnServiceThread(new Runnable() { 438 @Override 439 public void run() { 440 callback.onPollingFinished(allocated); 441 } 442 }); 443 } 444 } 445 }); 446 } 447 448 private boolean sendPollMessage(int address, int retryCount) { 449 assertRunOnIoThread(); 450 for (int i = 0; i < retryCount; ++i) { 451 // <Polling Message> is a message which has empty body and 452 // uses same address for both source and destination address. 453 // If sending <Polling Message> failed (NAK), it becomes 454 // new logical address for the device because no device uses 455 // it as logical address of the device. 456 if (nativeSendCecCommand(mNativePtr, address, address, EMPTY_BODY) 457 == HdmiControlService.SEND_RESULT_SUCCESS) { 458 return true; 459 } 460 } 461 return false; 462 } 463 464 private void assertRunOnIoThread() { 465 if (Looper.myLooper() != mIoHandler.getLooper()) { 466 throw new IllegalStateException("Should run on io thread."); 467 } 468 } 469 470 private void assertRunOnServiceThread() { 471 if (Looper.myLooper() != mControlHandler.getLooper()) { 472 throw new IllegalStateException("Should run on service thread."); 473 } 474 } 475 476 // Run a Runnable on IO thread. 477 // It should be careful to access member variables on IO thread because 478 // it can be accessed from system thread as well. 479 private void runOnIoThread(Runnable runnable) { 480 mIoHandler.post(runnable); 481 } 482 483 private void runOnServiceThread(Runnable runnable) { 484 mControlHandler.post(runnable); 485 } 486 487 private boolean isAcceptableAddress(int address) { 488 // Can access command targeting devices available in local device or broadcast command. 489 if (address == HdmiCec.ADDR_BROADCAST) { 490 return true; 491 } 492 return isAllocatedLocalDeviceAddress(address); 493 } 494 495 private void onReceiveCommand(HdmiCecMessage message) { 496 assertRunOnServiceThread(); 497 if (isAcceptableAddress(message.getDestination()) 498 && mService.handleCecCommand(message)) { 499 return; 500 } 501 502 // TODO: Use device's source address for broadcast message. 503 int sourceAddress = message.getDestination() != HdmiCec.ADDR_BROADCAST ? 504 message.getDestination() : 0; 505 // Reply <Feature Abort> to initiator (source) for all requests. 506 HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildFeatureAbortCommand 507 (sourceAddress, message.getSource(), message.getOpcode(), 508 HdmiCecMessageBuilder.ABORT_REFUSED); 509 sendCommand(cecMessage, null); 510 } 511 512 void sendCommand(HdmiCecMessage cecMessage) { 513 sendCommand(cecMessage, null); 514 } 515 516 void sendCommand(final HdmiCecMessage cecMessage, 517 final HdmiControlService.SendMessageCallback callback) { 518 runOnIoThread(new Runnable() { 519 @Override 520 public void run() { 521 byte[] body = buildBody(cecMessage.getOpcode(), cecMessage.getParams()); 522 final int error = nativeSendCecCommand(mNativePtr, cecMessage.getSource(), 523 cecMessage.getDestination(), body); 524 if (error != HdmiControlService.SEND_RESULT_SUCCESS) { 525 Slog.w(TAG, "Failed to send " + cecMessage); 526 } 527 if (callback != null) { 528 runOnServiceThread(new Runnable() { 529 @Override 530 public void run() { 531 callback.onSendCompleted(error); 532 } 533 }); 534 } 535 } 536 }); 537 } 538 539 /** 540 * Called by native when incoming CEC message arrived. 541 */ 542 private void handleIncomingCecCommand(int srcAddress, int dstAddress, byte[] body) { 543 assertRunOnServiceThread(); 544 onReceiveCommand(HdmiCecMessageBuilder.of(srcAddress, dstAddress, body)); 545 } 546 547 /** 548 * Called by native when a hotplug event issues. 549 */ 550 private void handleHotplug(boolean connected) { 551 // TODO: once add port number to cec HAL interface, pass port number 552 // to the service. 553 mService.onHotplug(0, connected); 554 } 555 556 private static native long nativeInit(HdmiCecController handler, MessageQueue messageQueue); 557 private static native int nativeSendCecCommand(long controllerPtr, int srcAddress, 558 int dstAddress, byte[] body); 559 private static native int nativeAddLogicalAddress(long controllerPtr, int logicalAddress); 560 private static native void nativeClearLogicalAddress(long controllerPtr); 561 private static native int nativeGetPhysicalAddress(long controllerPtr); 562 private static native int nativeGetVersion(long controllerPtr); 563 private static native int nativeGetVendorId(long controllerPtr); 564} 565