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 HdmiPortInfo portInfo = mService.getPortInfo(portId); 96 if (portInfo != null && portInfo.getType() == HdmiPortInfo.PORT_OUTPUT) { 97 mCecMessageCache.flushAll(); 98 } 99 // We'll not invalidate the active source on the hotplug event to pass CETC 11.2.2-2 ~ 3. 100 if (connected) { 101 mService.wakeUp(); 102 } 103 } 104 105 @Override 106 @ServiceThreadOnly sendStandby(int deviceId)107 protected void sendStandby(int deviceId) { 108 assertRunOnServiceThread(); 109 String powerControlMode = mService.getHdmiCecConfig().getStringValue( 110 HdmiControlManager.CEC_SETTING_NAME_POWER_CONTROL_MODE); 111 if (powerControlMode.equals(HdmiControlManager.POWER_CONTROL_MODE_BROADCAST)) { 112 mService.sendCecCommand( 113 HdmiCecMessageBuilder.buildStandby( 114 getDeviceInfo().getLogicalAddress(), Constants.ADDR_BROADCAST)); 115 return; 116 } 117 mService.sendCecCommand( 118 HdmiCecMessageBuilder.buildStandby( 119 getDeviceInfo().getLogicalAddress(), Constants.ADDR_TV)); 120 if (powerControlMode.equals(HdmiControlManager.POWER_CONTROL_MODE_TV_AND_AUDIO_SYSTEM)) { 121 mService.sendCecCommand( 122 HdmiCecMessageBuilder.buildStandby( 123 getDeviceInfo().getLogicalAddress(), Constants.ADDR_AUDIO_SYSTEM)); 124 } 125 } 126 127 @ServiceThreadOnly oneTouchPlay(IHdmiControlCallback callback)128 void oneTouchPlay(IHdmiControlCallback callback) { 129 assertRunOnServiceThread(); 130 List<OneTouchPlayAction> actions = getActions(OneTouchPlayAction.class); 131 if (!actions.isEmpty()) { 132 Slog.i(TAG, "oneTouchPlay already in progress"); 133 actions.get(0).addCallback(callback); 134 return; 135 } 136 OneTouchPlayAction action = OneTouchPlayAction.create(this, Constants.ADDR_TV, 137 callback); 138 if (action == null) { 139 Slog.w(TAG, "Cannot initiate oneTouchPlay"); 140 invokeCallback(callback, HdmiControlManager.RESULT_EXCEPTION); 141 return; 142 } 143 addAndStartAction(action); 144 } 145 146 @ServiceThreadOnly toggleAndFollowTvPower()147 void toggleAndFollowTvPower() { 148 assertRunOnServiceThread(); 149 if (mService.getPowerManager().isInteractive()) { 150 mService.pauseActiveMediaSessions(); 151 } else { 152 // Wake up Android framework to take over CEC control from the microprocessor. 153 mService.wakeUp(); 154 } 155 mService.queryDisplayStatus(new IHdmiControlCallback.Stub() { 156 @Override 157 public void onComplete(int status) { 158 if (status == HdmiControlManager.POWER_STATUS_UNKNOWN) { 159 Slog.i(TAG, "TV power toggle: TV power status unknown"); 160 sendUserControlPressedAndReleased(Constants.ADDR_TV, 161 HdmiCecKeycode.CEC_KEYCODE_POWER_TOGGLE_FUNCTION); 162 // Source device remains awake. 163 } else if (status == HdmiControlManager.POWER_STATUS_ON 164 || status == HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON) { 165 Slog.i(TAG, "TV power toggle: turning off TV"); 166 sendStandby(0 /*unused */); 167 // Source device goes to standby, to follow the toggled TV power state. 168 mService.standby(); 169 } else if (status == HdmiControlManager.POWER_STATUS_STANDBY 170 || status == HdmiControlManager.POWER_STATUS_TRANSIENT_TO_STANDBY) { 171 Slog.i(TAG, "TV power toggle: turning on TV"); 172 oneTouchPlay(new IHdmiControlCallback.Stub() { 173 @Override 174 public void onComplete(int result) { 175 if (result != HdmiControlManager.RESULT_SUCCESS) { 176 Slog.w(TAG, "Failed to complete One Touch Play. result=" + result); 177 sendUserControlPressedAndReleased(Constants.ADDR_TV, 178 HdmiCecKeycode.CEC_KEYCODE_POWER_TOGGLE_FUNCTION); 179 } 180 } 181 }); 182 // Source device remains awake, to follow the toggled TV power state. 183 } 184 } 185 }); 186 } 187 188 @ServiceThreadOnly onActiveSourceLost()189 protected void onActiveSourceLost() { 190 // Nothing to do. 191 } 192 193 @Override 194 @CallSuper 195 @ServiceThreadOnly setActiveSource(int logicalAddress, int physicalAddress, String caller)196 void setActiveSource(int logicalAddress, int physicalAddress, String caller) { 197 boolean wasActiveSource = isActiveSource(); 198 super.setActiveSource(logicalAddress, physicalAddress, caller); 199 if (wasActiveSource && !isActiveSource()) { 200 onActiveSourceLost(); 201 } 202 } 203 204 @ServiceThreadOnly setActiveSource(int physicalAddress, String caller)205 protected void setActiveSource(int physicalAddress, String caller) { 206 assertRunOnServiceThread(); 207 // Invalidate the internal active source record. 208 ActiveSource activeSource = ActiveSource.of(Constants.ADDR_INVALID, physicalAddress); 209 setActiveSource(activeSource, caller); 210 } 211 212 @ServiceThreadOnly 213 @Constants.HandleMessageResult handleActiveSource(HdmiCecMessage message)214 protected int handleActiveSource(HdmiCecMessage message) { 215 assertRunOnServiceThread(); 216 int logicalAddress = message.getSource(); 217 int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams()); 218 ActiveSource activeSource = ActiveSource.of(logicalAddress, physicalAddress); 219 if (!getActiveSource().equals(activeSource)) { 220 setActiveSource(activeSource, "HdmiCecLocalDeviceSource#handleActiveSource()"); 221 } 222 updateDevicePowerStatus(logicalAddress, HdmiControlManager.POWER_STATUS_ON); 223 if (isRoutingControlFeatureEnabled()) { 224 switchInputOnReceivingNewActivePath(physicalAddress); 225 } 226 return Constants.HANDLED; 227 } 228 229 @Override 230 @ServiceThreadOnly 231 @Constants.HandleMessageResult handleRequestActiveSource(HdmiCecMessage message)232 protected int handleRequestActiveSource(HdmiCecMessage message) { 233 assertRunOnServiceThread(); 234 maySendActiveSource(message.getSource()); 235 return Constants.HANDLED; 236 } 237 238 @Override 239 @ServiceThreadOnly 240 @Constants.HandleMessageResult handleSetStreamPath(HdmiCecMessage message)241 protected int handleSetStreamPath(HdmiCecMessage message) { 242 assertRunOnServiceThread(); 243 int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams()); 244 // If current device is the target path, set to Active Source. 245 // If the path is under the current device, should switch 246 if (physicalAddress == mService.getPhysicalAddress() && mService.isPlaybackDevice()) { 247 setAndBroadcastActiveSource(message, physicalAddress, 248 "HdmiCecLocalDeviceSource#handleSetStreamPath()"); 249 } else if (physicalAddress != mService.getPhysicalAddress() || !isActiveSource()) { 250 // Invalidate the active source if stream path is set to other physical address or 251 // our physical address while not active source 252 setActiveSource(physicalAddress, "HdmiCecLocalDeviceSource#handleSetStreamPath()"); 253 } 254 switchInputOnReceivingNewActivePath(physicalAddress); 255 return Constants.HANDLED; 256 } 257 258 @Override 259 @ServiceThreadOnly 260 @Constants.HandleMessageResult handleRoutingChange(HdmiCecMessage message)261 protected int handleRoutingChange(HdmiCecMessage message) { 262 assertRunOnServiceThread(); 263 int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams(), 2); 264 if (physicalAddress != mService.getPhysicalAddress() || !isActiveSource()) { 265 // Invalidate the active source if routing is changed to other physical address or 266 // our physical address while not active source 267 setActiveSource(physicalAddress, "HdmiCecLocalDeviceSource#handleRoutingChange()"); 268 } 269 if (!isRoutingControlFeatureEnabled()) { 270 return Constants.ABORT_REFUSED; 271 } 272 handleRoutingChangeAndInformation(physicalAddress, message); 273 return Constants.HANDLED; 274 } 275 276 @Override 277 @ServiceThreadOnly 278 @Constants.HandleMessageResult handleRoutingInformation(HdmiCecMessage message)279 protected int handleRoutingInformation(HdmiCecMessage message) { 280 assertRunOnServiceThread(); 281 int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams()); 282 if (physicalAddress != mService.getPhysicalAddress() || !isActiveSource()) { 283 // Invalidate the active source if routing is changed to other physical address or 284 // our physical address while not active source 285 setActiveSource(physicalAddress, "HdmiCecLocalDeviceSource#handleRoutingInformation()"); 286 } 287 if (!isRoutingControlFeatureEnabled()) { 288 return Constants.ABORT_REFUSED; 289 } 290 handleRoutingChangeAndInformation(physicalAddress, message); 291 return Constants.HANDLED; 292 } 293 294 // Method to switch Input with the new Active Path. 295 // All the devices with Switch functionality should implement this. switchInputOnReceivingNewActivePath(int physicalAddress)296 protected void switchInputOnReceivingNewActivePath(int physicalAddress) { 297 // do nothing 298 } 299 300 // Only source devices that react to routing control messages should implement 301 // this method (e.g. a TV with built in switch). handleRoutingChangeAndInformation(int physicalAddress, HdmiCecMessage message)302 protected void handleRoutingChangeAndInformation(int physicalAddress, HdmiCecMessage message) { 303 // do nothing 304 } 305 306 @Override 307 @ServiceThreadOnly disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback)308 protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) { 309 removeAction(OneTouchPlayAction.class); 310 removeAction(DevicePowerStatusAction.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