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 // Prevent focus stealing when losing active source. 201 removeAction(ActiveSourceAction.class); 202 onActiveSourceLost(); 203 } 204 } 205 206 @ServiceThreadOnly setActiveSource(int physicalAddress, String caller)207 protected void setActiveSource(int physicalAddress, String caller) { 208 assertRunOnServiceThread(); 209 // Invalidate the internal active source record. 210 ActiveSource activeSource = ActiveSource.of(Constants.ADDR_INVALID, physicalAddress); 211 setActiveSource(activeSource, caller); 212 } 213 214 @ServiceThreadOnly 215 @Constants.HandleMessageResult handleActiveSource(HdmiCecMessage message)216 protected int handleActiveSource(HdmiCecMessage message) { 217 assertRunOnServiceThread(); 218 int logicalAddress = message.getSource(); 219 int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams()); 220 ActiveSource activeSource = ActiveSource.of(logicalAddress, physicalAddress); 221 if (!getActiveSource().equals(activeSource)) { 222 setActiveSource(activeSource, "HdmiCecLocalDeviceSource#handleActiveSource()"); 223 } 224 updateDevicePowerStatus(logicalAddress, HdmiControlManager.POWER_STATUS_ON); 225 if (isRoutingControlFeatureEnabled()) { 226 switchInputOnReceivingNewActivePath(physicalAddress); 227 } 228 return Constants.HANDLED; 229 } 230 231 @Override 232 @ServiceThreadOnly 233 @Constants.HandleMessageResult handleRequestActiveSource(HdmiCecMessage message)234 protected int handleRequestActiveSource(HdmiCecMessage message) { 235 assertRunOnServiceThread(); 236 maySendActiveSource(message.getSource()); 237 return Constants.HANDLED; 238 } 239 240 @Override 241 @ServiceThreadOnly 242 @Constants.HandleMessageResult handleSetStreamPath(HdmiCecMessage message)243 protected int handleSetStreamPath(HdmiCecMessage message) { 244 assertRunOnServiceThread(); 245 int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams()); 246 // If current device is the target path, set to Active Source. 247 // If the path is under the current device, should switch 248 if (physicalAddress == mService.getPhysicalAddress() && mService.isPlaybackDevice()) { 249 setAndBroadcastActiveSource(message, physicalAddress, 250 "HdmiCecLocalDeviceSource#handleSetStreamPath()"); 251 } else if (physicalAddress != mService.getPhysicalAddress() || !isActiveSource()) { 252 // Invalidate the active source if stream path is set to other physical address or 253 // our physical address while not active source 254 setActiveSource(physicalAddress, "HdmiCecLocalDeviceSource#handleSetStreamPath()"); 255 } 256 switchInputOnReceivingNewActivePath(physicalAddress); 257 return Constants.HANDLED; 258 } 259 260 @Override 261 @ServiceThreadOnly 262 @Constants.HandleMessageResult handleRoutingChange(HdmiCecMessage message)263 protected int handleRoutingChange(HdmiCecMessage message) { 264 assertRunOnServiceThread(); 265 int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams(), 2); 266 if (physicalAddress != mService.getPhysicalAddress() || !isActiveSource()) { 267 // Invalidate the active source if routing is changed to other physical address or 268 // our physical address while not active source 269 setActiveSource(physicalAddress, "HdmiCecLocalDeviceSource#handleRoutingChange()"); 270 } 271 if (!isRoutingControlFeatureEnabled()) { 272 return Constants.ABORT_REFUSED; 273 } 274 handleRoutingChangeAndInformation(physicalAddress, message); 275 return Constants.HANDLED; 276 } 277 278 @Override 279 @ServiceThreadOnly 280 @Constants.HandleMessageResult handleRoutingInformation(HdmiCecMessage message)281 protected int handleRoutingInformation(HdmiCecMessage message) { 282 assertRunOnServiceThread(); 283 int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams()); 284 if (physicalAddress != mService.getPhysicalAddress() || !isActiveSource()) { 285 // Invalidate the active source if routing is changed to other physical address or 286 // our physical address while not active source 287 setActiveSource(physicalAddress, "HdmiCecLocalDeviceSource#handleRoutingInformation()"); 288 } 289 if (!isRoutingControlFeatureEnabled()) { 290 return Constants.ABORT_REFUSED; 291 } 292 handleRoutingChangeAndInformation(physicalAddress, message); 293 return Constants.HANDLED; 294 } 295 296 // Method to switch Input with the new Active Path. 297 // All the devices with Switch functionality should implement this. switchInputOnReceivingNewActivePath(int physicalAddress)298 protected void switchInputOnReceivingNewActivePath(int physicalAddress) { 299 // do nothing 300 } 301 302 // Only source devices that react to routing control messages should implement 303 // this method (e.g. a TV with built in switch). handleRoutingChangeAndInformation(int physicalAddress, HdmiCecMessage message)304 protected void handleRoutingChangeAndInformation(int physicalAddress, HdmiCecMessage message) { 305 // do nothing 306 } 307 308 @Override 309 @ServiceThreadOnly disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback)310 protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) { 311 removeAction(OneTouchPlayAction.class); 312 removeAction(DevicePowerStatusAction.class); 313 314 super.disableDevice(initiatedByCec, callback); 315 } 316 317 // Update the power status of the devices connected to the current device. 318 // This only works if the current device is a switch and keeps tracking the device info 319 // of the device connected to it. updateDevicePowerStatus(int logicalAddress, int newPowerStatus)320 protected void updateDevicePowerStatus(int logicalAddress, int newPowerStatus) { 321 // do nothing 322 } 323 324 @Constants.RcProfile 325 @Override getRcProfile()326 protected int getRcProfile() { 327 return Constants.RC_PROFILE_SOURCE; 328 } 329 330 @Override getRcFeatures()331 protected List<Integer> getRcFeatures() { 332 List<Integer> features = new ArrayList<>(); 333 HdmiCecConfig hdmiCecConfig = mService.getHdmiCecConfig(); 334 if (hdmiCecConfig.getIntValue( 335 HdmiControlManager.CEC_SETTING_NAME_RC_PROFILE_SOURCE_HANDLES_ROOT_MENU) 336 == HdmiControlManager.RC_PROFILE_SOURCE_MENU_HANDLED) { 337 features.add(Constants.RC_PROFILE_SOURCE_HANDLES_ROOT_MENU); 338 } 339 if (hdmiCecConfig.getIntValue( 340 HdmiControlManager.CEC_SETTING_NAME_RC_PROFILE_SOURCE_HANDLES_SETUP_MENU) 341 == HdmiControlManager.RC_PROFILE_SOURCE_MENU_HANDLED) { 342 features.add(Constants.RC_PROFILE_SOURCE_HANDLES_SETUP_MENU); 343 } 344 if (hdmiCecConfig.getIntValue( 345 HdmiControlManager.CEC_SETTING_NAME_RC_PROFILE_SOURCE_HANDLES_CONTENTS_MENU) 346 == HdmiControlManager.RC_PROFILE_SOURCE_MENU_HANDLED) { 347 features.add(Constants.RC_PROFILE_SOURCE_HANDLES_CONTENTS_MENU); 348 } 349 if (hdmiCecConfig.getIntValue( 350 HdmiControlManager.CEC_SETTING_NAME_RC_PROFILE_SOURCE_HANDLES_TOP_MENU) 351 == HdmiControlManager.RC_PROFILE_SOURCE_MENU_HANDLED) { 352 features.add(Constants.RC_PROFILE_SOURCE_HANDLES_TOP_MENU); 353 } 354 if (hdmiCecConfig.getIntValue(HdmiControlManager 355 .CEC_SETTING_NAME_RC_PROFILE_SOURCE_HANDLES_MEDIA_CONTEXT_SENSITIVE_MENU) 356 == HdmiControlManager.RC_PROFILE_SOURCE_MENU_HANDLED) { 357 features.add(Constants.RC_PROFILE_SOURCE_HANDLES_MEDIA_CONTEXT_SENSITIVE_MENU); 358 } 359 return features; 360 } 361 362 // Active source claiming needs to be handled in Service 363 // since service can decide who will be the active source when the device supports 364 // multiple device types in this method. 365 // This method should only be called when the device can be the active source. setAndBroadcastActiveSource(HdmiCecMessage message, int physicalAddress, String caller)366 protected void setAndBroadcastActiveSource(HdmiCecMessage message, int physicalAddress, 367 String caller) { 368 mService.setAndBroadcastActiveSource( 369 physicalAddress, getDeviceInfo().getDeviceType(), message.getSource(), caller); 370 } 371 372 // Indicates if current device is the active source or not 373 @ServiceThreadOnly isActiveSource()374 protected boolean isActiveSource() { 375 if (getDeviceInfo() == null) { 376 return false; 377 } 378 379 return getActiveSource().equals(getDeviceInfo().getLogicalAddress(), 380 getDeviceInfo().getPhysicalAddress()); 381 } 382 wakeUpIfActiveSource()383 protected void wakeUpIfActiveSource() { 384 if (!isActiveSource()) { 385 return; 386 } 387 // Wake up the device. This will also exit dream mode. 388 mService.wakeUp(); 389 return; 390 } 391 maySendActiveSource(int dest)392 protected void maySendActiveSource(int dest) { 393 if (!isActiveSource()) { 394 return; 395 } 396 addAndStartAction(new ActiveSourceAction(this, dest)); 397 } 398 399 /** 400 * Set {@link #mRoutingPort} to a specific {@link LocalActivePort} to record the current active 401 * CEC Routing Control related port. 402 * 403 * @param portId The portId of the new routing port. 404 */ 405 @VisibleForTesting setRoutingPort(@ocalActivePort int portId)406 protected void setRoutingPort(@LocalActivePort int portId) { 407 synchronized (mLock) { 408 mRoutingPort = portId; 409 } 410 } 411 412 /** 413 * Get {@link #mRoutingPort}. This is useful when the device needs to route to the last valid 414 * routing port. 415 */ 416 @LocalActivePort getRoutingPort()417 protected int getRoutingPort() { 418 synchronized (mLock) { 419 return mRoutingPort; 420 } 421 } 422 423 /** 424 * Get {@link #mLocalActivePort}. This is useful when device needs to know the current active 425 * port. 426 */ 427 @LocalActivePort getLocalActivePort()428 protected int getLocalActivePort() { 429 synchronized (mLock) { 430 return mLocalActivePort; 431 } 432 } 433 434 /** 435 * Set {@link #mLocalActivePort} to a specific {@link LocalActivePort} to record the current 436 * active port. 437 * 438 * <p>It does not have to be a Routing Control related port. For example it can be 439 * set to {@link Constants#CEC_SWITCH_ARC} but this port is System Audio related. 440 * 441 * @param activePort The portId of the new active port. 442 */ setLocalActivePort(@ocalActivePort int activePort)443 protected void setLocalActivePort(@LocalActivePort int activePort) { 444 synchronized (mLock) { 445 mLocalActivePort = activePort; 446 } 447 } 448 isRoutingControlFeatureEnabled()449 boolean isRoutingControlFeatureEnabled() { 450 synchronized (mLock) { 451 return mRoutingControlFeatureEnabled; 452 } 453 } 454 455 // Check if the device is trying to switch to the same input that is active right now. 456 // This can help avoid redundant port switching. isSwitchingToTheSameInput(@ocalActivePort int activePort)457 protected boolean isSwitchingToTheSameInput(@LocalActivePort int activePort) { 458 return activePort == getLocalActivePort(); 459 } 460 } 461