HdmiControlService.java revision 3ecdd832c77483c909fbf90d17d0e6d97ca365ee
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.annotation.Nullable; 20import android.content.Context; 21import android.hardware.hdmi.HdmiCec; 22import android.hardware.hdmi.HdmiCecDeviceInfo; 23import android.hardware.hdmi.HdmiCecMessage; 24import android.hardware.hdmi.HdmiHotplugEvent; 25import android.hardware.hdmi.HdmiPortInfo; 26import android.hardware.hdmi.IHdmiControlCallback; 27import android.hardware.hdmi.IHdmiControlService; 28import android.hardware.hdmi.IHdmiDeviceEventListener; 29import android.hardware.hdmi.IHdmiHotplugEventListener; 30import android.os.Build; 31import android.os.Handler; 32import android.os.HandlerThread; 33import android.os.IBinder; 34import android.os.Looper; 35import android.os.RemoteException; 36import android.util.Slog; 37import android.util.SparseArray; 38import android.util.SparseIntArray; 39 40import com.android.server.SystemService; 41import com.android.server.hdmi.HdmiCecController.AllocateAddressCallback; 42 43import java.util.ArrayList; 44import java.util.Collections; 45import java.util.List; 46 47/** 48 * Provides a service for sending and processing HDMI control messages, 49 * HDMI-CEC and MHL control command, and providing the information on both standard. 50 */ 51public final class HdmiControlService extends SystemService { 52 private static final String TAG = "HdmiControlService"; 53 54 // TODO: Rename the permission to HDMI_CONTROL. 55 private static final String PERMISSION = "android.permission.HDMI_CEC"; 56 57 /** 58 * Interface to report send result. 59 */ 60 interface SendMessageCallback { 61 /** 62 * Called when {@link HdmiControlService#sendCecCommand} is completed. 63 * 64 * @param error result of send request. 65 * @see {@link #SEND_RESULT_SUCCESS} 66 * @see {@link #SEND_RESULT_NAK} 67 * @see {@link #SEND_RESULT_FAILURE} 68 */ 69 void onSendCompleted(int error); 70 } 71 72 /** 73 * Interface to get a list of available logical devices. 74 */ 75 interface DevicePollingCallback { 76 /** 77 * Called when device polling is finished. 78 * 79 * @param ackedAddress a list of logical addresses of available devices 80 */ 81 void onPollingFinished(List<Integer> ackedAddress); 82 } 83 84 // A thread to handle synchronous IO of CEC and MHL control service. 85 // Since all of CEC and MHL HAL interfaces processed in short time (< 200ms) 86 // and sparse call it shares a thread to handle IO operations. 87 private final HandlerThread mIoThread = new HandlerThread("Hdmi Control Io Thread"); 88 89 // Used to synchronize the access to the service. 90 private final Object mLock = new Object(); 91 92 // Type of logical devices hosted in the system. Stored in the unmodifiable list. 93 private final List<Integer> mLocalDevices; 94 95 // List of listeners registered by callers that want to get notified of 96 // hotplug events. 97 private final ArrayList<IHdmiHotplugEventListener> mHotplugEventListeners = new ArrayList<>(); 98 99 // List of records for hotplug event listener to handle the the caller killed in action. 100 private final ArrayList<HotplugEventListenerRecord> mHotplugEventListenerRecords = 101 new ArrayList<>(); 102 103 // List of listeners registered by callers that want to get notified of 104 // device status events. 105 private final ArrayList<IHdmiDeviceEventListener> mDeviceEventListeners = new ArrayList<>(); 106 107 // List of records for device event listener to handle the the caller killed in action. 108 private final ArrayList<DeviceEventListenerRecord> mDeviceEventListenerRecords = 109 new ArrayList<>(); 110 111 // Handler running on service thread. It's used to run a task in service thread. 112 private final Handler mHandler = new Handler(); 113 114 @Nullable 115 private HdmiCecController mCecController; 116 117 @Nullable 118 private HdmiMhlController mMhlController; 119 120 // HDMI port information. Stored in the unmodifiable list to keep the static information 121 // from being modified. 122 private List<HdmiPortInfo> mPortInfo; 123 124 public HdmiControlService(Context context) { 125 super(context); 126 mLocalDevices = HdmiUtils.asImmutableList(getContext().getResources().getIntArray( 127 com.android.internal.R.array.config_hdmiCecLogicalDeviceType)); 128 } 129 130 @Override 131 public void onStart() { 132 mIoThread.start(); 133 mCecController = HdmiCecController.create(this); 134 135 if (mCecController != null) { 136 initializeLocalDevices(mLocalDevices); 137 } else { 138 Slog.i(TAG, "Device does not support HDMI-CEC."); 139 } 140 141 mMhlController = HdmiMhlController.create(this); 142 if (mMhlController == null) { 143 Slog.i(TAG, "Device does not support MHL-control."); 144 } 145 mPortInfo = initPortInfo(); 146 publishBinderService(Context.HDMI_CONTROL_SERVICE, new BinderService()); 147 148 // TODO: Read the preference for SystemAudioMode and initialize mSystemAudioMode and 149 // start to monitor the preference value and invoke SystemAudioActionFromTv if needed. 150 } 151 152 private void initializeLocalDevices(final List<Integer> deviceTypes) { 153 // A container for [Logical Address, Local device info]. 154 final SparseArray<HdmiCecLocalDevice> devices = new SparseArray<>(); 155 final SparseIntArray finished = new SparseIntArray(); 156 for (int type : deviceTypes) { 157 final HdmiCecLocalDevice localDevice = HdmiCecLocalDevice.create(this, type); 158 localDevice.init(); 159 mCecController.allocateLogicalAddress(type, 160 localDevice.getPreferredAddress(), new AllocateAddressCallback() { 161 @Override 162 public void onAllocated(int deviceType, int logicalAddress) { 163 if (logicalAddress == HdmiCec.ADDR_UNREGISTERED) { 164 Slog.e(TAG, "Failed to allocate address:[device_type:" + deviceType + "]"); 165 } else { 166 HdmiCecDeviceInfo deviceInfo = createDeviceInfo(logicalAddress, deviceType); 167 localDevice.setDeviceInfo(deviceInfo); 168 mCecController.addLocalDevice(deviceType, localDevice); 169 mCecController.addLogicalAddress(logicalAddress); 170 devices.append(logicalAddress, localDevice); 171 } 172 finished.append(deviceType, logicalAddress); 173 174 // Once finish address allocation for all devices, notify 175 // it to each device. 176 if (deviceTypes.size() == finished.size()) { 177 notifyAddressAllocated(devices); 178 } 179 } 180 }); 181 } 182 } 183 184 private void notifyAddressAllocated(SparseArray<HdmiCecLocalDevice> devices) { 185 for (int i = 0; i < devices.size(); ++i) { 186 int address = devices.keyAt(i); 187 HdmiCecLocalDevice device = devices.valueAt(i); 188 device.onAddressAllocated(address); 189 } 190 } 191 192 // Initialize HDMI port information. Combine the information from CEC and MHL HAL and 193 // keep them in one place. 194 private List<HdmiPortInfo> initPortInfo() { 195 HdmiPortInfo[] cecPortInfo = null; 196 197 // CEC HAL provides majority of the info while MHL does only MHL support flag for 198 // each port. Return empty array if CEC HAL didn't provide the info. 199 if (mCecController != null) { 200 cecPortInfo = mCecController.getPortInfos(); 201 } 202 if (cecPortInfo == null) { 203 return Collections.emptyList(); 204 } 205 206 HdmiPortInfo[] mhlPortInfo = new HdmiPortInfo[0]; 207 if (mMhlController != null) { 208 // TODO: Implement plumbing logic to get MHL port information. 209 // mhlPortInfo = mMhlController.getPortInfos(); 210 } 211 212 // Use the id (port number) to find the matched info between CEC and MHL to combine them 213 // into one. Leave the field `mhlSupported` to false if matched MHL entry is not found. 214 ArrayList<HdmiPortInfo> result = new ArrayList<>(cecPortInfo.length); 215 for (int i = 0; i < cecPortInfo.length; ++i) { 216 HdmiPortInfo cec = cecPortInfo[i]; 217 int id = cec.getId(); 218 boolean mhlInfoFound = false; 219 for (HdmiPortInfo mhl : mhlPortInfo) { 220 if (id == mhl.getId()) { 221 result.add(new HdmiPortInfo(id, cec.getType(), cec.getAddress(), 222 cec.isCecSupported(), mhl.isMhlSupported(), cec.isArcSupported())); 223 mhlInfoFound = true; 224 break; 225 } 226 } 227 if (!mhlInfoFound) { 228 result.add(cec); 229 } 230 } 231 232 return Collections.unmodifiableList(result); 233 } 234 235 /** 236 * Returns HDMI port information for the given port id. 237 * 238 * @param portId HDMI port id 239 * @return {@link HdmiPortInfo} for the given port 240 */ 241 HdmiPortInfo getPortInfo(int portId) { 242 // mPortInfo is an unmodifiable list and the only reference to its inner list. 243 // No lock is necessary. 244 for (HdmiPortInfo info : mPortInfo) { 245 if (portId == info.getId()) { 246 return info; 247 } 248 } 249 return null; 250 } 251 252 /** 253 * Returns the routing path (physical address) of the HDMI port for the given 254 * port id. 255 */ 256 int portIdToPath(int portId) { 257 HdmiPortInfo portInfo = getPortInfo(portId); 258 if (portInfo == null) { 259 Slog.e(TAG, "Cannot find the port info: " + portId); 260 return HdmiConstants.INVALID_PHYSICAL_ADDRESS; 261 } 262 return portInfo.getAddress(); 263 } 264 265 /** 266 * Returns the id of HDMI port located at the top of the hierarchy of 267 * the specified routing path. For the routing path 0x1220 (1.2.2.0), for instance, 268 * the port id to be returned is the ID associated with the port address 269 * 0x1000 (1.0.0.0) which is the topmost path of the given routing path. 270 */ 271 int pathToPortId(int path) { 272 int portAddress = path & HdmiConstants.ROUTING_PATH_TOP_MASK; 273 for (HdmiPortInfo info : mPortInfo) { 274 if (portAddress == info.getAddress()) { 275 return info.getId(); 276 } 277 } 278 return HdmiConstants.INVALID_PORT_ID; 279 } 280 281 /** 282 * Returns {@link Looper} for IO operation. 283 * 284 * <p>Declared as package-private. 285 */ 286 Looper getIoLooper() { 287 return mIoThread.getLooper(); 288 } 289 290 /** 291 * Returns {@link Looper} of main thread. Use this {@link Looper} instance 292 * for tasks that are running on main service thread. 293 * 294 * <p>Declared as package-private. 295 */ 296 Looper getServiceLooper() { 297 return mHandler.getLooper(); 298 } 299 300 /** 301 * Returns physical address of the device. 302 */ 303 int getPhysicalAddress() { 304 return mCecController.getPhysicalAddress(); 305 } 306 307 /** 308 * Returns vendor id of CEC service. 309 */ 310 int getVendorId() { 311 return mCecController.getVendorId(); 312 } 313 314 HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) { 315 assertRunOnServiceThread(); 316 HdmiCecLocalDeviceTv tv = tv(); 317 if (tv == null) { 318 return null; 319 } 320 return tv.getDeviceInfo(logicalAddress); 321 } 322 323 /** 324 * Returns version of CEC. 325 */ 326 int getCecVersion() { 327 return mCecController.getVersion(); 328 } 329 330 /** 331 * Whether a device of the specified physical address is connected to ARC enabled port. 332 */ 333 boolean isConnectedToArcPort(int physicalAddress) { 334 for (HdmiPortInfo portInfo : mPortInfo) { 335 if (hasSameTopPort(portInfo.getAddress(), physicalAddress) 336 && portInfo.isArcSupported()) { 337 return true; 338 } 339 } 340 return false; 341 } 342 343 void runOnServiceThread(Runnable runnable) { 344 mHandler.post(runnable); 345 } 346 347 void runOnServiceThreadAtFrontOfQueue(Runnable runnable) { 348 mHandler.postAtFrontOfQueue(runnable); 349 } 350 351 private void assertRunOnServiceThread() { 352 if (Looper.myLooper() != mHandler.getLooper()) { 353 throw new IllegalStateException("Should run on service thread."); 354 } 355 } 356 357 /** 358 * Transmit a CEC command to CEC bus. 359 * 360 * @param command CEC command to send out 361 * @param callback interface used to the result of send command 362 */ 363 void sendCecCommand(HdmiCecMessage command, @Nullable SendMessageCallback callback) { 364 mCecController.sendCommand(command, callback); 365 } 366 367 void sendCecCommand(HdmiCecMessage command) { 368 mCecController.sendCommand(command, null); 369 } 370 371 boolean handleCecCommand(HdmiCecMessage message) { 372 return dispatchMessageToLocalDevice(message); 373 } 374 375 void setAudioReturnChannel(boolean enabled) { 376 mCecController.setAudioReturnChannel(enabled); 377 } 378 379 private boolean dispatchMessageToLocalDevice(HdmiCecMessage message) { 380 for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) { 381 if (device.dispatchMessage(message) 382 && message.getDestination() != HdmiCec.ADDR_BROADCAST) { 383 return true; 384 } 385 } 386 387 Slog.w(TAG, "Unhandled cec command:" + message); 388 return false; 389 } 390 391 /** 392 * Called when a new hotplug event is issued. 393 * 394 * @param portNo hdmi port number where hot plug event issued. 395 * @param connected whether to be plugged in or not 396 */ 397 void onHotplug(int portNo, boolean connected) { 398 assertRunOnServiceThread(); 399 400 for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) { 401 device.onHotplug(portNo, connected); 402 } 403 404 announceHotplugEvent(portNo, connected); 405 } 406 407 /** 408 * Poll all remote devices. It sends <Polling Message> to all remote 409 * devices. 410 * 411 * @param callback an interface used to get a list of all remote devices' address 412 * @param pickStrategy strategy how to pick polling candidates 413 * @param retryCount the number of retry used to send polling message to remote devices 414 * @throw IllegalArgumentException if {@code pickStrategy} is invalid value 415 */ 416 void pollDevices(DevicePollingCallback callback, int pickStrategy, int retryCount) { 417 mCecController.pollDevices(callback, checkPollStrategy(pickStrategy), retryCount); 418 } 419 420 private int checkPollStrategy(int pickStrategy) { 421 int strategy = pickStrategy & HdmiConstants.POLL_STRATEGY_MASK; 422 if (strategy == 0) { 423 throw new IllegalArgumentException("Invalid poll strategy:" + pickStrategy); 424 } 425 int iterationStrategy = pickStrategy & HdmiConstants.POLL_ITERATION_STRATEGY_MASK; 426 if (iterationStrategy == 0) { 427 throw new IllegalArgumentException("Invalid iteration strategy:" + pickStrategy); 428 } 429 return strategy | iterationStrategy; 430 } 431 432 List<HdmiCecLocalDevice> getAllLocalDevices() { 433 assertRunOnServiceThread(); 434 return mCecController.getLocalDeviceList(); 435 } 436 437 Object getServiceLock() { 438 return mLock; 439 } 440 441 void setAudioStatus(boolean mute, int volume) { 442 // TODO: Hook up with AudioManager. 443 } 444 445 private HdmiCecDeviceInfo createDeviceInfo(int logicalAddress, int deviceType) { 446 // TODO: find better name instead of model name. 447 String displayName = Build.MODEL; 448 return new HdmiCecDeviceInfo(logicalAddress, 449 getPhysicalAddress(), deviceType, getVendorId(), displayName); 450 } 451 452 // Record class that monitors the event of the caller of being killed. Used to clean up 453 // the listener list and record list accordingly. 454 private final class HotplugEventListenerRecord implements IBinder.DeathRecipient { 455 private final IHdmiHotplugEventListener mListener; 456 457 public HotplugEventListenerRecord(IHdmiHotplugEventListener listener) { 458 mListener = listener; 459 } 460 461 @Override 462 public void binderDied() { 463 synchronized (mLock) { 464 mHotplugEventListenerRecords.remove(this); 465 mHotplugEventListeners.remove(mListener); 466 } 467 } 468 } 469 470 private final class DeviceEventListenerRecord implements IBinder.DeathRecipient { 471 private final IHdmiDeviceEventListener mListener; 472 473 public DeviceEventListenerRecord(IHdmiDeviceEventListener listener) { 474 mListener = listener; 475 } 476 477 @Override 478 public void binderDied() { 479 synchronized (mLock) { 480 mDeviceEventListenerRecords.remove(this); 481 mDeviceEventListeners.remove(mListener); 482 } 483 } 484 } 485 486 private void enforceAccessPermission() { 487 getContext().enforceCallingOrSelfPermission(PERMISSION, TAG); 488 } 489 490 private final class BinderService extends IHdmiControlService.Stub { 491 @Override 492 public int[] getSupportedTypes() { 493 enforceAccessPermission(); 494 // mLocalDevices is an unmodifiable list - no lock necesary. 495 int[] localDevices = new int[mLocalDevices.size()]; 496 for (int i = 0; i < localDevices.length; ++i) { 497 localDevices[i] = mLocalDevices.get(i); 498 } 499 return localDevices; 500 } 501 502 @Override 503 public void deviceSelect(final int logicalAddress, final IHdmiControlCallback callback) { 504 enforceAccessPermission(); 505 runOnServiceThread(new Runnable() { 506 @Override 507 public void run() { 508 HdmiCecLocalDeviceTv tv = tv(); 509 if (tv == null) { 510 Slog.w(TAG, "Local playback device not available"); 511 invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE); 512 return; 513 } 514 tv.deviceSelect(logicalAddress, callback); 515 } 516 }); 517 } 518 519 @Override 520 public void oneTouchPlay(final IHdmiControlCallback callback) { 521 enforceAccessPermission(); 522 runOnServiceThread(new Runnable() { 523 @Override 524 public void run() { 525 HdmiControlService.this.oneTouchPlay(callback); 526 } 527 }); 528 } 529 530 @Override 531 public void queryDisplayStatus(final IHdmiControlCallback callback) { 532 enforceAccessPermission(); 533 runOnServiceThread(new Runnable() { 534 @Override 535 public void run() { 536 HdmiControlService.this.queryDisplayStatus(callback); 537 } 538 }); 539 } 540 541 @Override 542 public void addHotplugEventListener(final IHdmiHotplugEventListener listener) { 543 enforceAccessPermission(); 544 runOnServiceThread(new Runnable() { 545 @Override 546 public void run() { 547 HdmiControlService.this.addHotplugEventListener(listener); 548 } 549 }); 550 } 551 552 @Override 553 public void removeHotplugEventListener(final IHdmiHotplugEventListener listener) { 554 enforceAccessPermission(); 555 runOnServiceThread(new Runnable() { 556 @Override 557 public void run() { 558 HdmiControlService.this.removeHotplugEventListener(listener); 559 } 560 }); 561 } 562 563 @Override 564 public void addDeviceEventListener(final IHdmiDeviceEventListener listener) { 565 enforceAccessPermission(); 566 runOnServiceThread(new Runnable() { 567 @Override 568 public void run() { 569 HdmiControlService.this.addDeviceEventListener(listener); 570 } 571 }); 572 } 573 574 @Override 575 public void portSelect(int portId, IHdmiControlCallback callback) { 576 // TODO: Implement this 577 } 578 579 @Override 580 public void sendKeyEvent(int keyCode, boolean isPressed) { 581 // TODO: Implement this 582 } 583 584 @Override 585 public List<HdmiPortInfo> getPortInfo() { 586 enforceAccessPermission(); 587 return mPortInfo; 588 } 589 } 590 591 private void oneTouchPlay(final IHdmiControlCallback callback) { 592 assertRunOnServiceThread(); 593 HdmiCecLocalDevicePlayback source = playback(); 594 if (source == null) { 595 Slog.w(TAG, "Local playback device not available"); 596 invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE); 597 return; 598 } 599 source.oneTouchPlay(callback); 600 } 601 602 private void queryDisplayStatus(final IHdmiControlCallback callback) { 603 assertRunOnServiceThread(); 604 HdmiCecLocalDevicePlayback source = playback(); 605 if (source == null) { 606 Slog.w(TAG, "Local playback device not available"); 607 invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE); 608 return; 609 } 610 source.queryDisplayStatus(callback); 611 } 612 613 private void addHotplugEventListener(IHdmiHotplugEventListener listener) { 614 HotplugEventListenerRecord record = new HotplugEventListenerRecord(listener); 615 try { 616 listener.asBinder().linkToDeath(record, 0); 617 } catch (RemoteException e) { 618 Slog.w(TAG, "Listener already died"); 619 return; 620 } 621 synchronized (mLock) { 622 mHotplugEventListenerRecords.add(record); 623 mHotplugEventListeners.add(listener); 624 } 625 } 626 627 private void removeHotplugEventListener(IHdmiHotplugEventListener listener) { 628 synchronized (mLock) { 629 for (HotplugEventListenerRecord record : mHotplugEventListenerRecords) { 630 if (record.mListener.asBinder() == listener.asBinder()) { 631 listener.asBinder().unlinkToDeath(record, 0); 632 mHotplugEventListenerRecords.remove(record); 633 break; 634 } 635 } 636 mHotplugEventListeners.remove(listener); 637 } 638 } 639 640 private void addDeviceEventListener(IHdmiDeviceEventListener listener) { 641 synchronized (mLock) { 642 for (DeviceEventListenerRecord record : mDeviceEventListenerRecords) { 643 if (record.mListener.asBinder() == listener.asBinder()) { 644 listener.asBinder().unlinkToDeath(record, 0); 645 mDeviceEventListenerRecords.remove(record); 646 break; 647 } 648 } 649 mHotplugEventListeners.remove(listener); 650 } 651 } 652 653 private void invokeCallback(IHdmiControlCallback callback, int result) { 654 try { 655 callback.onComplete(result); 656 } catch (RemoteException e) { 657 Slog.e(TAG, "Invoking callback failed:" + e); 658 } 659 } 660 661 private void announceHotplugEvent(int portNo, boolean connected) { 662 HdmiHotplugEvent event = new HdmiHotplugEvent(portNo, connected); 663 synchronized (mLock) { 664 for (IHdmiHotplugEventListener listener : mHotplugEventListeners) { 665 invokeHotplugEventListener(listener, event); 666 } 667 } 668 } 669 670 private void invokeHotplugEventListener(IHdmiHotplugEventListener listener, 671 HdmiHotplugEvent event) { 672 try { 673 listener.onReceived(event); 674 } catch (RemoteException e) { 675 Slog.e(TAG, "Failed to report hotplug event:" + event.toString(), e); 676 } 677 } 678 679 private static boolean hasSameTopPort(int path1, int path2) { 680 return (path1 & HdmiConstants.ROUTING_PATH_TOP_MASK) 681 == (path2 & HdmiConstants.ROUTING_PATH_TOP_MASK); 682 } 683 684 private HdmiCecLocalDeviceTv tv() { 685 return (HdmiCecLocalDeviceTv) mCecController.getLocalDevice(HdmiCec.DEVICE_TV); 686 } 687 688 private HdmiCecLocalDevicePlayback playback() { 689 return (HdmiCecLocalDevicePlayback) mCecController.getLocalDevice(HdmiCec.DEVICE_PLAYBACK); 690 } 691} 692