1 /* 2 * Copyright (C) 2018 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 17 package com.android.server.hdmi; 18 19 import android.annotation.CallSuper; 20 import android.hardware.hdmi.HdmiControlManager; 21 import android.hardware.hdmi.HdmiPortInfo; 22 import android.hardware.hdmi.IHdmiControlCallback; 23 import android.sysprop.HdmiProperties; 24 import android.util.Slog; 25 26 import com.android.internal.annotations.GuardedBy; 27 import com.android.internal.annotations.VisibleForTesting; 28 import com.android.server.hdmi.Constants.LocalActivePort; 29 import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly; 30 31 import java.util.ArrayList; 32 import java.util.List; 33 34 /** 35 * Represent a logical source device residing in Android system. 36 */ 37 abstract class HdmiCecLocalDeviceSource extends HdmiCecLocalDevice { 38 39 private static final String TAG = "HdmiCecLocalDeviceSource"; 40 41 // Device has cec switch functionality or not. 42 // Default is false. 43 protected boolean mIsSwitchDevice = HdmiProperties.is_switch().orElse(false); 44 45 // Routing port number used for Routing Control. 46 // This records the default routing port or the previous valid routing port. 47 // Default is HOME input. 48 // Note that we don't save active path here because for source device, 49 // new Active Source physical address might not match the active path 50 @GuardedBy("mLock") 51 @LocalActivePort 52 private int mRoutingPort = Constants.CEC_SWITCH_HOME; 53 54 // This records the current input of the device. 55 // When device is switched to ARC input, mRoutingPort does not record it 56 // since it's not an HDMI port used for Routing Control. 57 // mLocalActivePort will record whichever input we switch to to keep tracking on 58 // the current input status of the device. 59 // This can help prevent duplicate switching and provide status information. 60 @GuardedBy("mLock") 61 @LocalActivePort 62 protected int mLocalActivePort = Constants.CEC_SWITCH_HOME; 63 64 // Whether the Routing Coutrol feature is enabled or not. False by default. 65 @GuardedBy("mLock") 66 protected boolean mRoutingControlFeatureEnabled; 67 HdmiCecLocalDeviceSource(HdmiControlService service, int deviceType)68 protected HdmiCecLocalDeviceSource(HdmiControlService service, int deviceType) { 69 super(service, deviceType); 70 } 71 72 @ServiceThreadOnly queryDisplayStatus(IHdmiControlCallback callback)73 void queryDisplayStatus(IHdmiControlCallback callback) { 74 assertRunOnServiceThread(); 75 List<DevicePowerStatusAction> actions = getActions(DevicePowerStatusAction.class); 76 if (!actions.isEmpty()) { 77 Slog.i(TAG, "queryDisplayStatus already in progress"); 78 actions.get(0).addCallback(callback); 79 return; 80 } 81 DevicePowerStatusAction action = DevicePowerStatusAction.create(this, Constants.ADDR_TV, 82 callback); 83 if (action == null) { 84 Slog.w(TAG, "Cannot initiate queryDisplayStatus"); 85 invokeCallback(callback, HdmiControlManager.POWER_STATUS_UNKNOWN); 86 return; 87 } 88 addAndStartAction(action); 89 } 90 91 @Override 92 @ServiceThreadOnly onHotplug(int portId, boolean connected)93 void onHotplug(int portId, boolean connected) { 94 assertRunOnServiceThread(); 95 if (mService.getPortInfo(portId).getType() == HdmiPortInfo.PORT_OUTPUT) { 96 mCecMessageCache.flushAll(); 97 } 98 // We'll not invalidate the active source on the hotplug event to pass CETC 11.2.2-2 ~ 3. 99 if (connected) { 100 mService.wakeUp(); 101 } 102 } 103 104 @Override 105 @ServiceThreadOnly sendStandby(int deviceId)106 protected void sendStandby(int deviceId) { 107 assertRunOnServiceThread(); 108 String powerControlMode = mService.getHdmiCecConfig().getStringValue( 109 HdmiControlManager.CEC_SETTING_NAME_POWER_CONTROL_MODE); 110 if (powerControlMode.equals(HdmiControlManager.POWER_CONTROL_MODE_BROADCAST)) { 111 mService.sendCecCommand( 112 HdmiCecMessageBuilder.buildStandby( 113 getDeviceInfo().getLogicalAddress(), Constants.ADDR_BROADCAST)); 114 return; 115 } 116 mService.sendCecCommand( 117 HdmiCecMessageBuilder.buildStandby( 118 getDeviceInfo().getLogicalAddress(), Constants.ADDR_TV)); 119 if (powerControlMode.equals(HdmiControlManager.POWER_CONTROL_MODE_TV_AND_AUDIO_SYSTEM)) { 120 mService.sendCecCommand( 121 HdmiCecMessageBuilder.buildStandby( 122 getDeviceInfo().getLogicalAddress(), Constants.ADDR_AUDIO_SYSTEM)); 123 } 124 } 125 126 @ServiceThreadOnly oneTouchPlay(IHdmiControlCallback callback)127 void oneTouchPlay(IHdmiControlCallback callback) { 128 assertRunOnServiceThread(); 129 List<OneTouchPlayAction> actions = getActions(OneTouchPlayAction.class); 130 if (!actions.isEmpty()) { 131 Slog.i(TAG, "oneTouchPlay already in progress"); 132 actions.get(0).addCallback(callback); 133 return; 134 } 135 OneTouchPlayAction action = OneTouchPlayAction.create(this, Constants.ADDR_TV, 136 callback); 137 if (action == null) { 138 Slog.w(TAG, "Cannot initiate oneTouchPlay"); 139 invokeCallback(callback, HdmiControlManager.RESULT_EXCEPTION); 140 return; 141 } 142 addAndStartAction(action); 143 } 144 145 @ServiceThreadOnly toggleAndFollowTvPower()146 void toggleAndFollowTvPower() { 147 assertRunOnServiceThread(); 148 if (mService.getPowerManager().isInteractive()) { 149 mService.pauseActiveMediaSessions(); 150 } else { 151 // Wake up Android framework to take over CEC control from the microprocessor. 152 mService.wakeUp(); 153 } 154 mService.queryDisplayStatus(new IHdmiControlCallback.Stub() { 155 @Override 156 public void onComplete(int status) { 157 if (status == HdmiControlManager.POWER_STATUS_UNKNOWN) { 158 Slog.i(TAG, "TV power toggle: TV power status unknown"); 159 sendUserControlPressedAndReleased(Constants.ADDR_TV, 160 HdmiCecKeycode.CEC_KEYCODE_POWER_TOGGLE_FUNCTION); 161 // Source device remains awake. 162 } else if (status == HdmiControlManager.POWER_STATUS_ON 163 || status == HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON) { 164 Slog.i(TAG, "TV power toggle: turning off TV"); 165 sendStandby(0 /*unused */); 166 // Source device goes to standby, to follow the toggled TV power state. 167 mService.standby(); 168 } else if (status == HdmiControlManager.POWER_STATUS_STANDBY 169 || status == HdmiControlManager.POWER_STATUS_TRANSIENT_TO_STANDBY) { 170 Slog.i(TAG, "TV power toggle: turning on TV"); 171 oneTouchPlay(new IHdmiControlCallback.Stub() { 172 @Override 173 public void onComplete(int result) { 174 if (result != HdmiControlManager.RESULT_SUCCESS) { 175 Slog.w(TAG, "Failed to complete One Touch Play. result=" + result); 176 sendUserControlPressedAndReleased(Constants.ADDR_TV, 177 HdmiCecKeycode.CEC_KEYCODE_POWER_TOGGLE_FUNCTION); 178 } 179 } 180 }); 181 // Source device remains awake, to follow the toggled TV power state. 182 } 183 } 184 }); 185 } 186 187 @ServiceThreadOnly onActiveSourceLost()188 protected void onActiveSourceLost() { 189 // Nothing to do. 190 } 191 192 @Override 193 @CallSuper 194 @ServiceThreadOnly setActiveSource(int logicalAddress, int physicalAddress, String caller)195 void setActiveSource(int logicalAddress, int physicalAddress, String caller) { 196 boolean wasActiveSource = isActiveSource(); 197 super.setActiveSource(logicalAddress, physicalAddress, caller); 198 if (wasActiveSource && !isActiveSource()) { 199 onActiveSourceLost(); 200 } 201 } 202 203 @ServiceThreadOnly setActiveSource(int physicalAddress, String caller)204 protected void setActiveSource(int physicalAddress, String caller) { 205 assertRunOnServiceThread(); 206 // Invalidate the internal active source record. 207 ActiveSource activeSource = ActiveSource.of(Constants.ADDR_INVALID, physicalAddress); 208 setActiveSource(activeSource, caller); 209 } 210 211 @ServiceThreadOnly 212 @Constants.HandleMessageResult handleActiveSource(HdmiCecMessage message)213 protected int handleActiveSource(HdmiCecMessage message) { 214 assertRunOnServiceThread(); 215 int logicalAddress = message.getSource(); 216 int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams()); 217 ActiveSource activeSource = ActiveSource.of(logicalAddress, physicalAddress); 218 if (!getActiveSource().equals(activeSource)) { 219 setActiveSource(activeSource, "HdmiCecLocalDeviceSource#handleActiveSource()"); 220 } 221 updateDevicePowerStatus(logicalAddress, HdmiControlManager.POWER_STATUS_ON); 222 if (isRoutingControlFeatureEnabled()) { 223 switchInputOnReceivingNewActivePath(physicalAddress); 224 } 225 return Constants.HANDLED; 226 } 227 228 @Override 229 @ServiceThreadOnly 230 @Constants.HandleMessageResult handleRequestActiveSource(HdmiCecMessage message)231 protected int handleRequestActiveSource(HdmiCecMessage message) { 232 assertRunOnServiceThread(); 233 maySendActiveSource(message.getSource()); 234 return Constants.HANDLED; 235 } 236 237 @Override 238 @ServiceThreadOnly 239 @Constants.HandleMessageResult handleSetStreamPath(HdmiCecMessage message)240 protected int handleSetStreamPath(HdmiCecMessage message) { 241 assertRunOnServiceThread(); 242 int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams()); 243 // If current device is the target path, set to Active Source. 244 // If the path is under the current device, should switch 245 if (physicalAddress == mService.getPhysicalAddress() && mService.isPlaybackDevice()) { 246 setAndBroadcastActiveSource(message, physicalAddress, 247 "HdmiCecLocalDeviceSource#handleSetStreamPath()"); 248 } else if (physicalAddress != mService.getPhysicalAddress() || !isActiveSource()) { 249 // Invalidate the active source if stream path is set to other physical address or 250 // our physical address while not active source 251 setActiveSource(physicalAddress, "HdmiCecLocalDeviceSource#handleSetStreamPath()"); 252 } 253 switchInputOnReceivingNewActivePath(physicalAddress); 254 return Constants.HANDLED; 255 } 256 257 @Override 258 @ServiceThreadOnly 259 @Constants.HandleMessageResult handleRoutingChange(HdmiCecMessage message)260 protected int handleRoutingChange(HdmiCecMessage message) { 261 assertRunOnServiceThread(); 262 int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams(), 2); 263 if (physicalAddress != mService.getPhysicalAddress() || !isActiveSource()) { 264 // Invalidate the active source if routing is changed to other physical address or 265 // our physical address while not active source 266 setActiveSource(physicalAddress, "HdmiCecLocalDeviceSource#handleRoutingChange()"); 267 } 268 if (!isRoutingControlFeatureEnabled()) { 269 return Constants.ABORT_REFUSED; 270 } 271 handleRoutingChangeAndInformation(physicalAddress, message); 272 return Constants.HANDLED; 273 } 274 275 @Override 276 @ServiceThreadOnly 277 @Constants.HandleMessageResult handleRoutingInformation(HdmiCecMessage message)278 protected int handleRoutingInformation(HdmiCecMessage message) { 279 assertRunOnServiceThread(); 280 int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams()); 281 if (physicalAddress != mService.getPhysicalAddress() || !isActiveSource()) { 282 // Invalidate the active source if routing is changed to other physical address or 283 // our physical address while not active source 284 setActiveSource(physicalAddress, "HdmiCecLocalDeviceSource#handleRoutingInformation()"); 285 } 286 if (!isRoutingControlFeatureEnabled()) { 287 return Constants.ABORT_REFUSED; 288 } 289 handleRoutingChangeAndInformation(physicalAddress, message); 290 return Constants.HANDLED; 291 } 292 293 // Method to switch Input with the new Active Path. 294 // All the devices with Switch functionality should implement this. switchInputOnReceivingNewActivePath(int physicalAddress)295 protected void switchInputOnReceivingNewActivePath(int physicalAddress) { 296 // do nothing 297 } 298 299 // Only source devices that react to routing control messages should implement 300 // this method (e.g. a TV with built in switch). handleRoutingChangeAndInformation(int physicalAddress, HdmiCecMessage message)301 protected void handleRoutingChangeAndInformation(int physicalAddress, HdmiCecMessage message) { 302 // do nothing 303 } 304 305 @Override 306 @ServiceThreadOnly disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback)307 protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) { 308 removeAction(OneTouchPlayAction.class); 309 removeAction(DevicePowerStatusAction.class); 310 removeAction(AbsoluteVolumeAudioStatusAction.class); 311 312 super.disableDevice(initiatedByCec, callback); 313 } 314 315 // Update the power status of the devices connected to the current device. 316 // This only works if the current device is a switch and keeps tracking the device info 317 // of the device connected to it. updateDevicePowerStatus(int logicalAddress, int newPowerStatus)318 protected void updateDevicePowerStatus(int logicalAddress, int newPowerStatus) { 319 // do nothing 320 } 321 322 @Constants.RcProfile 323 @Override getRcProfile()324 protected int getRcProfile() { 325 return Constants.RC_PROFILE_SOURCE; 326 } 327 328 @Override getRcFeatures()329 protected List<Integer> getRcFeatures() { 330 List<Integer> features = new ArrayList<>(); 331 HdmiCecConfig hdmiCecConfig = mService.getHdmiCecConfig(); 332 if (hdmiCecConfig.getIntValue( 333 HdmiControlManager.CEC_SETTING_NAME_RC_PROFILE_SOURCE_HANDLES_ROOT_MENU) 334 == HdmiControlManager.RC_PROFILE_SOURCE_MENU_HANDLED) { 335 features.add(Constants.RC_PROFILE_SOURCE_HANDLES_ROOT_MENU); 336 } 337 if (hdmiCecConfig.getIntValue( 338 HdmiControlManager.CEC_SETTING_NAME_RC_PROFILE_SOURCE_HANDLES_SETUP_MENU) 339 == HdmiControlManager.RC_PROFILE_SOURCE_MENU_HANDLED) { 340 features.add(Constants.RC_PROFILE_SOURCE_HANDLES_SETUP_MENU); 341 } 342 if (hdmiCecConfig.getIntValue( 343 HdmiControlManager.CEC_SETTING_NAME_RC_PROFILE_SOURCE_HANDLES_CONTENTS_MENU) 344 == HdmiControlManager.RC_PROFILE_SOURCE_MENU_HANDLED) { 345 features.add(Constants.RC_PROFILE_SOURCE_HANDLES_CONTENTS_MENU); 346 } 347 if (hdmiCecConfig.getIntValue( 348 HdmiControlManager.CEC_SETTING_NAME_RC_PROFILE_SOURCE_HANDLES_TOP_MENU) 349 == HdmiControlManager.RC_PROFILE_SOURCE_MENU_HANDLED) { 350 features.add(Constants.RC_PROFILE_SOURCE_HANDLES_TOP_MENU); 351 } 352 if (hdmiCecConfig.getIntValue(HdmiControlManager 353 .CEC_SETTING_NAME_RC_PROFILE_SOURCE_HANDLES_MEDIA_CONTEXT_SENSITIVE_MENU) 354 == HdmiControlManager.RC_PROFILE_SOURCE_MENU_HANDLED) { 355 features.add(Constants.RC_PROFILE_SOURCE_HANDLES_MEDIA_CONTEXT_SENSITIVE_MENU); 356 } 357 return features; 358 } 359 360 // Active source claiming needs to be handled in Service 361 // since service can decide who will be the active source when the device supports 362 // multiple device types in this method. 363 // This method should only be called when the device can be the active source. setAndBroadcastActiveSource(HdmiCecMessage message, int physicalAddress, String caller)364 protected void setAndBroadcastActiveSource(HdmiCecMessage message, int physicalAddress, 365 String caller) { 366 mService.setAndBroadcastActiveSource( 367 physicalAddress, getDeviceInfo().getDeviceType(), message.getSource(), caller); 368 } 369 370 // Indicates if current device is the active source or not 371 @ServiceThreadOnly isActiveSource()372 protected boolean isActiveSource() { 373 if (getDeviceInfo() == null) { 374 return false; 375 } 376 377 return getActiveSource().equals(getDeviceInfo().getLogicalAddress(), 378 getDeviceInfo().getPhysicalAddress()); 379 } 380 wakeUpIfActiveSource()381 protected void wakeUpIfActiveSource() { 382 if (!isActiveSource()) { 383 return; 384 } 385 // Wake up the device. This will also exit dream mode. 386 mService.wakeUp(); 387 return; 388 } 389 maySendActiveSource(int dest)390 protected void maySendActiveSource(int dest) { 391 if (!isActiveSource()) { 392 return; 393 } 394 addAndStartAction(new ActiveSourceAction(this, dest)); 395 } 396 397 /** 398 * Set {@link #mRoutingPort} to a specific {@link LocalActivePort} to record the current active 399 * CEC Routing Control related port. 400 * 401 * @param portId The portId of the new routing port. 402 */ 403 @VisibleForTesting setRoutingPort(@ocalActivePort int portId)404 protected void setRoutingPort(@LocalActivePort int portId) { 405 synchronized (mLock) { 406 mRoutingPort = portId; 407 } 408 } 409 410 /** 411 * Get {@link #mRoutingPort}. This is useful when the device needs to route to the last valid 412 * routing port. 413 */ 414 @LocalActivePort getRoutingPort()415 protected int getRoutingPort() { 416 synchronized (mLock) { 417 return mRoutingPort; 418 } 419 } 420 421 /** 422 * Get {@link #mLocalActivePort}. This is useful when device needs to know the current active 423 * port. 424 */ 425 @LocalActivePort getLocalActivePort()426 protected int getLocalActivePort() { 427 synchronized (mLock) { 428 return mLocalActivePort; 429 } 430 } 431 432 /** 433 * Set {@link #mLocalActivePort} to a specific {@link LocalActivePort} to record the current 434 * active port. 435 * 436 * <p>It does not have to be a Routing Control related port. For example it can be 437 * set to {@link Constants#CEC_SWITCH_ARC} but this port is System Audio related. 438 * 439 * @param activePort The portId of the new active port. 440 */ setLocalActivePort(@ocalActivePort int activePort)441 protected void setLocalActivePort(@LocalActivePort int activePort) { 442 synchronized (mLock) { 443 mLocalActivePort = activePort; 444 } 445 } 446 isRoutingControlFeatureEnabled()447 boolean isRoutingControlFeatureEnabled() { 448 synchronized (mLock) { 449 return mRoutingControlFeatureEnabled; 450 } 451 } 452 453 // Check if the device is trying to switch to the same input that is active right now. 454 // This can help avoid redundant port switching. isSwitchingToTheSameInput(@ocalActivePort int activePort)455 protected boolean isSwitchingToTheSameInput(@LocalActivePort int activePort) { 456 return activePort == getLocalActivePort(); 457 } 458 } 459