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