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