HdmiCecLocalDeviceTv.java revision 79c58a4b97f27ede6a1b680d2fece9c2a0edf7b7
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 57 @Override 58 protected void onAddressAllocated(int logicalAddress) { 59 assertRunOnServiceThread(); 60 // TODO: vendor-specific initialization here. 61 62 mService.sendCecCommand(HdmiCecMessageBuilder.buildReportPhysicalAddressCommand( 63 mAddress, mService.getPhysicalAddress(), mDeviceType)); 64 mService.sendCecCommand(HdmiCecMessageBuilder.buildDeviceVendorIdCommand( 65 mAddress, mService.getVendorId())); 66 67 launchDeviceDiscovery(); 68 // TODO: Start routing control action, device discovery action. 69 } 70 71 /** 72 * Performs the action 'device select', or 'one touch play' initiated by TV. 73 * 74 * @param targetAddress logical address of the device to select 75 * @param callback callback object to report the result with 76 */ 77 void deviceSelect(int targetAddress, IHdmiControlCallback callback) { 78 assertRunOnServiceThread(); 79 HdmiCecDeviceInfo targetDevice = mService.getDeviceInfo(targetAddress); 80 if (targetDevice == null) { 81 invokeCallback(callback, HdmiCec.RESULT_TARGET_NOT_AVAILABLE); 82 return; 83 } 84 removeAction(DeviceSelectAction.class); 85 addAndStartAction(new DeviceSelectAction(this, targetDevice, callback)); 86 } 87 88 private static void invokeCallback(IHdmiControlCallback callback, int result) { 89 try { 90 callback.onComplete(result); 91 } catch (RemoteException e) { 92 Slog.e(TAG, "Invoking callback failed:" + e); 93 } 94 } 95 96 @Override 97 protected boolean handleGetMenuLanguage(HdmiCecMessage message) { 98 assertRunOnServiceThread(); 99 HdmiCecMessage command = HdmiCecMessageBuilder.buildSetMenuLanguageCommand( 100 mAddress, Locale.getDefault().getISO3Language()); 101 // TODO: figure out how to handle failed to get language code. 102 if (command != null) { 103 mService.sendCecCommand(command); 104 } else { 105 Slog.w(TAG, "Failed to respond to <Get Menu Language>: " + message.toString()); 106 } 107 return true; 108 } 109 110 @Override 111 protected boolean handleReportPhysicalAddress(HdmiCecMessage message) { 112 assertRunOnServiceThread(); 113 // Ignore if [Device Discovery Action] is going on. 114 if (hasAction(DeviceDiscoveryAction.class)) { 115 Slog.i(TAG, "Ignore unrecognizable <Report Physical Address> " 116 + "because Device Discovery Action is on-going:" + message); 117 return true; 118 } 119 120 int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams()); 121 int logicalAddress = message.getSource(); 122 123 // If it is a new device and connected to the tail of active path, 124 // it's required to change routing path. 125 boolean requireRoutingChange = !isInDeviceList(physicalAddress, logicalAddress) 126 && isTailOfActivePath(physicalAddress); 127 addAndStartAction(new NewDeviceAction(this, message.getSource(), physicalAddress, 128 requireRoutingChange)); 129 return true; 130 } 131 132 @Override 133 protected boolean handleVendorSpecificCommand(HdmiCecMessage message) { 134 assertRunOnServiceThread(); 135 List<VendorSpecificAction> actions = Collections.emptyList(); 136 // TODO: Call mService.getActions(VendorSpecificAction.class) to get all the actions. 137 138 // We assume that there can be multiple vendor-specific command actions running 139 // at the same time. Pass the message to each action to see if one of them needs it. 140 for (VendorSpecificAction action : actions) { 141 if (action.processCommand(message)) { 142 return true; 143 } 144 } 145 // Handle the message here if it is not already consumed by one of the running actions. 146 // Respond with a appropriate vendor-specific command or <Feature Abort>, or create another 147 // vendor-specific action: 148 // 149 // mService.addAndStartAction(new VendorSpecificAction(mService, mAddress)); 150 // 151 // For now, simply reply with <Feature Abort> and mark it consumed by returning true. 152 mService.sendCecCommand(HdmiCecMessageBuilder.buildFeatureAbortCommand( 153 message.getDestination(), message.getSource(), message.getOpcode(), 154 HdmiConstants.ABORT_REFUSED)); 155 return true; 156 } 157 158 private void launchDeviceDiscovery() { 159 assertRunOnServiceThread(); 160 clearDeviceInfoList(); 161 DeviceDiscoveryAction action = new DeviceDiscoveryAction(this, 162 new DeviceDiscoveryCallback() { 163 @Override 164 public void onDeviceDiscoveryDone(List<HdmiCecDeviceInfo> deviceInfos) { 165 for (HdmiCecDeviceInfo info : deviceInfos) { 166 addCecDevice(info); 167 } 168 169 // Since we removed all devices when it's start and 170 // device discovery action does not poll local devices, 171 // we should put device info of local device manually here 172 for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) { 173 addCecDevice(device.getDeviceInfo()); 174 } 175 176 addAndStartAction(new HotplugDetectionAction(HdmiCecLocalDeviceTv.this)); 177 } 178 }); 179 addAndStartAction(action); 180 } 181 182 // Clear all device info. 183 private void clearDeviceInfoList() { 184 assertRunOnServiceThread(); 185 mDeviceInfos.clear(); 186 } 187 188 void setSystemAudioMode(boolean on) { 189 synchronized (mLock) { 190 if (on != mSystemAudioMode) { 191 mSystemAudioMode = on; 192 // TODO: Need to set the preference for SystemAudioMode. 193 // TODO: Need to handle the notification of changing the mode and 194 // to identify the notification should be handled in the service or TvSettings. 195 } 196 } 197 } 198 199 boolean getSystemAudioMode() { 200 synchronized (mLock) { 201 assertRunOnServiceThread(); 202 return mSystemAudioMode; 203 } 204 } 205 206 /** 207 * Change ARC status into the given {@code enabled} status. 208 * 209 * @return {@code true} if ARC was in "Enabled" status 210 */ 211 boolean setArcStatus(boolean enabled) { 212 synchronized (mLock) { 213 boolean oldStatus = mArcStatusEnabled; 214 // 1. Enable/disable ARC circuit. 215 mService.setAudioReturnChannel(enabled); 216 217 // TODO: notify arc mode change to AudioManager. 218 219 // 2. Update arc status; 220 mArcStatusEnabled = enabled; 221 return oldStatus; 222 } 223 } 224 225 /** 226 * Returns whether ARC is enabled or not. 227 */ 228 boolean getArcStatus() { 229 synchronized (mLock) { 230 return mArcStatusEnabled; 231 } 232 } 233 234 void setAudioStatus(boolean mute, int volume) { 235 mService.setAudioStatus(mute, volume); 236 } 237 238 @Override 239 protected boolean handleInitiateArc(HdmiCecMessage message) { 240 assertRunOnServiceThread(); 241 // In case where <Initiate Arc> is started by <Request ARC Initiation> 242 // need to clean up RequestArcInitiationAction. 243 removeAction(RequestArcInitiationAction.class); 244 SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this, 245 message.getSource(), true); 246 addAndStartAction(action); 247 return true; 248 } 249 250 @Override 251 protected boolean handleTerminateArc(HdmiCecMessage message) { 252 assertRunOnServiceThread(); 253 // In case where <Terminate Arc> is started by <Request ARC Termination> 254 // need to clean up RequestArcInitiationAction. 255 // TODO: check conditions of power status by calling is_connected api 256 // to be added soon. 257 removeAction(RequestArcTerminationAction.class); 258 SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this, 259 message.getSource(), false); 260 addAndStartAction(action); 261 return true; 262 } 263 264 @Override 265 protected boolean handleSetSystemAudioMode(HdmiCecMessage message) { 266 assertRunOnServiceThread(); 267 if (!isMessageForSystemAudio(message)) { 268 return false; 269 } 270 SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this, 271 message.getSource(), HdmiUtils.parseCommandParamSystemAudioStatus(message)); 272 addAndStartAction(action); 273 return true; 274 } 275 276 @Override 277 protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) { 278 assertRunOnServiceThread(); 279 if (!isMessageForSystemAudio(message)) { 280 return false; 281 } 282 setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message)); 283 return true; 284 } 285 286 private boolean isMessageForSystemAudio(HdmiCecMessage message) { 287 if (message.getSource() != HdmiCec.ADDR_AUDIO_SYSTEM 288 || message.getDestination() != HdmiCec.ADDR_TV 289 || getAvrDeviceInfo() == null) { 290 Slog.w(TAG, "Skip abnormal CecMessage: " + message); 291 return false; 292 } 293 return true; 294 } 295 296 /** 297 * Add a new {@link HdmiCecDeviceInfo}. It returns old device info which has the same 298 * logical address as new device info's. 299 * 300 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 301 * 302 * @param deviceInfo a new {@link HdmiCecDeviceInfo} to be added. 303 * @return {@code null} if it is new device. Otherwise, returns old {@HdmiCecDeviceInfo} 304 * that has the same logical address as new one has. 305 */ 306 HdmiCecDeviceInfo addDeviceInfo(HdmiCecDeviceInfo deviceInfo) { 307 assertRunOnServiceThread(); 308 HdmiCecDeviceInfo oldDeviceInfo = getDeviceInfo(deviceInfo.getLogicalAddress()); 309 if (oldDeviceInfo != null) { 310 removeDeviceInfo(deviceInfo.getLogicalAddress()); 311 } 312 mDeviceInfos.append(deviceInfo.getLogicalAddress(), deviceInfo); 313 return oldDeviceInfo; 314 } 315 316 /** 317 * Remove a device info corresponding to the given {@code logicalAddress}. 318 * It returns removed {@link HdmiCecDeviceInfo} if exists. 319 * 320 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 321 * 322 * @param logicalAddress logical address of device to be removed 323 * @return removed {@link HdmiCecDeviceInfo} it exists. Otherwise, returns {@code null} 324 */ 325 HdmiCecDeviceInfo removeDeviceInfo(int logicalAddress) { 326 assertRunOnServiceThread(); 327 HdmiCecDeviceInfo deviceInfo = mDeviceInfos.get(logicalAddress); 328 if (deviceInfo != null) { 329 mDeviceInfos.remove(logicalAddress); 330 } 331 return deviceInfo; 332 } 333 334 /** 335 * Return a list of all {@link HdmiCecDeviceInfo}. 336 * 337 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 338 */ 339 List<HdmiCecDeviceInfo> getDeviceInfoList(boolean includelLocalDevice) { 340 assertRunOnServiceThread(); 341 if (includelLocalDevice) { 342 return HdmiUtils.sparseArrayToList(mDeviceInfos); 343 } else { 344 345 ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>(); 346 for (int i = 0; i < mDeviceInfos.size(); ++i) { 347 HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i); 348 if (!isLocalDeviceAddress(info.getLogicalAddress())) { 349 infoList.add(info); 350 } 351 } 352 return infoList; 353 } 354 } 355 356 private boolean isLocalDeviceAddress(int address) { 357 assertRunOnServiceThread(); 358 for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) { 359 if (device.isAddressOf(address)) { 360 return true; 361 } 362 } 363 return false; 364 } 365 366 /** 367 * Return a {@link HdmiCecDeviceInfo} corresponding to the given {@code logicalAddress}. 368 * 369 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 370 * 371 * @param logicalAddress logical address to be retrieved 372 * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}. 373 * Returns null if no logical address matched 374 */ 375 HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) { 376 assertRunOnServiceThread(); 377 return mDeviceInfos.get(logicalAddress); 378 } 379 380 HdmiCecDeviceInfo getAvrDeviceInfo() { 381 assertRunOnServiceThread(); 382 return getDeviceInfo(HdmiCec.ADDR_AUDIO_SYSTEM); 383 } 384 385 /** 386 * Called when a device is newly added or a new device is detected. 387 * 388 * @param info device info of a new device. 389 */ 390 final void addCecDevice(HdmiCecDeviceInfo info) { 391 assertRunOnServiceThread(); 392 addDeviceInfo(info); 393 394 // TODO: announce new device detection. 395 } 396 397 /** 398 * Called when a device is removed or removal of device is detected. 399 * 400 * @param address a logical address of a device to be removed 401 */ 402 final void removeCecDevice(int address) { 403 assertRunOnServiceThread(); 404 removeDeviceInfo(address); 405 mCecMessageCache.flushMessagesFrom(address); 406 407 // TODO: announce a device removal. 408 } 409 410 /** 411 * Returns the {@link HdmiCecDeviceInfo} instance whose physical address matches 412 * the given routing path. CEC devices use routing path for its physical address to 413 * describe the hierarchy of the devices in the network. 414 * 415 * @param path routing path or physical address 416 * @return {@link HdmiCecDeviceInfo} if the matched info is found; otherwise null 417 */ 418 final HdmiCecDeviceInfo getDeviceInfoByPath(int path) { 419 assertRunOnServiceThread(); 420 for (HdmiCecDeviceInfo info : getDeviceInfoList(false)) { 421 if (info.getPhysicalAddress() == path) { 422 return info; 423 } 424 } 425 return null; 426 } 427 428 /** 429 * Whether a device of the specified physical address and logical address exists 430 * in a device info list. However, both are minimal condition and it could 431 * be different device from the original one. 432 * 433 * @param physicalAddress physical address of a device to be searched 434 * @param logicalAddress logical address of a device to be searched 435 * @return true if exist; otherwise false 436 */ 437 boolean isInDeviceList(int physicalAddress, int logicalAddress) { 438 assertRunOnServiceThread(); 439 HdmiCecDeviceInfo device = getDeviceInfo(logicalAddress); 440 if (device == null) { 441 return false; 442 } 443 return device.getPhysicalAddress() == physicalAddress; 444 } 445 446 @Override 447 void onHotplug(int portNo, boolean connected) { 448 assertRunOnServiceThread(); 449 // TODO: delegate onHotplug event to each local device. 450 451 // Tv device will have permanent HotplugDetectionAction. 452 List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class); 453 if (!hotplugActions.isEmpty()) { 454 // Note that hotplug action is single action running on a machine. 455 // "pollAllDevicesNow" cleans up timer and start poll action immediately. 456 hotplugActions.get(0).pollAllDevicesNow(); 457 } 458 } 459} 460