HdmiCecLocalDeviceTv.java revision fa8e90db6a84ce1b19af86d46b547664d8a7ac37
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.hardware.hdmi.HdmiCec; 20import android.hardware.hdmi.HdmiCecDeviceInfo; 21import android.hardware.hdmi.HdmiCecMessage; 22import android.hardware.hdmi.IHdmiControlCallback; 23import android.os.RemoteException; 24import android.util.Slog; 25import android.util.SparseArray; 26 27import com.android.internal.annotations.GuardedBy; 28import com.android.server.hdmi.DeviceDiscoveryAction.DeviceDiscoveryCallback; 29 30import java.util.ArrayList; 31import java.util.Collections; 32import java.util.List; 33import java.util.Locale; 34 35/** 36 * Represent a logical device of type TV residing in Android system. 37 */ 38final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { 39 private static final String TAG = "HdmiCecLocalDeviceTv"; 40 41 // Whether ARC is "enabled" or not. 42 @GuardedBy("mLock") 43 private boolean mArcStatusEnabled = false; 44 45 // Whether SystemAudioMode is "On" or not. 46 @GuardedBy("mLock") 47 private boolean mSystemAudioMode; 48 49 50 // Copy of mDeviceInfos to guarantee thread-safety. 51 @GuardedBy("mLock") 52 private List<HdmiCecDeviceInfo> mSafeAllDeviceInfos = Collections.emptyList(); 53 // All external cec device which excludes local devices. 54 @GuardedBy("mLock") 55 private List<HdmiCecDeviceInfo> mSafeExternalDeviceInfos = Collections.emptyList(); 56 57 // Map-like container of all cec devices including local ones. 58 // A logical address of device is used as key of container. 59 // This is not thread-safe. For external purpose use mSafeDeviceInfos. 60 private final SparseArray<HdmiCecDeviceInfo> mDeviceInfos = new SparseArray<>(); 61 62 HdmiCecLocalDeviceTv(HdmiControlService service) { 63 super(service, HdmiCec.DEVICE_TV); 64 65 // TODO: load system audio mode and set it to mSystemAudioMode. 66 } 67 68 @Override 69 protected void onAddressAllocated(int logicalAddress) { 70 assertRunOnServiceThread(); 71 // TODO: vendor-specific initialization here. 72 73 mService.sendCecCommand(HdmiCecMessageBuilder.buildReportPhysicalAddressCommand( 74 mAddress, mService.getPhysicalAddress(), mDeviceType)); 75 mService.sendCecCommand(HdmiCecMessageBuilder.buildDeviceVendorIdCommand( 76 mAddress, mService.getVendorId())); 77 78 launchDeviceDiscovery(); 79 // TODO: Start routing control action, device discovery action. 80 } 81 82 /** 83 * Performs the action 'device select', or 'one touch play' initiated by TV. 84 * 85 * @param targetAddress logical address of the device to select 86 * @param callback callback object to report the result with 87 */ 88 void deviceSelect(int targetAddress, IHdmiControlCallback callback) { 89 assertRunOnServiceThread(); 90 HdmiCecDeviceInfo targetDevice = getDeviceInfo(targetAddress); 91 if (targetDevice == null) { 92 invokeCallback(callback, HdmiCec.RESULT_TARGET_NOT_AVAILABLE); 93 return; 94 } 95 removeAction(DeviceSelectAction.class); 96 addAndStartAction(new DeviceSelectAction(this, targetDevice, callback)); 97 } 98 99 /** 100 * Performs the action routing control. 101 * 102 * @param portId new HDMI port to route to 103 * @param callback callback object to report the result with 104 */ 105 void portSelect(int portId, IHdmiControlCallback callback) { 106 assertRunOnServiceThread(); 107 if (isInPresetInstallationMode()) { 108 invokeCallback(callback, HdmiCec.RESULT_INCORRECT_MODE); 109 return; 110 } 111 // Make sure this call does not stem from <Active Source> message reception, in 112 // which case the two ports will be the same. 113 if (portId == getActivePortId()) { 114 invokeCallback(callback, HdmiCec.RESULT_SUCCESS); 115 return; 116 } 117 setActivePortId(portId); 118 119 // TODO: Return immediately if the operation is triggered by <Text/Image View On> 120 // TODO: Handle invalid port id / active input which should be treated as an 121 // internal tuner. 122 123 removeAction(RoutingControlAction.class); 124 125 int oldPath = mService.portIdToPath(mService.portIdToPath(getActivePortId())); 126 int newPath = mService.portIdToPath(portId); 127 HdmiCecMessage routingChange = 128 HdmiCecMessageBuilder.buildRoutingChange(mAddress, oldPath, newPath); 129 mService.sendCecCommand(routingChange); 130 addAndStartAction(new RoutingControlAction(this, newPath, callback)); 131 } 132 133 /** 134 * Sends key to a target CEC device. 135 * 136 * @param keyCode key code to send. Defined in {@link android.view.KeyEvent}. 137 * @param isPressed true if this is keypress event 138 */ 139 void sendKeyEvent(int keyCode, boolean isPressed) { 140 assertRunOnServiceThread(); 141 List<SendKeyAction> action = getActions(SendKeyAction.class); 142 if (!action.isEmpty()) { 143 action.get(0).processKeyEvent(keyCode, isPressed); 144 } else { 145 if (isPressed) { 146 addAndStartAction(new SendKeyAction(this, getActiveSource(), keyCode)); 147 } else { 148 Slog.w(TAG, "Discard key release event"); 149 } 150 } 151 } 152 153 private static void invokeCallback(IHdmiControlCallback callback, int result) { 154 if (callback == null) { 155 return; 156 } 157 try { 158 callback.onComplete(result); 159 } catch (RemoteException e) { 160 Slog.e(TAG, "Invoking callback failed:" + e); 161 } 162 } 163 164 @Override 165 protected boolean handleGetMenuLanguage(HdmiCecMessage message) { 166 assertRunOnServiceThread(); 167 HdmiCecMessage command = HdmiCecMessageBuilder.buildSetMenuLanguageCommand( 168 mAddress, Locale.getDefault().getISO3Language()); 169 // TODO: figure out how to handle failed to get language code. 170 if (command != null) { 171 mService.sendCecCommand(command); 172 } else { 173 Slog.w(TAG, "Failed to respond to <Get Menu Language>: " + message.toString()); 174 } 175 return true; 176 } 177 178 @Override 179 protected boolean handleReportPhysicalAddress(HdmiCecMessage message) { 180 assertRunOnServiceThread(); 181 // Ignore if [Device Discovery Action] is going on. 182 if (hasAction(DeviceDiscoveryAction.class)) { 183 Slog.i(TAG, "Ignore unrecognizable <Report Physical Address> " 184 + "because Device Discovery Action is on-going:" + message); 185 return true; 186 } 187 188 int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams()); 189 int logicalAddress = message.getSource(); 190 191 // If it is a new device and connected to the tail of active path, 192 // it's required to change routing path. 193 boolean requireRoutingChange = !isInDeviceList(physicalAddress, logicalAddress) 194 && isTailOfActivePath(physicalAddress); 195 addAndStartAction(new NewDeviceAction(this, message.getSource(), physicalAddress, 196 requireRoutingChange)); 197 return true; 198 } 199 200 @Override 201 protected boolean handleVendorSpecificCommand(HdmiCecMessage message) { 202 assertRunOnServiceThread(); 203 List<VendorSpecificAction> actions = Collections.emptyList(); 204 // TODO: Call mService.getActions(VendorSpecificAction.class) to get all the actions. 205 206 // We assume that there can be multiple vendor-specific command actions running 207 // at the same time. Pass the message to each action to see if one of them needs it. 208 for (VendorSpecificAction action : actions) { 209 if (action.processCommand(message)) { 210 return true; 211 } 212 } 213 // Handle the message here if it is not already consumed by one of the running actions. 214 // Respond with a appropriate vendor-specific command or <Feature Abort>, or create another 215 // vendor-specific action: 216 // 217 // mService.addAndStartAction(new VendorSpecificAction(mService, mAddress)); 218 // 219 // For now, simply reply with <Feature Abort> and mark it consumed by returning true. 220 mService.sendCecCommand(HdmiCecMessageBuilder.buildFeatureAbortCommand( 221 message.getDestination(), message.getSource(), message.getOpcode(), 222 HdmiConstants.ABORT_REFUSED)); 223 return true; 224 } 225 226 private void launchDeviceDiscovery() { 227 assertRunOnServiceThread(); 228 clearDeviceInfoList(); 229 DeviceDiscoveryAction action = new DeviceDiscoveryAction(this, 230 new DeviceDiscoveryCallback() { 231 @Override 232 public void onDeviceDiscoveryDone(List<HdmiCecDeviceInfo> deviceInfos) { 233 for (HdmiCecDeviceInfo info : deviceInfos) { 234 addCecDevice(info); 235 } 236 237 // Since we removed all devices when it's start and 238 // device discovery action does not poll local devices, 239 // we should put device info of local device manually here 240 for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) { 241 addCecDevice(device.getDeviceInfo()); 242 } 243 244 addAndStartAction(new HotplugDetectionAction(HdmiCecLocalDeviceTv.this)); 245 246 // If there is AVR, initiate System Audio Auto initiation action, 247 // which turns on and off system audio according to last system 248 // audio setting. 249 HdmiCecDeviceInfo avrInfo = getAvrDeviceInfo(); 250 if (avrInfo != null) { 251 addAndStartAction(new SystemAudioAutoInitiationAction( 252 HdmiCecLocalDeviceTv.this, avrInfo.getLogicalAddress())); 253 } 254 } 255 }); 256 addAndStartAction(action); 257 } 258 259 // Clear all device info. 260 private void clearDeviceInfoList() { 261 assertRunOnServiceThread(); 262 mDeviceInfos.clear(); 263 updateSafeDeviceInfoList(); 264 } 265 266 void changeSystemAudioMode(boolean enabled, IHdmiControlCallback callback) { 267 assertRunOnServiceThread(); 268 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 269 if (avr == null) { 270 invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE); 271 return; 272 } 273 274 addAndStartAction( 275 new SystemAudioActionFromTv(this, avr.getLogicalAddress(), enabled, callback)); 276 } 277 278 boolean canChangeSystemAudioMode() { 279 // TODO: once have immutable device info, test whether avr info exists or not. 280 return false; 281 } 282 283 void setSystemAudioMode(boolean on) { 284 synchronized (mLock) { 285 if (on != mSystemAudioMode) { 286 mSystemAudioMode = on; 287 // TODO: Need to set the preference for SystemAudioMode. 288 mService.announceSystemAudioModeChange(on); 289 } 290 } 291 } 292 293 boolean getSystemAudioMode() { 294 synchronized (mLock) { 295 return mSystemAudioMode; 296 } 297 } 298 299 /** 300 * Change ARC status into the given {@code enabled} status. 301 * 302 * @return {@code true} if ARC was in "Enabled" status 303 */ 304 boolean setArcStatus(boolean enabled) { 305 synchronized (mLock) { 306 boolean oldStatus = mArcStatusEnabled; 307 // 1. Enable/disable ARC circuit. 308 mService.setAudioReturnChannel(enabled); 309 310 // TODO: notify arc mode change to AudioManager. 311 312 // 2. Update arc status; 313 mArcStatusEnabled = enabled; 314 return oldStatus; 315 } 316 } 317 318 /** 319 * Returns whether ARC is enabled or not. 320 */ 321 boolean getArcStatus() { 322 synchronized (mLock) { 323 return mArcStatusEnabled; 324 } 325 } 326 327 void setAudioStatus(boolean mute, int volume) { 328 mService.setAudioStatus(mute, volume); 329 } 330 331 @Override 332 protected boolean handleInitiateArc(HdmiCecMessage message) { 333 assertRunOnServiceThread(); 334 // In case where <Initiate Arc> is started by <Request ARC Initiation> 335 // need to clean up RequestArcInitiationAction. 336 removeAction(RequestArcInitiationAction.class); 337 SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this, 338 message.getSource(), true); 339 addAndStartAction(action); 340 return true; 341 } 342 343 @Override 344 protected boolean handleTerminateArc(HdmiCecMessage message) { 345 assertRunOnServiceThread(); 346 // In case where <Terminate Arc> is started by <Request ARC Termination> 347 // need to clean up RequestArcInitiationAction. 348 // TODO: check conditions of power status by calling is_connected api 349 // to be added soon. 350 removeAction(RequestArcTerminationAction.class); 351 SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this, 352 message.getSource(), false); 353 addAndStartAction(action); 354 return true; 355 } 356 357 @Override 358 protected boolean handleSetSystemAudioMode(HdmiCecMessage message) { 359 assertRunOnServiceThread(); 360 if (!isMessageForSystemAudio(message)) { 361 return false; 362 } 363 SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this, 364 message.getSource(), HdmiUtils.parseCommandParamSystemAudioStatus(message), null); 365 addAndStartAction(action); 366 return true; 367 } 368 369 @Override 370 protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) { 371 assertRunOnServiceThread(); 372 if (!isMessageForSystemAudio(message)) { 373 return false; 374 } 375 setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message)); 376 return true; 377 } 378 379 private boolean isMessageForSystemAudio(HdmiCecMessage message) { 380 if (message.getSource() != HdmiCec.ADDR_AUDIO_SYSTEM 381 || message.getDestination() != HdmiCec.ADDR_TV 382 || getAvrDeviceInfo() == null) { 383 Slog.w(TAG, "Skip abnormal CecMessage: " + message); 384 return false; 385 } 386 return true; 387 } 388 389 /** 390 * Add a new {@link HdmiCecDeviceInfo}. It returns old device info which has the same 391 * logical address as new device info's. 392 * 393 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 394 * 395 * @param deviceInfo a new {@link HdmiCecDeviceInfo} to be added. 396 * @return {@code null} if it is new device. Otherwise, returns old {@HdmiCecDeviceInfo} 397 * that has the same logical address as new one has. 398 */ 399 HdmiCecDeviceInfo addDeviceInfo(HdmiCecDeviceInfo deviceInfo) { 400 assertRunOnServiceThread(); 401 HdmiCecDeviceInfo oldDeviceInfo = getDeviceInfo(deviceInfo.getLogicalAddress()); 402 if (oldDeviceInfo != null) { 403 removeDeviceInfo(deviceInfo.getLogicalAddress()); 404 } 405 mDeviceInfos.append(deviceInfo.getLogicalAddress(), deviceInfo); 406 updateSafeDeviceInfoList(); 407 return oldDeviceInfo; 408 } 409 410 /** 411 * Remove a device info corresponding to the given {@code logicalAddress}. 412 * It returns removed {@link HdmiCecDeviceInfo} if exists. 413 * 414 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 415 * 416 * @param logicalAddress logical address of device to be removed 417 * @return removed {@link HdmiCecDeviceInfo} it exists. Otherwise, returns {@code null} 418 */ 419 HdmiCecDeviceInfo removeDeviceInfo(int logicalAddress) { 420 assertRunOnServiceThread(); 421 HdmiCecDeviceInfo deviceInfo = mDeviceInfos.get(logicalAddress); 422 if (deviceInfo != null) { 423 mDeviceInfos.remove(logicalAddress); 424 } 425 updateSafeDeviceInfoList(); 426 return deviceInfo; 427 } 428 429 /** 430 * Return a list of all {@link HdmiCecDeviceInfo}. 431 * 432 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 433 * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfoList(boolean)}. 434 */ 435 List<HdmiCecDeviceInfo> getDeviceInfoList(boolean includelLocalDevice) { 436 assertRunOnServiceThread(); 437 if (includelLocalDevice) { 438 return HdmiUtils.sparseArrayToList(mDeviceInfos); 439 } else { 440 ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>(); 441 for (int i = 0; i < mDeviceInfos.size(); ++i) { 442 HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i); 443 if (!isLocalDeviceAddress(info.getLogicalAddress())) { 444 infoList.add(info); 445 } 446 } 447 return infoList; 448 } 449 } 450 451 /** 452 * Return a list of {@link HdmiCecDeviceInfo}. 453 * 454 * @param includeLocalDevice whether to include local device in result. 455 */ 456 List<HdmiCecDeviceInfo> getSafeDeviceInfoList(boolean includeLocalDevice) { 457 synchronized (mLock) { 458 if (includeLocalDevice) { 459 return mSafeAllDeviceInfos; 460 } else { 461 return mSafeExternalDeviceInfos; 462 } 463 } 464 } 465 466 private void updateSafeDeviceInfoList() { 467 assertRunOnServiceThread(); 468 List<HdmiCecDeviceInfo> copiedDevices = HdmiUtils.sparseArrayToList(mDeviceInfos); 469 List<HdmiCecDeviceInfo> externalDeviceInfos = getDeviceInfoList(false); 470 synchronized (mLock) { 471 mSafeAllDeviceInfos = copiedDevices; 472 mSafeExternalDeviceInfos = externalDeviceInfos; 473 } 474 } 475 476 private boolean isLocalDeviceAddress(int address) { 477 assertRunOnServiceThread(); 478 for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) { 479 if (device.isAddressOf(address)) { 480 return true; 481 } 482 } 483 return false; 484 } 485 486 /** 487 * Return a {@link HdmiCecDeviceInfo} corresponding to the given {@code logicalAddress}. 488 * 489 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 490 * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfo(int)}. 491 * 492 * @param logicalAddress logical address to be retrieved 493 * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}. 494 * Returns null if no logical address matched 495 */ 496 HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) { 497 assertRunOnServiceThread(); 498 return mDeviceInfos.get(logicalAddress); 499 } 500 501 HdmiCecDeviceInfo getAvrDeviceInfo() { 502 assertRunOnServiceThread(); 503 return getDeviceInfo(HdmiCec.ADDR_AUDIO_SYSTEM); 504 } 505 506 /** 507 * Thread safe version of {@link #getDeviceInfo(int)}. 508 * 509 * @param logicalAddress logical address to be retrieved 510 * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}. 511 * Returns null if no logical address matched 512 */ 513 HdmiCecDeviceInfo getSafeDeviceInfo(int logicalAddress) { 514 synchronized (mLock) { 515 return mSafeAllDeviceInfos.get(logicalAddress); 516 } 517 } 518 519 HdmiCecDeviceInfo getSafeAvrDeviceInfo() { 520 return getSafeDeviceInfo(HdmiCec.ADDR_AUDIO_SYSTEM); 521 } 522 523 /** 524 * Called when a device is newly added or a new device is detected. 525 * 526 * @param info device info of a new device. 527 */ 528 final void addCecDevice(HdmiCecDeviceInfo info) { 529 assertRunOnServiceThread(); 530 addDeviceInfo(info); 531 if (info.getLogicalAddress() == mAddress) { 532 // The addition of TV device itself should not be notified. 533 return; 534 } 535 mService.invokeDeviceEventListeners(info, true); 536 } 537 538 /** 539 * Called when a device is removed or removal of device is detected. 540 * 541 * @param address a logical address of a device to be removed 542 */ 543 final void removeCecDevice(int address) { 544 assertRunOnServiceThread(); 545 HdmiCecDeviceInfo info = removeDeviceInfo(address); 546 mCecMessageCache.flushMessagesFrom(address); 547 mService.invokeDeviceEventListeners(info, false); 548 } 549 550 /** 551 * Returns the {@link HdmiCecDeviceInfo} instance whose physical address matches 552 * the given routing path. CEC devices use routing path for its physical address to 553 * describe the hierarchy of the devices in the network. 554 * 555 * @param path routing path or physical address 556 * @return {@link HdmiCecDeviceInfo} if the matched info is found; otherwise null 557 */ 558 final HdmiCecDeviceInfo getDeviceInfoByPath(int path) { 559 assertRunOnServiceThread(); 560 for (HdmiCecDeviceInfo info : getDeviceInfoList(false)) { 561 if (info.getPhysicalAddress() == path) { 562 return info; 563 } 564 } 565 return null; 566 } 567 568 /** 569 * Whether a device of the specified physical address and logical address exists 570 * in a device info list. However, both are minimal condition and it could 571 * be different device from the original one. 572 * 573 * @param physicalAddress physical address of a device to be searched 574 * @param logicalAddress logical address of a device to be searched 575 * @return true if exist; otherwise false 576 */ 577 boolean isInDeviceList(int physicalAddress, int logicalAddress) { 578 assertRunOnServiceThread(); 579 HdmiCecDeviceInfo device = getDeviceInfo(logicalAddress); 580 if (device == null) { 581 return false; 582 } 583 return device.getPhysicalAddress() == physicalAddress; 584 } 585 586 @Override 587 void onHotplug(int portNo, boolean connected) { 588 assertRunOnServiceThread(); 589 // TODO: delegate onHotplug event to each local device. 590 591 // Tv device will have permanent HotplugDetectionAction. 592 List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class); 593 if (!hotplugActions.isEmpty()) { 594 // Note that hotplug action is single action running on a machine. 595 // "pollAllDevicesNow" cleans up timer and start poll action immediately. 596 hotplugActions.get(0).pollAllDevicesNow(); 597 } 598 } 599 600 boolean canChangeSystemAudio() { 601 // TODO: implement this. 602 // return true if no system audio control sequence is running. 603 return false; 604 } 605} 606