• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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