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