1 /*
2  * Copyright (C) 2014 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.content.ActivityNotFoundException;
21 import android.content.ComponentName;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.hardware.hdmi.HdmiControlManager;
25 import android.hardware.hdmi.HdmiDeviceInfo;
26 import android.hardware.hdmi.IHdmiControlCallback;
27 import android.hardware.tv.cec.V1_0.SendMessageResult;
28 import android.os.Binder;
29 import android.os.Handler;
30 import android.os.PowerManager;
31 import android.os.SystemProperties;
32 import android.sysprop.HdmiProperties;
33 import android.util.Slog;
34 
35 import com.android.internal.annotations.VisibleForTesting;
36 import com.android.internal.app.LocalePicker;
37 import com.android.internal.app.LocalePicker.LocaleInfo;
38 import com.android.internal.util.IndentingPrintWriter;
39 import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly;
40 import com.android.server.hdmi.HdmiControlService.SendMessageCallback;
41 
42 import java.io.UnsupportedEncodingException;
43 import java.util.List;
44 import java.util.Locale;
45 
46 /**
47  * Represent a logical device of type Playback residing in Android system.
48  */
49 public class HdmiCecLocalDevicePlayback extends HdmiCecLocalDeviceSource {
50     private static final String TAG = "HdmiCecLocalDevicePlayback";
51 
52     // How long to wait after hotplug out before possibly going to Standby.
53     @VisibleForTesting
54     static final long STANDBY_AFTER_HOTPLUG_OUT_DELAY_MS = 30_000;
55 
56     // How long to wait on active source lost before possibly going to Standby.
57     @VisibleForTesting
58     static final long STANDBY_AFTER_ACTIVE_SOURCE_LOST_DELAY_MS = 30_000;
59 
60     // How long to wait after losing active source, before launching the pop-up that allows the user
61     // to keep the device as the current active source.
62     // We do this to prevent an unnecessary pop-up from being displayed when we lose and regain
63     // active source within this timeout.
64     static final long POPUP_AFTER_ACTIVE_SOURCE_LOST_DELAY_MS = 5_000;
65 
66     // Used to keep the device awake while it is the active source. For devices that
67     // cannot wake up via CEC commands, this address the inconvenience of having to
68     // turn them on. True by default, and can be disabled (i.e. device can go to sleep
69     // in active device status) by explicitly setting the system property
70     // persist.sys.hdmi.keep_awake to false.
71     // Lazily initialized - should call getWakeLock() to get the instance.
72     private ActiveWakeLock mWakeLock;
73 
74     // Handler for queueing a delayed Standby runnable after hotplug out.
75     private Handler mDelayedStandbyHandler;
76 
77     // Handler for queueing a delayed Standby runnable after active source lost and after the pop-up
78     // on active source lost was displayed.
79     Handler mDelayedStandbyOnActiveSourceLostHandler;
80 
81     // Handler for queueing a delayed runnable that triggers a pop-up notification on active source
82     // lost.
83     private Handler mDelayedPopupOnActiveSourceLostHandler;
84 
85     // Determines what action should be taken upon receiving Routing Control messages.
86     @VisibleForTesting
87     protected HdmiProperties.playback_device_action_on_routing_control_values
88             mPlaybackDeviceActionOnRoutingControl = HdmiProperties
89                     .playback_device_action_on_routing_control()
90                     .orElse(HdmiProperties.playback_device_action_on_routing_control_values.NONE);
91 
HdmiCecLocalDevicePlayback(HdmiControlService service)92     HdmiCecLocalDevicePlayback(HdmiControlService service) {
93         super(service, HdmiDeviceInfo.DEVICE_PLAYBACK);
94 
95         mDelayedStandbyHandler = new Handler(service.getServiceLooper());
96         mDelayedStandbyOnActiveSourceLostHandler = new Handler(service.getServiceLooper());
97         mDelayedPopupOnActiveSourceLostHandler = new Handler(service.getServiceLooper());
98         mStandbyHandler = new HdmiCecStandbyModeHandler(service, this);
99     }
100 
101     @Override
102     @ServiceThreadOnly
onAddressAllocated(int logicalAddress, int reason)103     protected void onAddressAllocated(int logicalAddress, int reason) {
104         assertRunOnServiceThread();
105         if (reason == mService.INITIATED_BY_ENABLE_CEC) {
106             mService.setAndBroadcastActiveSource(mService.getPhysicalAddress(),
107                     getDeviceInfo().getDeviceType(), Constants.ADDR_BROADCAST,
108                     "HdmiCecLocalDevicePlayback#onAddressAllocated()");
109         }
110         mService.sendCecCommand(
111                 HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
112                         getDeviceInfo().getLogicalAddress(),
113                         mService.getPhysicalAddress(),
114                         mDeviceType));
115         mService.sendCecCommand(
116                 HdmiCecMessageBuilder.buildDeviceVendorIdCommand(
117                         getDeviceInfo().getLogicalAddress(), mService.getVendorId()));
118         // Actively send out an OSD name to the TV to update the TV panel in case the TV
119         // does not query the OSD name on time. This is not a required behavior by the spec.
120         // It is used for some TVs that need the OSD name update but don't query it themselves.
121         buildAndSendSetOsdName(Constants.ADDR_TV);
122         if (mService.audioSystem() == null) {
123             // If current device is not a functional audio system device,
124             // send message to potential audio system device in the system to get the system
125             // audio mode status. If no response, set to false.
126             mService.sendCecCommand(
127                     HdmiCecMessageBuilder.buildGiveSystemAudioModeStatus(
128                             getDeviceInfo().getLogicalAddress(), Constants.ADDR_AUDIO_SYSTEM),
129                     new SendMessageCallback() {
130                         @Override
131                         public void onSendCompleted(int error) {
132                             // In consideration of occasional transmission failures.
133                             if (error == SendMessageResult.NACK) {
134                                 HdmiLogger.debug(
135                                         "AVR did not respond to <Give System Audio Mode Status>");
136                                 mService.setSystemAudioActivated(false);
137                             }
138                         }
139                     });
140         }
141         launchDeviceDiscovery();
142         startQueuedActions();
143     }
144 
145     @ServiceThreadOnly
launchDeviceDiscovery()146     private void launchDeviceDiscovery() {
147         assertRunOnServiceThread();
148         clearDeviceInfoList();
149         if (hasAction(DeviceDiscoveryAction.class)) {
150             Slog.i(TAG, "Device Discovery Action is in progress. Restarting.");
151             removeAction(DeviceDiscoveryAction.class);
152         }
153         DeviceDiscoveryAction action = new DeviceDiscoveryAction(this,
154                 new DeviceDiscoveryAction.DeviceDiscoveryCallback() {
155                     @Override
156                     public void onDeviceDiscoveryDone(List<HdmiDeviceInfo> deviceInfos) {
157                         for (HdmiDeviceInfo info : deviceInfos) {
158                             mService.getHdmiCecNetwork().addCecDevice(info);
159                         }
160 
161                         // Since we removed all devices when it starts and device discovery action
162                         // does not poll local devices, we should put device info of local device
163                         // manually here.
164                         for (HdmiCecLocalDevice device : mService.getAllCecLocalDevices()) {
165                             mService.getHdmiCecNetwork().addCecDevice(device.getDeviceInfo());
166                         }
167 
168                         List<HotplugDetectionAction> hotplugActions =
169                                 getActions(HotplugDetectionAction.class);
170                         if (hotplugActions.isEmpty()) {
171                             addAndStartAction(
172                                     new HotplugDetectionAction(HdmiCecLocalDevicePlayback.this));
173                         }
174                     }
175                 });
176         addAndStartAction(action);
177     }
178 
179     @Override
180     @ServiceThreadOnly
getPreferredAddress()181     protected int getPreferredAddress() {
182         assertRunOnServiceThread();
183         return SystemProperties.getInt(Constants.PROPERTY_PREFERRED_ADDRESS_PLAYBACK,
184                 Constants.ADDR_UNREGISTERED);
185     }
186 
187     @Override
188     @ServiceThreadOnly
setPreferredAddress(int addr)189     protected void setPreferredAddress(int addr) {
190         assertRunOnServiceThread();
191         mService.writeStringSystemProperty(Constants.PROPERTY_PREFERRED_ADDRESS_PLAYBACK,
192                 String.valueOf(addr));
193     }
194 
195     /**
196      * Performs the action 'device select' or 'one touch play' initiated by a Playback device.
197      *
198      * @param id id of HDMI device to select
199      * @param callback callback object to report the result with
200      */
201     @ServiceThreadOnly
deviceSelect(int id, IHdmiControlCallback callback)202     void deviceSelect(int id, IHdmiControlCallback callback) {
203         assertRunOnServiceThread();
204         if (id == getDeviceInfo().getId()) {
205             mService.oneTouchPlay(callback);
206             return;
207         }
208         HdmiDeviceInfo targetDevice = mService.getHdmiCecNetwork().getDeviceInfo(id);
209         if (targetDevice == null) {
210             invokeCallback(callback, HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
211             return;
212         }
213         int targetAddress = targetDevice.getLogicalAddress();
214         if (isAlreadyActiveSource(targetDevice, targetAddress, callback)) {
215             return;
216         }
217         if (!mService.isCecControlEnabled()) {
218             setActiveSource(targetDevice, "HdmiCecLocalDevicePlayback#deviceSelect()");
219             invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE);
220             return;
221         }
222         removeAction(DeviceSelectActionFromPlayback.class);
223         addAndStartAction(new DeviceSelectActionFromPlayback(this, targetDevice, callback));
224     }
225 
226     @Override
227     @ServiceThreadOnly
onHotplug(int portId, boolean connected)228     void onHotplug(int portId, boolean connected) {
229         assertRunOnServiceThread();
230         mCecMessageCache.flushAll();
231 
232         if (connected) {
233             mDelayedStandbyHandler.removeCallbacksAndMessages(null);
234         } else {
235             // We'll not invalidate the active source on the hotplug event to pass CETC 11.2.2-2 ~ 3
236             getWakeLock().release();
237             mService.getHdmiCecNetwork().removeDevicesConnectedToPort(portId);
238 
239             mDelayedStandbyHandler.removeCallbacksAndMessages(null);
240             mDelayedStandbyHandler.postDelayed(new DelayedStandbyRunnable(),
241                     STANDBY_AFTER_HOTPLUG_OUT_DELAY_MS);
242         }
243     }
244 
245     /**
246      * Runnable for going to Standby if the device has been inactive for a certain amount of time.
247      * Posts a new instance of itself as a delayed message if the device was active.
248      */
249     private class DelayedStandbyRunnable implements Runnable {
250         @Override
run()251         public void run() {
252             if (mService.getPowerManagerInternal().wasDeviceIdleFor(
253                     STANDBY_AFTER_HOTPLUG_OUT_DELAY_MS)) {
254                 mService.standby();
255             } else {
256                 mDelayedStandbyHandler.postDelayed(new DelayedStandbyRunnable(),
257                         STANDBY_AFTER_HOTPLUG_OUT_DELAY_MS);
258             }
259         }
260     }
261 
262     private class DelayedStandbyOnActiveSourceLostRunnable implements Runnable {
263         @Override
run()264         public void run() {
265             if (mService.getPowerManagerInternal().wasDeviceIdleFor(
266                     STANDBY_AFTER_ACTIVE_SOURCE_LOST_DELAY_MS)) {
267                 mService.standby();
268             } else {
269                 mService.setAndBroadcastActiveSource(mService.getPhysicalAddress(),
270                         getDeviceInfo().getDeviceType(), Constants.ADDR_TV,
271                         "DelayedActiveSourceLostStandbyRunnable");
272             }
273         }
274     }
275 
276     @ServiceThreadOnly
dismissUiOnActiveSourceStatusRecovered()277     void dismissUiOnActiveSourceStatusRecovered() {
278         assertRunOnServiceThread();
279         Intent intent = new Intent(HdmiControlManager.ACTION_ON_ACTIVE_SOURCE_RECOVERED_DISMISS_UI);
280         mService.sendBroadcastAsUser(intent);
281     }
282 
283     @Override
284     @ServiceThreadOnly
onStandby(boolean initiatedByCec, int standbyAction, StandbyCompletedCallback callback)285     protected void onStandby(boolean initiatedByCec, int standbyAction,
286             StandbyCompletedCallback callback) {
287         assertRunOnServiceThread();
288         if (!mService.isCecControlEnabled()) {
289             invokeStandbyCompletedCallback(callback);
290             return;
291         }
292         boolean wasActiveSource = isActiveSource();
293         // Invalidate the internal active source record when going to standby
294         mService.setActiveSource(Constants.ADDR_INVALID, Constants.INVALID_PHYSICAL_ADDRESS,
295                 "HdmiCecLocalDevicePlayback#onStandby()");
296         if (!wasActiveSource) {
297             invokeStandbyCompletedCallback(callback);
298             return;
299         }
300         SendMessageCallback sendMessageCallback = new SendMessageCallback() {
301             @Override
302             public void onSendCompleted(int error) {
303                 invokeStandbyCompletedCallback(callback);
304             }
305         };
306         if (initiatedByCec) {
307             mService.sendCecCommand(
308                     HdmiCecMessageBuilder.buildInactiveSource(
309                             getDeviceInfo().getLogicalAddress(), mService.getPhysicalAddress()),
310                     sendMessageCallback);
311             return;
312         }
313         switch (standbyAction) {
314             case HdmiControlService.STANDBY_SCREEN_OFF:
315                 // Get latest setting value
316                 @HdmiControlManager.PowerControlMode
317                 String powerControlMode = mService.getHdmiCecConfig().getStringValue(
318                         HdmiControlManager.CEC_SETTING_NAME_POWER_CONTROL_MODE);
319                 switch (powerControlMode) {
320                     case HdmiControlManager.POWER_CONTROL_MODE_TV:
321                         mService.sendCecCommand(
322                                 HdmiCecMessageBuilder.buildStandby(
323                                         getDeviceInfo().getLogicalAddress(), Constants.ADDR_TV),
324                                 sendMessageCallback);
325                         break;
326                     case HdmiControlManager.POWER_CONTROL_MODE_TV_AND_AUDIO_SYSTEM:
327                         mService.sendCecCommand(
328                                 HdmiCecMessageBuilder.buildStandby(
329                                         getDeviceInfo().getLogicalAddress(), Constants.ADDR_TV));
330                         mService.sendCecCommand(
331                                 HdmiCecMessageBuilder.buildStandby(
332                                         getDeviceInfo().getLogicalAddress(),
333                                         Constants.ADDR_AUDIO_SYSTEM), sendMessageCallback);
334                         break;
335                     case HdmiControlManager.POWER_CONTROL_MODE_BROADCAST:
336                         mService.sendCecCommand(
337                                 HdmiCecMessageBuilder.buildStandby(
338                                         getDeviceInfo().getLogicalAddress(),
339                                         Constants.ADDR_BROADCAST), sendMessageCallback);
340                         break;
341                     case HdmiControlManager.POWER_CONTROL_MODE_NONE:
342                         mService.sendCecCommand(
343                                 HdmiCecMessageBuilder.buildInactiveSource(
344                                         getDeviceInfo().getLogicalAddress(),
345                                         mService.getPhysicalAddress()), sendMessageCallback);
346                         break;
347                 }
348                 break;
349             case HdmiControlService.STANDBY_SHUTDOWN:
350                 // ACTION_SHUTDOWN is taken as a signal to power off all the devices.
351                 mService.sendCecCommand(
352                         HdmiCecMessageBuilder.buildStandby(
353                                 getDeviceInfo().getLogicalAddress(), Constants.ADDR_BROADCAST),
354                         sendMessageCallback);
355                 break;
356         }
357     }
358 
359     @Override
360     @ServiceThreadOnly
onInitializeCecComplete(int initiatedBy)361     protected void onInitializeCecComplete(int initiatedBy) {
362         if (initiatedBy != HdmiControlService.INITIATED_BY_SCREEN_ON) {
363             return;
364         }
365         @HdmiControlManager.PowerControlMode
366         String powerControlMode = mService.getHdmiCecConfig().getStringValue(
367                 HdmiControlManager.CEC_SETTING_NAME_POWER_CONTROL_MODE);
368         if (powerControlMode.equals(HdmiControlManager.POWER_CONTROL_MODE_NONE)) {
369             return;
370         }
371         oneTouchPlay(new IHdmiControlCallback.Stub() {
372             @Override
373             public void onComplete(int result) {
374                 if (result != HdmiControlManager.RESULT_SUCCESS) {
375                     Slog.w(TAG, "Failed to complete One Touch Play. result=" + result);
376                 }
377             }
378         });
379     }
380 
381     @Override
382     @CallSuper
383     @ServiceThreadOnly
384     @VisibleForTesting
setActiveSource(int logicalAddress, int physicalAddress, String caller)385     protected void setActiveSource(int logicalAddress, int physicalAddress, String caller) {
386         assertRunOnServiceThread();
387         super.setActiveSource(logicalAddress, physicalAddress, caller);
388         if (isActiveSource()) {
389             getWakeLock().acquire();
390         } else {
391             getWakeLock().release();
392         }
393     }
394 
395     @ServiceThreadOnly
getWakeLock()396     private ActiveWakeLock getWakeLock() {
397         assertRunOnServiceThread();
398         if (mWakeLock == null) {
399             if (SystemProperties.getBoolean(Constants.PROPERTY_KEEP_AWAKE, true)) {
400                 mWakeLock = new SystemWakeLock();
401             } else {
402                 // Create a stub lock object that doesn't do anything about wake lock,
403                 // hence allows the device to go to sleep even if it's the active source.
404                 mWakeLock = new ActiveWakeLock() {
405                     @Override
406                     public void acquire() { }
407                     @Override
408                     public void release() { }
409                     @Override
410                     public boolean isHeld() { return false; }
411                 };
412                 HdmiLogger.debug("No wakelock is used to keep the display on.");
413             }
414         }
415         return mWakeLock;
416     }
417 
418     @Override
canGoToStandby()419     protected boolean canGoToStandby() {
420         return !getWakeLock().isHeld();
421     }
422 
423     @Override
424     @ServiceThreadOnly
onActiveSourceLost()425     protected void onActiveSourceLost() {
426         assertRunOnServiceThread();
427         mService.pauseActiveMediaSessions();
428         switch (mService.getHdmiCecConfig().getStringValue(
429                     HdmiControlManager.CEC_SETTING_NAME_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST)) {
430             case HdmiControlManager.POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST_STANDBY_NOW:
431                 mDelayedPopupOnActiveSourceLostHandler.removeCallbacksAndMessages(null);
432                 mDelayedPopupOnActiveSourceLostHandler.postDelayed(
433                         new Runnable() {
434                             @Override
435                             public void run() {
436                                 if (!isActiveSource()) {
437                                     startHdmiCecActiveSourceLostActivity();
438                                     mDelayedStandbyOnActiveSourceLostHandler
439                                             .removeCallbacksAndMessages(null);
440                                     mDelayedStandbyOnActiveSourceLostHandler.postDelayed(
441                                             new DelayedStandbyOnActiveSourceLostRunnable(),
442                                             STANDBY_AFTER_ACTIVE_SOURCE_LOST_DELAY_MS);
443                                 }
444                             }
445                         }, POPUP_AFTER_ACTIVE_SOURCE_LOST_DELAY_MS);
446                 return;
447             case HdmiControlManager.POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST_NONE:
448                 return;
449         }
450     }
451 
452     @VisibleForTesting
453     @ServiceThreadOnly
startHdmiCecActiveSourceLostActivity()454     void startHdmiCecActiveSourceLostActivity() {
455         final long identity = Binder.clearCallingIdentity();
456         try {
457             Context context = mService.getContext();
458             Intent intent = new Intent();
459             intent.setComponent(
460                     ComponentName.unflattenFromString(context.getResources().getString(
461                             com.android.internal.R.string.config_hdmiCecActiveSourceLostActivity
462                     )));
463             intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
464             context.startActivityAsUser(intent, context.getUser());
465         } catch (ActivityNotFoundException e) {
466             Slog.e(TAG, "Unable to start HdmiCecActiveSourceLostActivity");
467         } finally {
468             Binder.restoreCallingIdentity(identity);
469         }
470     }
471 
472     @ServiceThreadOnly
473     @Constants.HandleMessageResult
handleUserControlPressed(HdmiCecMessage message)474     protected int handleUserControlPressed(HdmiCecMessage message) {
475         assertRunOnServiceThread();
476         wakeUpIfActiveSource();
477         return super.handleUserControlPressed(message);
478     }
479 
480     @ServiceThreadOnly
481     @Constants.HandleMessageResult
handleSetMenuLanguage(HdmiCecMessage message)482     protected int handleSetMenuLanguage(HdmiCecMessage message) {
483         assertRunOnServiceThread();
484         if (mService.getHdmiCecConfig().getIntValue(
485                 HdmiControlManager.CEC_SETTING_NAME_SET_MENU_LANGUAGE)
486                     == HdmiControlManager.SET_MENU_LANGUAGE_DISABLED) {
487             return Constants.ABORT_UNRECOGNIZED_OPCODE;
488         }
489 
490         try {
491             String iso3Language = new String(message.getParams(), 0, 3, "US-ASCII");
492             Locale currentLocale = mService.getContext().getResources().getConfiguration().locale;
493             String curIso3Language = mService.localeToMenuLanguage(currentLocale);
494             HdmiLogger.debug("handleSetMenuLanguage " + iso3Language + " cur:" + curIso3Language);
495             if (curIso3Language.equals(iso3Language)) {
496                 // Do not switch language if the new language is the same as the current one.
497                 // This helps avoid accidental country variant switching from en_US to en_AU
498                 // due to the limitation of CEC. See the warning below.
499                 return Constants.HANDLED;
500             }
501 
502             // Don't use Locale.getAvailableLocales() since it returns a locale
503             // which is not available on Settings.
504             final List<LocaleInfo> localeInfos = LocalePicker.getAllAssetLocales(
505                     mService.getContext(), false);
506             for (LocaleInfo localeInfo : localeInfos) {
507                 if (mService.localeToMenuLanguage(localeInfo.getLocale()).equals(iso3Language)) {
508                     // WARNING: CEC adopts ISO/FDIS-2 for language code, while Android requires
509                     // additional country variant to pinpoint the locale. This keeps the right
510                     // locale from being chosen. 'eng' in the CEC command, for instance,
511                     // will always be mapped to en-AU among other variants like en-US, en-GB,
512                     // an en-IN, which may not be the expected one.
513                     startSetMenuLanguageActivity(localeInfo.getLocale());
514                     return Constants.HANDLED;
515                 }
516             }
517             Slog.w(TAG, "Can't handle <Set Menu Language> of " + iso3Language);
518             return Constants.ABORT_INVALID_OPERAND;
519         } catch (UnsupportedEncodingException e) {
520             Slog.w(TAG, "Can't handle <Set Menu Language>", e);
521             return Constants.ABORT_INVALID_OPERAND;
522         }
523     }
524 
startSetMenuLanguageActivity(Locale locale)525     private void startSetMenuLanguageActivity(Locale locale) {
526         final long identity = Binder.clearCallingIdentity();
527         try {
528             Context context = mService.getContext();
529             Intent intent = new Intent();
530             intent.putExtra(HdmiControlManager.EXTRA_LOCALE, locale.toLanguageTag());
531             intent.setComponent(
532                     ComponentName.unflattenFromString(context.getResources().getString(
533                             com.android.internal.R.string.config_hdmiCecSetMenuLanguageActivity)));
534             intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
535             context.startActivityAsUser(intent, context.getUser());
536         } catch (ActivityNotFoundException e) {
537             Slog.e(TAG, "unable to start HdmiCecSetMenuLanguageActivity");
538         } finally {
539             Binder.restoreCallingIdentity(identity);
540         }
541     }
542 
543     @Override
544     @Constants.HandleMessageResult
handleSetSystemAudioMode(HdmiCecMessage message)545     protected int handleSetSystemAudioMode(HdmiCecMessage message) {
546         // System Audio Mode only turns on/off when Audio System broadcasts on/off message.
547         // For device with type 4 and 5, it can set system audio mode on/off
548         // when there is another audio system device connected into the system first.
549         if (message.getDestination() != Constants.ADDR_BROADCAST
550                 || message.getSource() != Constants.ADDR_AUDIO_SYSTEM
551                 || mService.audioSystem() != null) {
552             return Constants.HANDLED;
553         }
554         boolean setSystemAudioModeOn = HdmiUtils.parseCommandParamSystemAudioStatus(message);
555         if (mService.isSystemAudioActivated() != setSystemAudioModeOn) {
556             mService.setSystemAudioActivated(setSystemAudioModeOn);
557         }
558         return Constants.HANDLED;
559     }
560 
561     @Override
562     @Constants.HandleMessageResult
handleSystemAudioModeStatus(HdmiCecMessage message)563     protected int handleSystemAudioModeStatus(HdmiCecMessage message) {
564         // Only directly addressed System Audio Mode Status message can change internal
565         // system audio mode status.
566         if (message.getDestination() == getDeviceInfo().getLogicalAddress()
567                 && message.getSource() == Constants.ADDR_AUDIO_SYSTEM) {
568             boolean setSystemAudioModeOn = HdmiUtils.parseCommandParamSystemAudioStatus(message);
569             if (mService.isSystemAudioActivated() != setSystemAudioModeOn) {
570                 mService.setSystemAudioActivated(setSystemAudioModeOn);
571             }
572         }
573         return Constants.HANDLED;
574     }
575 
576     @Override
577     @ServiceThreadOnly
578     @Constants.HandleMessageResult
handleRoutingChange(HdmiCecMessage message)579     protected int handleRoutingChange(HdmiCecMessage message) {
580         assertRunOnServiceThread();
581         int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams(), 2);
582         handleRoutingChangeAndInformation(physicalAddress, message);
583         return Constants.HANDLED;
584     }
585 
586     @Override
587     @ServiceThreadOnly
588     @Constants.HandleMessageResult
handleRoutingInformation(HdmiCecMessage message)589     protected int handleRoutingInformation(HdmiCecMessage message) {
590         assertRunOnServiceThread();
591         int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams());
592         HdmiDeviceInfo sourceDevice = mService.getHdmiCecNetwork()
593                 .getCecDeviceInfo(message.getSource());
594         // Ignore <Routing Information> messages pointing to the same physical address as the
595         // message sender. In this case, we shouldn't consider the sender to be the active source.
596         // See more b/321771821#comment7.
597         if (sourceDevice != null
598                 && sourceDevice.getLogicalAddress() != Constants.ADDR_TV
599                 && sourceDevice.getPhysicalAddress() == physicalAddress) {
600             Slog.d(TAG, "<Routing Information> is ignored, it is pointing to the same physical"
601                     + " address as the message sender");
602             return Constants.HANDLED;
603         }
604         handleRoutingChangeAndInformation(physicalAddress, message);
605         return Constants.HANDLED;
606     }
607 
608     @Override
609     @ServiceThreadOnly
handleRoutingChangeAndInformation(int physicalAddress, HdmiCecMessage message)610     protected void handleRoutingChangeAndInformation(int physicalAddress, HdmiCecMessage message) {
611         assertRunOnServiceThread();
612         // If the device is active source and received a <Routing Change> or <Routing Information>
613         // message to a physical address in the same active path do not change the Active Source
614         // status.
615         // E.g. TV [0.0.0.0] ------ Switch [2.0.0.0] ------ OTT [2.1.0.0] (Active Source)
616         // TV sends <Routing Change>[2.0.0.0] -> OTT is still Active Source
617         // TV sends <Routing Change>[0.0.0.0] -> OTT is not Active Source anymore.
618         // TV sends <Routing Change>[3.0.0.0] -> OTT is not Active Source anymore.
619         if (HdmiUtils.isInActiveRoutingPath(mService.getPhysicalAddress(), physicalAddress)
620                 && physicalAddress != Constants.TV_PHYSICAL_ADDRESS
621                 && isActiveSource()) {
622             return;
623         }
624         if (physicalAddress != mService.getPhysicalAddress()) {
625             setActiveSource(physicalAddress,
626                     "HdmiCecLocalDevicePlayback#handleRoutingChangeAndInformation()");
627             return;
628         }
629         if (!isActiveSource()) {
630             // If routing is changed to the device while Active Source, don't invalidate the
631             // Active Source
632             setActiveSource(physicalAddress,
633                     "HdmiCecLocalDevicePlayback#handleRoutingChangeAndInformation()");
634         }
635         dismissUiOnActiveSourceStatusRecovered();
636         switch (mPlaybackDeviceActionOnRoutingControl) {
637             case WAKE_UP_AND_SEND_ACTIVE_SOURCE:
638                 setAndBroadcastActiveSource(message, physicalAddress,
639                         "HdmiCecLocalDevicePlayback#handleRoutingChangeAndInformation()");
640                 break;
641             case WAKE_UP_ONLY:
642                 mService.wakeUp();
643                 break;
644             case NONE:
645                 break;
646         }
647     }
648 
649     /**
650      * Called after logical address allocation is finished, allowing a local device to react to
651      * messages in the buffer before they are processed. This method may be used to cancel deferred
652      * actions.
653      */
654     @Override
preprocessBufferedMessages(List<HdmiCecMessage> bufferedMessages)655     protected void preprocessBufferedMessages(List<HdmiCecMessage> bufferedMessages) {
656         for (HdmiCecMessage message: bufferedMessages) {
657             // Prevent the device from broadcasting <Active Source> message if the active path
658             // changed during address allocation.
659             if (message.getOpcode() == Constants.MESSAGE_ROUTING_CHANGE
660                     || message.getOpcode() == Constants.MESSAGE_SET_STREAM_PATH
661                     || message.getOpcode() == Constants.MESSAGE_ACTIVE_SOURCE) {
662                 removeAction(ActiveSourceAction.class);
663                 removeAction(OneTouchPlayAction.class);
664                 return;
665             }
666         }
667     }
668 
669     @Override
findKeyReceiverAddress()670     protected int findKeyReceiverAddress() {
671         return Constants.ADDR_TV;
672     }
673 
674     @Override
findAudioReceiverAddress()675     protected int findAudioReceiverAddress() {
676         if (mService.isSystemAudioActivated()) {
677             return Constants.ADDR_AUDIO_SYSTEM;
678         }
679         return Constants.ADDR_TV;
680     }
681 
682     @Override
683     @ServiceThreadOnly
disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback)684     protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) {
685         assertRunOnServiceThread();
686         removeAction(DeviceDiscoveryAction.class);
687         removeAction(HotplugDetectionAction.class);
688         removeAction(NewDeviceAction.class);
689         super.disableDevice(initiatedByCec, callback);
690         clearDeviceInfoList();
691         checkIfPendingActionsCleared();
692     }
693 
694     @Override
dump(final IndentingPrintWriter pw)695     protected void dump(final IndentingPrintWriter pw) {
696         super.dump(pw);
697         pw.println("isActiveSource(): " + isActiveSource());
698     }
699 
700     // Wrapper interface over PowerManager.WakeLock
701     private interface ActiveWakeLock {
acquire()702         void acquire();
release()703         void release();
isHeld()704         boolean isHeld();
705     }
706 
707     private class SystemWakeLock implements ActiveWakeLock {
708         private final WakeLockWrapper mWakeLock;
SystemWakeLock()709         public SystemWakeLock() {
710             mWakeLock = mService.getPowerManager().newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
711             mWakeLock.setReferenceCounted(false);
712         }
713 
714         @Override
acquire()715         public void acquire() {
716             mWakeLock.acquire();
717             HdmiLogger.debug("active source: %b. Wake lock acquired", isActiveSource());
718         }
719 
720         @Override
release()721         public void release() {
722             mWakeLock.release();
723             HdmiLogger.debug("Wake lock released");
724         }
725 
726         @Override
isHeld()727         public boolean isHeld() {
728             return mWakeLock.isHeld();
729         }
730     }
731 }
732