HdmiControlService.java revision 8b308d93c8fdcc7304b33d9b445ae3807eae97c8
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.IHdmiControlCallback; 25import android.hardware.hdmi.IHdmiControlService; 26import android.hardware.hdmi.IHdmiHotplugEventListener; 27import android.os.Handler; 28import android.os.HandlerThread; 29import android.os.IBinder; 30import android.os.Looper; 31import android.os.RemoteException; 32import android.util.Slog; 33import android.util.SparseIntArray; 34 35import com.android.internal.annotations.GuardedBy; 36import com.android.server.SystemService; 37import com.android.server.hdmi.DeviceDiscoveryAction.DeviceDiscoveryCallback; 38import com.android.server.hdmi.HdmiCecLocalDevice.AddressAllocationCallback; 39 40import java.util.ArrayList; 41import java.util.Iterator; 42import java.util.LinkedList; 43import java.util.List; 44import java.util.Locale; 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 /** 61 * Interface to report send result. 62 */ 63 interface SendMessageCallback { 64 /** 65 * Called when {@link HdmiControlService#sendCecCommand} is completed. 66 * 67 * @param error result of send request. 68 * @see {@link #SEND_RESULT_SUCCESS} 69 * @see {@link #SEND_RESULT_NAK} 70 * @see {@link #SEND_RESULT_FAILURE} 71 */ 72 void onSendCompleted(int error); 73 } 74 75 /** 76 * Interface to get a list of available logical devices. 77 */ 78 interface DevicePollingCallback { 79 /** 80 * Called when device polling is finished. 81 * 82 * @param ackedAddress a list of logical addresses of available devices 83 */ 84 void onPollingFinished(List<Integer> ackedAddress); 85 } 86 87 // A thread to handle synchronous IO of CEC and MHL control service. 88 // Since all of CEC and MHL HAL interfaces processed in short time (< 200ms) 89 // and sparse call it shares a thread to handle IO operations. 90 private final HandlerThread mIoThread = new HandlerThread("Hdmi Control Io Thread"); 91 92 // A collection of FeatureAction. 93 // Note that access to this collection should happen in service thread. 94 private final LinkedList<FeatureAction> mActions = new LinkedList<>(); 95 96 // Used to synchronize the access to the service. 97 private final Object mLock = new Object(); 98 99 // Type of logical devices hosted in the system. 100 @GuardedBy("mLock") 101 private final int[] mLocalDevices; 102 103 // List of listeners registered by callers that want to get notified of 104 // hotplug events. 105 private final ArrayList<IHdmiHotplugEventListener> mHotplugEventListeners = new ArrayList<>(); 106 107 // List of records for hotplug event listener to handle the the caller killed in action. 108 private final ArrayList<HotplugEventListenerRecord> mHotplugEventListenerRecords = 109 new ArrayList<>(); 110 111 @Nullable 112 private HdmiCecController mCecController; 113 114 @Nullable 115 private HdmiMhlController mMhlController; 116 117 // Whether ARC is "enabled" or not. 118 // TODO: it may need to hold lock if it's accessed from others. 119 private boolean mArcStatusEnabled = false; 120 121 // Handler running on service thread. It's used to run a task in service thread. 122 private Handler mHandler = new Handler(); 123 124 public HdmiControlService(Context context) { 125 super(context); 126 mLocalDevices = 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 if (mCecController != null) { 135 mCecController.initializeLocalDevices(mLocalDevices, new AddressAllocationCallback() { 136 private final SparseIntArray mAllocated = new SparseIntArray(); 137 138 @Override 139 public void onAddressAllocated(int deviceType, int logicalAddress) { 140 mAllocated.append(deviceType, logicalAddress); 141 // TODO: get HdmiLCecLocalDevice and call onAddressAllocated here. 142 143 // Once all logical allocation is done, launch device discovery 144 // action if one of local device is TV. 145 int tvAddress = mAllocated.get(HdmiCec.DEVICE_TV, -1); 146 if (mLocalDevices.length == mAllocated.size() && tvAddress != -1) { 147 launchDeviceDiscovery(tvAddress); 148 } 149 } 150 }); 151 } else { 152 Slog.i(TAG, "Device does not support HDMI-CEC."); 153 } 154 155 mMhlController = HdmiMhlController.create(this); 156 if (mMhlController == null) { 157 Slog.i(TAG, "Device does not support MHL-control."); 158 } 159 160 publishBinderService(Context.HDMI_CONTROL_SERVICE, new BinderService()); 161 } 162 163 /** 164 * Returns {@link Looper} for IO operation. 165 * 166 * <p>Declared as package-private. 167 */ 168 Looper getIoLooper() { 169 return mIoThread.getLooper(); 170 } 171 172 /** 173 * Returns {@link Looper} of main thread. Use this {@link Looper} instance 174 * for tasks that are running on main service thread. 175 * 176 * <p>Declared as package-private. 177 */ 178 Looper getServiceLooper() { 179 return mHandler.getLooper(); 180 } 181 182 /** 183 * Add and start a new {@link FeatureAction} to the action queue. 184 * 185 * @param action {@link FeatureAction} to add and start 186 */ 187 void addAndStartAction(final FeatureAction action) { 188 // TODO: may need to check the number of stale actions. 189 runOnServiceThread(new Runnable() { 190 @Override 191 public void run() { 192 mActions.add(action); 193 action.start(); 194 } 195 }); 196 } 197 198 // See if we have an action of a given type in progress. 199 private <T extends FeatureAction> boolean hasAction(final Class<T> clazz) { 200 for (FeatureAction action : mActions) { 201 if (action.getClass().equals(clazz)) { 202 return true; 203 } 204 } 205 return false; 206 } 207 208 /** 209 * Remove the given {@link FeatureAction} object from the action queue. 210 * 211 * @param action {@link FeatureAction} to remove 212 */ 213 void removeAction(final FeatureAction action) { 214 runOnServiceThread(new Runnable() { 215 @Override 216 public void run() { 217 mActions.remove(action); 218 } 219 }); 220 } 221 222 // Remove all actions matched with the given Class type. 223 private <T extends FeatureAction> void removeAction(final Class<T> clazz) { 224 runOnServiceThread(new Runnable() { 225 @Override 226 public void run() { 227 Iterator<FeatureAction> iter = mActions.iterator(); 228 while (iter.hasNext()) { 229 FeatureAction action = iter.next(); 230 if (action.getClass().equals(clazz)) { 231 action.clear(); 232 mActions.remove(action); 233 } 234 } 235 } 236 }); 237 } 238 239 private void runOnServiceThread(Runnable runnable) { 240 mHandler.post(runnable); 241 } 242 243 /** 244 * Change ARC status into the given {@code enabled} status. 245 * 246 * @return {@code true} if ARC was in "Enabled" status 247 */ 248 boolean setArcStatus(boolean enabled) { 249 boolean oldStatus = mArcStatusEnabled; 250 // 1. Enable/disable ARC circuit. 251 // TODO: call set_audio_return_channel of hal interface. 252 253 // 2. Update arc status; 254 mArcStatusEnabled = enabled; 255 return oldStatus; 256 } 257 258 /** 259 * Transmit a CEC command to CEC bus. 260 * 261 * @param command CEC command to send out 262 * @param callback interface used to the result of send command 263 */ 264 void sendCecCommand(HdmiCecMessage command, @Nullable SendMessageCallback callback) { 265 mCecController.sendCommand(command, callback); 266 } 267 268 void sendCecCommand(HdmiCecMessage command) { 269 mCecController.sendCommand(command, null); 270 } 271 272 /** 273 * Add a new {@link HdmiCecDeviceInfo} to controller. 274 * 275 * @param deviceInfo new device information object to add 276 */ 277 void addDeviceInfo(HdmiCecDeviceInfo deviceInfo) { 278 // TODO: Implement this. 279 } 280 281 boolean handleCecCommand(HdmiCecMessage message) { 282 // Commands that queries system information replies directly instead 283 // of creating FeatureAction because they are state-less. 284 switch (message.getOpcode()) { 285 case HdmiCec.MESSAGE_GET_MENU_LANGUAGE: 286 handleGetMenuLanguage(message); 287 return true; 288 case HdmiCec.MESSAGE_GIVE_OSD_NAME: 289 handleGiveOsdName(message); 290 return true; 291 case HdmiCec.MESSAGE_GIVE_PHYSICAL_ADDRESS: 292 handleGivePhysicalAddress(message); 293 return true; 294 case HdmiCec.MESSAGE_GIVE_DEVICE_VENDOR_ID: 295 handleGiveDeviceVendorId(message); 296 return true; 297 case HdmiCec.MESSAGE_GET_CEC_VERSION: 298 handleGetCecVersion(message); 299 return true; 300 case HdmiCec.MESSAGE_INITIATE_ARC: 301 handleInitiateArc(message); 302 return true; 303 case HdmiCec.MESSAGE_TERMINATE_ARC: 304 handleTerminateArc(message); 305 return true; 306 case HdmiCec.MESSAGE_REPORT_PHYSICAL_ADDRESS: 307 handleReportPhysicalAddress(message); 308 return true; 309 // TODO: Add remaining system information query such as 310 // <Give Device Power Status> and <Request Active Source> handler. 311 default: 312 return dispatchMessageToAction(message); 313 } 314 } 315 316 /** 317 * Called when a new hotplug event is issued. 318 * 319 * @param portNo hdmi port number where hot plug event issued. 320 * @param connected whether to be plugged in or not 321 */ 322 void onHotplug(int portNo, boolean connected) { 323 // TODO: Start "RequestArcInitiationAction" if ARC port. 324 } 325 326 /** 327 * Poll all remote devices. It sends <Polling Message> to all remote 328 * devices. 329 * 330 * @param callback an interface used to get a list of all remote devices' address 331 * @param retryCount the number of retry used to send polling message to remote devices 332 */ 333 void pollDevices(DevicePollingCallback callback, int retryCount) { 334 mCecController.pollDevices(callback, retryCount); 335 } 336 337 private void handleReportPhysicalAddress(HdmiCecMessage message) { 338 // At first, try to consume it. 339 if (dispatchMessageToAction(message)) { 340 return; 341 } 342 343 // Ignore if [Device Discovery Action] is on going ignore message. 344 if (hasAction(DeviceDiscoveryAction.class)) { 345 Slog.i(TAG, "Ignore unrecognizable <Report Physical Address> " 346 + "because Device Discovery Action is on-going:" + message); 347 return; 348 } 349 350 // TODO: start new device action. 351 } 352 353 private void handleInitiateArc(HdmiCecMessage message){ 354 // In case where <Initiate Arc> is started by <Request ARC Initiation> 355 // need to clean up RequestArcInitiationAction. 356 removeAction(RequestArcInitiationAction.class); 357 SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this, 358 message.getDestination(), message.getSource(), true); 359 addAndStartAction(action); 360 } 361 362 private void handleTerminateArc(HdmiCecMessage message) { 363 // In case where <Terminate Arc> is started by <Request ARC Termination> 364 // need to clean up RequestArcInitiationAction. 365 // TODO: check conditions of power status by calling is_connected api 366 // to be added soon. 367 removeAction(RequestArcTerminationAction.class); 368 SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this, 369 message.getDestination(), message.getSource(), false); 370 addAndStartAction(action); 371 } 372 373 private void handleGetCecVersion(HdmiCecMessage message) { 374 int version = mCecController.getVersion(); 375 HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildCecVersion(message.getDestination(), 376 message.getSource(), 377 version); 378 sendCecCommand(cecMessage); 379 } 380 381 private void handleGiveDeviceVendorId(HdmiCecMessage message) { 382 int vendorId = mCecController.getVendorId(); 383 HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildDeviceVendorIdCommand( 384 message.getDestination(), vendorId); 385 sendCecCommand(cecMessage); 386 } 387 388 private void handleGivePhysicalAddress(HdmiCecMessage message) { 389 int physicalAddress = mCecController.getPhysicalAddress(); 390 int deviceType = HdmiCec.getTypeFromAddress(message.getDestination()); 391 HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildReportPhysicalAddressCommand( 392 message.getDestination(), physicalAddress, deviceType); 393 sendCecCommand(cecMessage); 394 } 395 396 private void handleGiveOsdName(HdmiCecMessage message) { 397 // TODO: read device name from settings or property. 398 String name = HdmiCec.getDefaultDeviceName(message.getDestination()); 399 HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildSetOsdNameCommand( 400 message.getDestination(), message.getSource(), name); 401 if (cecMessage != null) { 402 sendCecCommand(cecMessage); 403 } else { 404 Slog.w(TAG, "Failed to build <Get Osd Name>:" + name); 405 } 406 } 407 408 private void handleGetMenuLanguage(HdmiCecMessage message) { 409 // Only 0 (TV), 14 (specific use) can answer. 410 if (message.getDestination() != HdmiCec.ADDR_TV 411 && message.getDestination() != HdmiCec.ADDR_SPECIFIC_USE) { 412 Slog.w(TAG, "Only TV can handle <Get Menu Language>:" + message.toString()); 413 sendCecCommand( 414 HdmiCecMessageBuilder.buildFeatureAbortCommand(message.getDestination(), 415 message.getSource(), HdmiCec.MESSAGE_GET_MENU_LANGUAGE, 416 HdmiCecMessageBuilder.ABORT_UNRECOGNIZED_MODE)); 417 return; 418 } 419 420 HdmiCecMessage command = HdmiCecMessageBuilder.buildSetMenuLanguageCommand( 421 message.getDestination(), 422 Locale.getDefault().getISO3Language()); 423 // TODO: figure out how to handle failed to get language code. 424 if (command != null) { 425 sendCecCommand(command); 426 } else { 427 Slog.w(TAG, "Failed to respond to <Get Menu Language>: " + message.toString()); 428 } 429 } 430 431 private boolean dispatchMessageToAction(HdmiCecMessage message) { 432 for (FeatureAction action : mActions) { 433 if (action.processCommand(message)) { 434 return true; 435 } 436 } 437 Slog.w(TAG, "Unsupported cec command:" + message); 438 return false; 439 } 440 441 // Record class that monitors the event of the caller of being killed. Used to clean up 442 // the listener list and record list accordingly. 443 private final class HotplugEventListenerRecord implements IBinder.DeathRecipient { 444 private final IHdmiHotplugEventListener mListener; 445 446 public HotplugEventListenerRecord(IHdmiHotplugEventListener listener) { 447 mListener = listener; 448 } 449 450 @Override 451 public void binderDied() { 452 synchronized (mLock) { 453 mHotplugEventListenerRecords.remove(this); 454 mHotplugEventListeners.remove(mListener); 455 } 456 } 457 } 458 459 void addCecDevice(HdmiCecDeviceInfo info) { 460 mCecController.addDeviceInfo(info); 461 } 462 463 // Launch device discovery sequence. 464 // It starts with clearing the existing device info list. 465 // Note that it assumes that logical address of all local devices is already allocated. 466 private void launchDeviceDiscovery(int sourceAddress) { 467 // At first, clear all existing device infos. 468 mCecController.clearDeviceInfoList(); 469 470 // TODO: check whether TV is one of local devices. 471 DeviceDiscoveryAction action = new DeviceDiscoveryAction(this, sourceAddress, 472 new DeviceDiscoveryCallback() { 473 @Override 474 public void onDeviceDiscoveryDone(List<HdmiCecDeviceInfo> deviceInfos) { 475 for (HdmiCecDeviceInfo info : deviceInfos) { 476 mCecController.addDeviceInfo(info); 477 } 478 479 // Add device info of all local devices. 480 for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) { 481 mCecController.addDeviceInfo(device.getDeviceInfo()); 482 } 483 484 // TODO: start hot-plug detection sequence here. 485 // addAndStartAction(new HotplugDetectionAction()); 486 } 487 }); 488 addAndStartAction(action); 489 } 490 491 private void enforceAccessPermission() { 492 getContext().enforceCallingOrSelfPermission(PERMISSION, TAG); 493 } 494 495 private final class BinderService extends IHdmiControlService.Stub { 496 @Override 497 public int[] getSupportedTypes() { 498 enforceAccessPermission(); 499 synchronized (mLock) { 500 return mLocalDevices; 501 } 502 } 503 504 @Override 505 public void oneTouchPlay(final IHdmiControlCallback callback) { 506 enforceAccessPermission(); 507 runOnServiceThread(new Runnable() { 508 @Override 509 public void run() { 510 HdmiControlService.this.oneTouchPlay(callback); 511 } 512 }); 513 } 514 515 @Override 516 public void queryDisplayStatus(final IHdmiControlCallback callback) { 517 enforceAccessPermission(); 518 runOnServiceThread(new Runnable() { 519 @Override 520 public void run() { 521 HdmiControlService.this.queryDisplayStatus(callback); 522 } 523 }); 524 } 525 526 @Override 527 public void addHotplugEventListener(final IHdmiHotplugEventListener listener) { 528 enforceAccessPermission(); 529 runOnServiceThread(new Runnable() { 530 @Override 531 public void run() { 532 HdmiControlService.this.addHotplugEventListener(listener); 533 } 534 }); 535 } 536 537 @Override 538 public void removeHotplugEventListener(final IHdmiHotplugEventListener listener) { 539 enforceAccessPermission(); 540 runOnServiceThread(new Runnable() { 541 @Override 542 public void run() { 543 HdmiControlService.this.removeHotplugEventListener(listener); 544 } 545 }); 546 } 547 } 548 549 private void oneTouchPlay(IHdmiControlCallback callback) { 550 if (hasAction(OneTouchPlayAction.class)) { 551 Slog.w(TAG, "oneTouchPlay already in progress"); 552 invokeCallback(callback, HdmiCec.RESULT_ALREADY_IN_PROGRESS); 553 return; 554 } 555 HdmiCecLocalDevice source = mCecController.getLocalDevice(HdmiCec.DEVICE_PLAYBACK); 556 if (source == null) { 557 Slog.w(TAG, "Local playback device not available"); 558 invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE); 559 return; 560 } 561 // TODO: Consider the case of multiple TV sets. For now we always direct the command 562 // to the primary one. 563 OneTouchPlayAction action = OneTouchPlayAction.create(this, 564 source.getDeviceInfo().getLogicalAddress(), 565 source.getDeviceInfo().getPhysicalAddress(), HdmiCec.ADDR_TV, callback); 566 if (action == null) { 567 Slog.w(TAG, "Cannot initiate oneTouchPlay"); 568 invokeCallback(callback, HdmiCec.RESULT_EXCEPTION); 569 return; 570 } 571 addAndStartAction(action); 572 } 573 574 private void queryDisplayStatus(IHdmiControlCallback callback) { 575 if (hasAction(DevicePowerStatusAction.class)) { 576 Slog.w(TAG, "queryDisplayStatus already in progress"); 577 invokeCallback(callback, HdmiCec.RESULT_ALREADY_IN_PROGRESS); 578 return; 579 } 580 HdmiCecLocalDevice source = mCecController.getLocalDevice(HdmiCec.DEVICE_PLAYBACK); 581 if (source == null) { 582 Slog.w(TAG, "Local playback device not available"); 583 invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE); 584 return; 585 } 586 DevicePowerStatusAction action = DevicePowerStatusAction.create(this, 587 source.getDeviceInfo().getLogicalAddress(), HdmiCec.ADDR_TV, callback); 588 if (action == null) { 589 Slog.w(TAG, "Cannot initiate queryDisplayStatus"); 590 invokeCallback(callback, HdmiCec.RESULT_EXCEPTION); 591 return; 592 } 593 addAndStartAction(action); 594 } 595 596 private void addHotplugEventListener(IHdmiHotplugEventListener listener) { 597 HotplugEventListenerRecord record = new HotplugEventListenerRecord(listener); 598 try { 599 listener.asBinder().linkToDeath(record, 0); 600 } catch (RemoteException e) { 601 Slog.w(TAG, "Listener already died"); 602 return; 603 } 604 synchronized (mLock) { 605 mHotplugEventListenerRecords.add(record); 606 mHotplugEventListeners.add(listener); 607 } 608 } 609 610 private void removeHotplugEventListener(IHdmiHotplugEventListener listener) { 611 synchronized (mLock) { 612 for (HotplugEventListenerRecord record : mHotplugEventListenerRecords) { 613 if (record.mListener.asBinder() == listener.asBinder()) { 614 listener.asBinder().unlinkToDeath(record, 0); 615 mHotplugEventListenerRecords.remove(record); 616 break; 617 } 618 } 619 mHotplugEventListeners.remove(listener); 620 } 621 } 622 623 private void invokeCallback(IHdmiControlCallback callback, int result) { 624 try { 625 callback.onComplete(result); 626 } catch (RemoteException e) { 627 Slog.e(TAG, "Invoking callback failed:" + e); 628 } 629 } 630} 631