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