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