HdmiControlService.java revision d42a7a322b7adf532ae0b70cb9eb1df7e62a8f2d
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 Slog.w(TAG, "Unsupported cec command:" + message.toString()); 294 return false; 295 } 296 } 297 298 /** 299 * Called when a new hotplug event is issued. 300 * 301 * @param port hdmi port number where hot plug event issued. 302 * @param connected whether to be plugged in or not 303 */ 304 void onHotplug(int portNo, boolean connected) { 305 // TODO: Start "RequestArcInitiationAction" if ARC port. 306 } 307 308 /** 309 * Poll all remote devices. It sends <Polling Message> to all remote 310 * devices. 311 * 312 * @param callback an interface used to get a list of all remote devices' address 313 * @param retryCount the number of retry used to send polling message to remote devices 314 */ 315 void pollDevices(DevicePollingCallback callback, int retryCount) { 316 mCecController.pollDevices(callback, retryCount); 317 } 318 319 private void handleInitiateArc(HdmiCecMessage message){ 320 // In case where <Initiate Arc> is started by <Request ARC Initiation> 321 // need to clean up RequestArcInitiationAction. 322 removeAction(RequestArcInitiationAction.class); 323 SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this, 324 message.getDestination(), message.getSource(), true); 325 addAndStartAction(action); 326 } 327 328 private void handleTerminateArc(HdmiCecMessage message) { 329 // In case where <Terminate Arc> is started by <Request ARC Termination> 330 // need to clean up RequestArcInitiationAction. 331 // TODO: check conditions of power status by calling is_connected api 332 // to be added soon. 333 removeAction(RequestArcTerminationAction.class); 334 SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this, 335 message.getDestination(), message.getSource(), false); 336 addAndStartAction(action); 337 } 338 339 private void handleGetCecVersion(HdmiCecMessage message) { 340 int version = mCecController.getVersion(); 341 HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildCecVersion(message.getDestination(), 342 message.getSource(), 343 version); 344 sendCecCommand(cecMessage); 345 } 346 347 private void handleGiveDeviceVendorId(HdmiCecMessage message) { 348 int vendorId = mCecController.getVendorId(); 349 HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildDeviceVendorIdCommand( 350 message.getDestination(), vendorId); 351 sendCecCommand(cecMessage); 352 } 353 354 private void handleGivePhysicalAddress(HdmiCecMessage message) { 355 int physicalAddress = mCecController.getPhysicalAddress(); 356 int deviceType = HdmiCec.getTypeFromAddress(message.getDestination()); 357 HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildReportPhysicalAddressCommand( 358 message.getDestination(), physicalAddress, deviceType); 359 sendCecCommand(cecMessage); 360 } 361 362 private void handleGiveOsdName(HdmiCecMessage message) { 363 // TODO: read device name from settings or property. 364 String name = HdmiCec.getDefaultDeviceName(message.getDestination()); 365 HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildSetOsdNameCommand( 366 message.getDestination(), message.getSource(), name); 367 if (cecMessage != null) { 368 sendCecCommand(cecMessage); 369 } else { 370 Slog.w(TAG, "Failed to build <Get Osd Name>:" + name); 371 } 372 } 373 374 private void handleGetMenuLanguage(HdmiCecMessage message) { 375 // Only 0 (TV), 14 (specific use) can answer. 376 if (message.getDestination() != HdmiCec.ADDR_TV 377 && message.getDestination() != HdmiCec.ADDR_SPECIFIC_USE) { 378 Slog.w(TAG, "Only TV can handle <Get Menu Language>:" + message.toString()); 379 sendCecCommand( 380 HdmiCecMessageBuilder.buildFeatureAbortCommand(message.getDestination(), 381 message.getSource(), HdmiCec.MESSAGE_GET_MENU_LANGUAGE, 382 HdmiCecMessageBuilder.ABORT_UNRECOGNIZED_MODE)); 383 return; 384 } 385 386 HdmiCecMessage command = HdmiCecMessageBuilder.buildSetMenuLanguageCommand( 387 message.getDestination(), 388 Locale.getDefault().getISO3Language()); 389 // TODO: figure out how to handle failed to get language code. 390 if (command != null) { 391 sendCecCommand(command); 392 } else { 393 Slog.w(TAG, "Failed to respond to <Get Menu Language>: " + message.toString()); 394 } 395 } 396 397 // Record class that monitors the event of the caller of being killed. Used to clean up 398 // the listener list and record list accordingly. 399 private final class HotplugEventListenerRecord implements IBinder.DeathRecipient { 400 private final IHdmiHotplugEventListener mListener; 401 402 public HotplugEventListenerRecord(IHdmiHotplugEventListener listener) { 403 mListener = listener; 404 } 405 406 @Override 407 public void binderDied() { 408 synchronized (mLock) { 409 mHotplugEventListenerRecords.remove(this); 410 mHotplugEventListeners.remove(mListener); 411 } 412 } 413 } 414 415 void addCecDevice(HdmiCecDeviceInfo info) { 416 mCecController.addDeviceInfo(info); 417 } 418 419 // Launch device discovery sequence. 420 // It starts with clearing the existing device info list. 421 // Note that it assumes that logical address of all local devices is already allocated. 422 void launchDeviceDiscovery() { 423 // At first, clear all existing device infos. 424 mCecController.clearDeviceInfoList(); 425 426 // TODO: check whether TV is one of local devices. 427 DeviceDiscoveryAction action = new DeviceDiscoveryAction(this, HdmiCec.ADDR_TV, 428 new DeviceDiscoveryCallback() { 429 @Override 430 public void onDeviceDiscoveryDone(List<HdmiCecDeviceInfo> deviceInfos) { 431 for (HdmiCecDeviceInfo info : deviceInfos) { 432 mCecController.addDeviceInfo(info); 433 } 434 435 // Add device info of all local devices. 436 for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) { 437 mCecController.addDeviceInfo(device.getDeviceInfo()); 438 } 439 440 // TODO: start hot-plug detection sequence here. 441 // addAndStartAction(new HotplugDetectionAction()); 442 } 443 }); 444 addAndStartAction(action); 445 } 446 447 private void enforceAccessPermission() { 448 getContext().enforceCallingOrSelfPermission(PERMISSION, TAG); 449 } 450 451 private final class BinderService extends IHdmiControlService.Stub { 452 @Override 453 public int[] getSupportedTypes() { 454 enforceAccessPermission(); 455 synchronized (mLock) { 456 return mLocalDevices; 457 } 458 } 459 460 @Override 461 public void oneTouchPlay(final IHdmiControlCallback callback) { 462 enforceAccessPermission(); 463 runOnServiceThread(new Runnable() { 464 @Override 465 public void run() { 466 HdmiControlService.this.oneTouchPlay(callback); 467 } 468 }); 469 } 470 471 @Override 472 public void queryDisplayStatus(final IHdmiControlCallback callback) { 473 enforceAccessPermission(); 474 runOnServiceThread(new Runnable() { 475 @Override 476 public void run() { 477 HdmiControlService.this.queryDisplayStatus(callback); 478 } 479 }); 480 } 481 482 @Override 483 public void addHotplugEventListener(final IHdmiHotplugEventListener listener) { 484 enforceAccessPermission(); 485 runOnServiceThread(new Runnable() { 486 @Override 487 public void run() { 488 HdmiControlService.this.addHotplugEventListener(listener); 489 } 490 }); 491 } 492 493 @Override 494 public void removeHotplugEventListener(final IHdmiHotplugEventListener listener) { 495 enforceAccessPermission(); 496 runOnServiceThread(new Runnable() { 497 @Override 498 public void run() { 499 HdmiControlService.this.removeHotplugEventListener(listener); 500 } 501 }); 502 } 503 } 504 505 private void oneTouchPlay(IHdmiControlCallback callback) { 506 if (hasAction(OneTouchPlayAction.class)) { 507 Slog.w(TAG, "oneTouchPlay already in progress"); 508 invokeCallback(callback, HdmiCec.RESULT_ALREADY_IN_PROGRESS); 509 return; 510 } 511 HdmiCecLocalDevice source = mCecController.getLocalDevice(HdmiCec.DEVICE_PLAYBACK); 512 if (source == null) { 513 Slog.w(TAG, "Local playback device not available"); 514 invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE); 515 return; 516 } 517 // TODO: Consider the case of multiple TV sets. For now we always direct the command 518 // to the primary one. 519 OneTouchPlayAction action = OneTouchPlayAction.create(this, 520 source.getDeviceInfo().getLogicalAddress(), 521 source.getDeviceInfo().getPhysicalAddress(), HdmiCec.ADDR_TV, callback); 522 if (action == null) { 523 Slog.w(TAG, "Cannot initiate oneTouchPlay"); 524 invokeCallback(callback, HdmiCec.RESULT_EXCEPTION); 525 return; 526 } 527 addAndStartAction(action); 528 } 529 530 private void queryDisplayStatus(IHdmiControlCallback callback) { 531 if (hasAction(DevicePowerStatusAction.class)) { 532 Slog.w(TAG, "queryDisplayStatus already in progress"); 533 invokeCallback(callback, HdmiCec.RESULT_ALREADY_IN_PROGRESS); 534 return; 535 } 536 HdmiCecLocalDevice source = mCecController.getLocalDevice(HdmiCec.DEVICE_PLAYBACK); 537 if (source == null) { 538 Slog.w(TAG, "Local playback device not available"); 539 invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE); 540 return; 541 } 542 DevicePowerStatusAction action = DevicePowerStatusAction.create(this, 543 source.getDeviceInfo().getLogicalAddress(), HdmiCec.ADDR_TV, callback); 544 if (action == null) { 545 Slog.w(TAG, "Cannot initiate queryDisplayStatus"); 546 invokeCallback(callback, HdmiCec.RESULT_EXCEPTION); 547 return; 548 } 549 addAndStartAction(action); 550 } 551 552 private void addHotplugEventListener(IHdmiHotplugEventListener listener) { 553 HotplugEventListenerRecord record = new HotplugEventListenerRecord(listener); 554 try { 555 listener.asBinder().linkToDeath(record, 0); 556 } catch (RemoteException e) { 557 Slog.w(TAG, "Listener already died"); 558 return; 559 } 560 synchronized (mLock) { 561 mHotplugEventListenerRecords.add(record); 562 mHotplugEventListeners.add(listener); 563 } 564 } 565 566 private void removeHotplugEventListener(IHdmiHotplugEventListener listener) { 567 synchronized (mLock) { 568 for (HotplugEventListenerRecord record : mHotplugEventListenerRecords) { 569 if (record.mListener.asBinder() == listener.asBinder()) { 570 listener.asBinder().unlinkToDeath(record, 0); 571 mHotplugEventListenerRecords.remove(record); 572 break; 573 } 574 } 575 mHotplugEventListeners.remove(listener); 576 } 577 } 578 579 private void invokeCallback(IHdmiControlCallback callback, int result) { 580 try { 581 callback.onComplete(result); 582 } catch (RemoteException e) { 583 Slog.e(TAG, "Invoking callback failed:" + e); 584 } 585 } 586} 587