HdmiControlService.java revision 16e24a2d143345b2052ea7ccbe85fcda6d843608
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; 36 37import java.util.ArrayList; 38import java.util.Iterator; 39import java.util.LinkedList; 40import java.util.List; 41import java.util.Locale; 42 43/** 44 * Provides a service for sending and processing HDMI control messages, 45 * HDMI-CEC and MHL control command, and providing the information on both standard. 46 */ 47public final class HdmiControlService extends SystemService { 48 private static final String TAG = "HdmiControlService"; 49 50 // TODO: Rename the permission to HDMI_CONTROL. 51 private static final String PERMISSION = "android.permission.HDMI_CEC"; 52 53 static final int SEND_RESULT_SUCCESS = 0; 54 static final int SEND_RESULT_NAK = -1; 55 static final int SEND_RESULT_FAILURE = -2; 56 57 /** 58 * Interface to report send result. 59 */ 60 interface SendMessageCallback { 61 /** 62 * Called when {@link HdmiControlService#sendCecCommand} is completed. 63 * 64 * @param error result of send request. 65 * @see {@link #SEND_RESULT_SUCCESS} 66 * @see {@link #SEND_RESULT_NAK} 67 * @see {@link #SEND_RESULT_FAILURE} 68 */ 69 void onSendCompleted(int error); 70 } 71 72 /** 73 * Interface to get a list of available logical devices. 74 */ 75 interface DevicePollingCallback { 76 /** 77 * Called when device polling is finished. 78 * 79 * @param ackedAddress a list of logical addresses of available devices 80 */ 81 void onPollingFinished(List<Integer> ackedAddress); 82 } 83 84 // A thread to handle synchronous IO of CEC and MHL control service. 85 // Since all of CEC and MHL HAL interfaces processed in short time (< 200ms) 86 // and sparse call it shares a thread to handle IO operations. 87 private final HandlerThread mIoThread = new HandlerThread("Hdmi Control Io Thread"); 88 89 // A collection of FeatureAction. 90 // Note that access to this collection should happen in service thread. 91 private final LinkedList<FeatureAction> mActions = new LinkedList<>(); 92 93 // Used to synchronize the access to the service. 94 private final Object mLock = new Object(); 95 96 // Type of logical devices hosted in the system. 97 @GuardedBy("mLock") 98 private final int[] mLocalDevices; 99 100 // List of listeners registered by callers that want to get notified of 101 // hotplug events. 102 private final ArrayList<IHdmiHotplugEventListener> mHotplugEventListeners = new ArrayList<>(); 103 104 // List of records for hotplug event listener to handle the the caller killed in action. 105 private final ArrayList<HotplugEventListenerRecord> mHotplugEventListenerRecords = 106 new ArrayList<>(); 107 108 @Nullable 109 private HdmiCecController mCecController; 110 111 @Nullable 112 private HdmiMhlController mMhlController; 113 114 // Whether ARC is "enabled" or not. 115 // TODO: it may need to hold lock if it's accessed from others. 116 private boolean mArcStatusEnabled = false; 117 118 // Handler running on service thread. It's used to run a task in service thread. 119 private Handler mHandler = new Handler(); 120 121 public HdmiControlService(Context context) { 122 super(context); 123 mLocalDevices = getContext().getResources().getIntArray( 124 com.android.internal.R.array.config_hdmiCecLogicalDeviceType); 125 } 126 127 @Override 128 public void onStart() { 129 mIoThread.start(); 130 mCecController = HdmiCecController.create(this); 131 if (mCecController != null) { 132 mCecController.initializeLocalDevices(mLocalDevices); 133 } else { 134 Slog.i(TAG, "Device does not support HDMI-CEC."); 135 } 136 137 mMhlController = HdmiMhlController.create(this); 138 if (mMhlController == null) { 139 Slog.i(TAG, "Device does not support MHL-control."); 140 } 141 142 // TODO: Publish the BinderService 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 Slog.w(TAG, "Unsupported cec command:" + message.toString()); 293 return false; 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 // Record class that monitors the event of the caller of being killed. Used to clean up 397 // the listener list and record list accordingly. 398 private final class HotplugEventListenerRecord implements IBinder.DeathRecipient { 399 private final IHdmiHotplugEventListener mListener; 400 401 public HotplugEventListenerRecord(IHdmiHotplugEventListener listener) { 402 mListener = listener; 403 } 404 405 @Override 406 public void binderDied() { 407 synchronized (mLock) { 408 mHotplugEventListenerRecords.remove(this); 409 mHotplugEventListeners.remove(mListener); 410 } 411 } 412 } 413 414 private void enforceAccessPermission() { 415 getContext().enforceCallingOrSelfPermission(PERMISSION, TAG); 416 } 417 418 private final class BinderService extends IHdmiControlService.Stub { 419 @Override 420 public int[] getSupportedTypes() { 421 enforceAccessPermission(); 422 synchronized (mLock) { 423 return mLocalDevices; 424 } 425 } 426 427 @Override 428 public void oneTouchPlay(final IHdmiControlCallback callback) { 429 enforceAccessPermission(); 430 runOnServiceThread(new Runnable() { 431 @Override 432 public void run() { 433 HdmiControlService.this.oneTouchPlay(callback); 434 } 435 }); 436 } 437 438 @Override 439 public void queryDisplayStatus(final IHdmiControlCallback callback) { 440 enforceAccessPermission(); 441 runOnServiceThread(new Runnable() { 442 @Override 443 public void run() { 444 HdmiControlService.this.queryDisplayStatus(callback); 445 } 446 }); 447 } 448 449 @Override 450 public void addHotplugEventListener(final IHdmiHotplugEventListener listener) { 451 enforceAccessPermission(); 452 runOnServiceThread(new Runnable() { 453 @Override 454 public void run() { 455 HdmiControlService.this.addHotplugEventListener(listener); 456 } 457 }); 458 } 459 460 @Override 461 public void removeHotplugEventListener(final IHdmiHotplugEventListener listener) { 462 enforceAccessPermission(); 463 runOnServiceThread(new Runnable() { 464 @Override 465 public void run() { 466 HdmiControlService.this.removeHotplugEventListener(listener); 467 } 468 }); 469 } 470 } 471 472 private void oneTouchPlay(IHdmiControlCallback callback) { 473 if (hasAction(OneTouchPlayAction.class)) { 474 Slog.w(TAG, "oneTouchPlay already in progress"); 475 invokeCallback(callback, HdmiCec.RESULT_ALREADY_IN_PROGRESS); 476 return; 477 } 478 HdmiCecLocalDevice source = mCecController.getLocalDevice(HdmiCec.DEVICE_PLAYBACK); 479 if (source == null) { 480 Slog.w(TAG, "Local playback device not available"); 481 invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE); 482 return; 483 } 484 // TODO: Consider the case of multiple TV sets. For now we always direct the command 485 // to the primary one. 486 OneTouchPlayAction action = OneTouchPlayAction.create(this, 487 source.getDeviceInfo().getLogicalAddress(), 488 source.getDeviceInfo().getPhysicalAddress(), HdmiCec.ADDR_TV, callback); 489 if (action == null) { 490 Slog.w(TAG, "Cannot initiate oneTouchPlay"); 491 invokeCallback(callback, HdmiCec.RESULT_EXCEPTION); 492 return; 493 } 494 addAndStartAction(action); 495 } 496 497 private void queryDisplayStatus(IHdmiControlCallback callback) { 498 if (hasAction(DevicePowerStatusAction.class)) { 499 Slog.w(TAG, "queryDisplayStatus already in progress"); 500 invokeCallback(callback, HdmiCec.RESULT_ALREADY_IN_PROGRESS); 501 return; 502 } 503 HdmiCecLocalDevice source = mCecController.getLocalDevice(HdmiCec.DEVICE_PLAYBACK); 504 if (source == null) { 505 Slog.w(TAG, "Local playback device not available"); 506 invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE); 507 return; 508 } 509 DevicePowerStatusAction action = DevicePowerStatusAction.create(this, 510 source.getDeviceInfo().getLogicalAddress(), HdmiCec.ADDR_TV, callback); 511 if (action == null) { 512 Slog.w(TAG, "Cannot initiate queryDisplayStatus"); 513 invokeCallback(callback, HdmiCec.RESULT_EXCEPTION); 514 return; 515 } 516 addAndStartAction(action); 517 } 518 519 private void addHotplugEventListener(IHdmiHotplugEventListener listener) { 520 HotplugEventListenerRecord record = new HotplugEventListenerRecord(listener); 521 try { 522 listener.asBinder().linkToDeath(record, 0); 523 } catch (RemoteException e) { 524 Slog.w(TAG, "Listener already died"); 525 return; 526 } 527 synchronized (mLock) { 528 mHotplugEventListenerRecords.add(record); 529 mHotplugEventListeners.add(listener); 530 } 531 } 532 533 private void removeHotplugEventListener(IHdmiHotplugEventListener listener) { 534 synchronized (mLock) { 535 for (HotplugEventListenerRecord record : mHotplugEventListenerRecords) { 536 if (record.mListener.asBinder() == listener.asBinder()) { 537 listener.asBinder().unlinkToDeath(record, 0); 538 mHotplugEventListenerRecords.remove(record); 539 break; 540 } 541 } 542 mHotplugEventListeners.remove(listener); 543 } 544 } 545 546 private void invokeCallback(IHdmiControlCallback callback, int result) { 547 try { 548 callback.onComplete(result); 549 } catch (RemoteException e) { 550 Slog.e(TAG, "Invoking callback failed:" + e); 551 } 552 } 553} 554