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