HdmiCecLocalDevicePlayback.java revision 795415b57b7271a11d16ca42703251a325df1097
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
17package com.android.server.hdmi;
18
19import android.hardware.hdmi.HdmiControlManager;
20import android.hardware.hdmi.HdmiDeviceInfo;
21import android.hardware.hdmi.IHdmiControlCallback;
22import android.os.PowerManager;
23import android.os.PowerManager.WakeLock;
24import android.os.RemoteException;
25import android.os.SystemProperties;
26import android.provider.Settings.Global;
27import android.util.Slog;
28
29import com.android.internal.app.LocalePicker;
30import com.android.internal.app.LocalePicker.LocaleInfo;
31import com.android.internal.util.IndentingPrintWriter;
32import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly;
33
34import java.io.UnsupportedEncodingException;
35import java.util.List;
36
37/**
38 * Represent a logical device of type Playback residing in Android system.
39 */
40final class HdmiCecLocalDevicePlayback extends HdmiCecLocalDevice {
41    private static final String TAG = "HdmiCecLocalDevicePlayback";
42
43    private static final boolean WAKE_ON_HOTPLUG =
44            SystemProperties.getBoolean(Constants.PROPERTY_WAKE_ON_HOTPLUG, true);
45
46    private boolean mIsActiveSource = false;
47
48    // Used to keep the device awake while it is the active source. For devices that
49    // cannot wake up via CEC commands, this address the inconvenience of having to
50    // turn them on. True by default, and can be disabled (i.e. device can go to sleep
51    // in active device status) by explicitly setting the system property
52    // persist.sys.hdmi.keep_awake to false.
53    // Lazily initialized - should call getWakeLock() to get the instance.
54    private ActiveWakeLock mWakeLock;
55
56    // If true, turn off TV upon standby. False by default.
57    private boolean mAutoTvOff;
58
59    HdmiCecLocalDevicePlayback(HdmiControlService service) {
60        super(service, HdmiDeviceInfo.DEVICE_PLAYBACK);
61
62        mAutoTvOff = mService.readBooleanSetting(Global.HDMI_CONTROL_AUTO_DEVICE_OFF_ENABLED, false);
63
64        // The option is false by default. Update settings db as well to have the right
65        // initial setting on UI.
66        mService.writeBooleanSetting(Global.HDMI_CONTROL_AUTO_DEVICE_OFF_ENABLED, mAutoTvOff);
67    }
68
69    @Override
70    @ServiceThreadOnly
71    protected void onAddressAllocated(int logicalAddress, int reason) {
72        assertRunOnServiceThread();
73        mService.sendCecCommand(HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
74                mAddress, mService.getPhysicalAddress(), mDeviceType));
75        mService.sendCecCommand(HdmiCecMessageBuilder.buildDeviceVendorIdCommand(
76                mAddress, mService.getVendorId()));
77        startQueuedActions();
78    }
79
80    @Override
81    @ServiceThreadOnly
82    protected int getPreferredAddress() {
83        assertRunOnServiceThread();
84        return SystemProperties.getInt(Constants.PROPERTY_PREFERRED_ADDRESS_PLAYBACK,
85                Constants.ADDR_UNREGISTERED);
86    }
87
88    @Override
89    @ServiceThreadOnly
90    protected void setPreferredAddress(int addr) {
91        assertRunOnServiceThread();
92        SystemProperties.set(Constants.PROPERTY_PREFERRED_ADDRESS_PLAYBACK,
93                String.valueOf(addr));
94    }
95
96    @ServiceThreadOnly
97    void oneTouchPlay(IHdmiControlCallback callback) {
98        assertRunOnServiceThread();
99        if (hasAction(OneTouchPlayAction.class)) {
100            Slog.w(TAG, "oneTouchPlay already in progress");
101            invokeCallback(callback, HdmiControlManager.RESULT_ALREADY_IN_PROGRESS);
102            return;
103        }
104
105        // TODO: Consider the case of multiple TV sets. For now we always direct the command
106        //       to the primary one.
107        OneTouchPlayAction action = OneTouchPlayAction.create(this, Constants.ADDR_TV,
108                callback);
109        if (action == null) {
110            Slog.w(TAG, "Cannot initiate oneTouchPlay");
111            invokeCallback(callback, HdmiControlManager.RESULT_EXCEPTION);
112            return;
113        }
114        addAndStartAction(action);
115    }
116
117    @ServiceThreadOnly
118    void queryDisplayStatus(IHdmiControlCallback callback) {
119        assertRunOnServiceThread();
120        if (hasAction(DevicePowerStatusAction.class)) {
121            Slog.w(TAG, "queryDisplayStatus already in progress");
122            invokeCallback(callback, HdmiControlManager.RESULT_ALREADY_IN_PROGRESS);
123            return;
124        }
125        DevicePowerStatusAction action = DevicePowerStatusAction.create(this,
126                Constants.ADDR_TV, callback);
127        if (action == null) {
128            Slog.w(TAG, "Cannot initiate queryDisplayStatus");
129            invokeCallback(callback, HdmiControlManager.RESULT_EXCEPTION);
130            return;
131        }
132        addAndStartAction(action);
133    }
134
135    @ServiceThreadOnly
136    private void invokeCallback(IHdmiControlCallback callback, int result) {
137        assertRunOnServiceThread();
138        try {
139            callback.onComplete(result);
140        } catch (RemoteException e) {
141            Slog.e(TAG, "Invoking callback failed:" + e);
142        }
143    }
144
145    @Override
146    @ServiceThreadOnly
147    void onHotplug(int portId, boolean connected) {
148        assertRunOnServiceThread();
149        mCecMessageCache.flushAll();
150        // We'll not clear mIsActiveSource on the hotplug event to pass CETC 11.2.2-2 ~ 3.
151        if (WAKE_ON_HOTPLUG && connected && mService.isPowerStandbyOrTransient()) {
152            mService.wakeUp();
153        }
154        if (!connected) {
155            getWakeLock().release();
156        }
157    }
158
159    @Override
160    @ServiceThreadOnly
161    protected void onStandby(boolean initiatedByCec, int standbyAction) {
162        assertRunOnServiceThread();
163        if (!mService.isControlEnabled() || initiatedByCec) {
164            return;
165        }
166        switch (standbyAction) {
167            case HdmiControlService.STANDBY_SCREEN_OFF:
168                if (mAutoTvOff) {
169                    mService.sendCecCommand(
170                            HdmiCecMessageBuilder.buildStandby(mAddress, Constants.ADDR_TV));
171                }
172                break;
173            case HdmiControlService.STANDBY_SHUTDOWN:
174                // ACTION_SHUTDOWN is taken as a signal to power off all the devices.
175                mService.sendCecCommand(
176                        HdmiCecMessageBuilder.buildStandby(mAddress, Constants.ADDR_BROADCAST));
177                break;
178        }
179    }
180
181    @Override
182    @ServiceThreadOnly
183    void setAutoDeviceOff(boolean enabled) {
184        assertRunOnServiceThread();
185        mAutoTvOff = enabled;
186    }
187
188    @ServiceThreadOnly
189    void setActiveSource(boolean on) {
190        assertRunOnServiceThread();
191        mIsActiveSource = on;
192        if (on) {
193            getWakeLock().acquire();
194        } else {
195            getWakeLock().release();
196        }
197    }
198
199    @ServiceThreadOnly
200    private ActiveWakeLock getWakeLock() {
201        assertRunOnServiceThread();
202        if (mWakeLock == null) {
203            if (SystemProperties.getBoolean(Constants.PROPERTY_KEEP_AWAKE, true)) {
204                mWakeLock = new SystemWakeLock();
205            } else {
206                // Create a dummy lock object that doesn't do anything about wake lock,
207                // hence allows the device to go to sleep even if it's the active source.
208                mWakeLock = new ActiveWakeLock() {
209                    @Override
210                    public void acquire() { }
211                    @Override
212                    public void release() { }
213                    @Override
214                    public boolean isHeld() { return false; }
215                };
216                HdmiLogger.debug("No wakelock is used to keep the display on.");
217            }
218        }
219        return mWakeLock;
220    }
221
222    @Override
223    protected boolean canGoToStandby() {
224        return !getWakeLock().isHeld();
225    }
226
227    @Override
228    @ServiceThreadOnly
229    protected boolean handleActiveSource(HdmiCecMessage message) {
230        assertRunOnServiceThread();
231        int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams());
232        mayResetActiveSource(physicalAddress);
233        return true;  // Broadcast message.
234    }
235
236    private void mayResetActiveSource(int physicalAddress) {
237        if (physicalAddress != mService.getPhysicalAddress()) {
238            setActiveSource(false);
239        }
240    }
241
242    @ServiceThreadOnly
243    protected boolean handleUserControlPressed(HdmiCecMessage message) {
244        assertRunOnServiceThread();
245        wakeUpIfActiveSource();
246        return super.handleUserControlPressed(message);
247    }
248
249    @Override
250    @ServiceThreadOnly
251    protected boolean handleSetStreamPath(HdmiCecMessage message) {
252        assertRunOnServiceThread();
253        int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams());
254        maySetActiveSource(physicalAddress);
255        maySendActiveSource(message.getSource());
256        wakeUpIfActiveSource();
257        return true;  // Broadcast message.
258    }
259
260    // Samsung model we tested sends <Routing Change> and <Request Active Source>
261    // in a row, and then changes the input to the internal source if there is no
262    // <Active Source> in response. To handle this, we'll set ActiveSource aggressively.
263    @Override
264    @ServiceThreadOnly
265    protected boolean handleRoutingChange(HdmiCecMessage message) {
266        assertRunOnServiceThread();
267        int newPath = HdmiUtils.twoBytesToInt(message.getParams(), 2);
268        maySetActiveSource(newPath);
269        return true;  // Broadcast message.
270    }
271
272    @Override
273    @ServiceThreadOnly
274    protected boolean handleRoutingInformation(HdmiCecMessage message) {
275        assertRunOnServiceThread();
276        int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams());
277        maySetActiveSource(physicalAddress);
278        return true;  // Broadcast message.
279    }
280
281    private void maySetActiveSource(int physicalAddress) {
282        setActiveSource(physicalAddress == mService.getPhysicalAddress());
283    }
284
285    private void wakeUpIfActiveSource() {
286        if (!mIsActiveSource) {
287            return;
288        }
289        // Wake up the device if the power is in standby mode, or its screen is off -
290        // which can happen if the device is holding a partial lock.
291        if (mService.isPowerStandbyOrTransient() || !mService.getPowerManager().isScreenOn()) {
292            mService.wakeUp();
293        }
294    }
295
296    private void maySendActiveSource(int dest) {
297        if (mIsActiveSource) {
298            mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource(
299                    mAddress, mService.getPhysicalAddress()));
300            // Always reports menu-status active to receive RCP.
301            mService.sendCecCommand(HdmiCecMessageBuilder.buildReportMenuStatus(
302                    mAddress, dest, Constants.MENU_STATE_ACTIVATED));
303        }
304    }
305
306    @Override
307    @ServiceThreadOnly
308    protected boolean handleRequestActiveSource(HdmiCecMessage message) {
309        assertRunOnServiceThread();
310        maySendActiveSource(message.getSource());
311        return true;  // Broadcast message.
312    }
313
314    @ServiceThreadOnly
315    protected boolean handleSetMenuLanguage(HdmiCecMessage message) {
316        assertRunOnServiceThread();
317
318        try {
319            String iso3Language = new String(message.getParams(), 0, 3, "US-ASCII");
320
321            // Don't use Locale.getAvailableLocales() since it returns a locale
322            // which is not available on Settings.
323            final List<LocaleInfo> localeInfos = LocalePicker.getAllAssetLocales(
324                    mService.getContext(), false);
325            for (LocaleInfo localeInfo : localeInfos) {
326                if (localeInfo.getLocale().getISO3Language().equals(iso3Language)) {
327                    // WARNING: CEC adopts ISO/FDIS-2 for language code, while Android requires
328                    // additional country variant to pinpoint the locale. This keeps the right
329                    // locale from being chosen. 'eng' in the CEC command, for instance,
330                    // will always be mapped to en-AU among other variants like en-US, en-GB,
331                    // an en-IN, which may not be the expected one.
332                    LocalePicker.updateLocale(localeInfo.getLocale());
333                    return true;
334                }
335            }
336            Slog.w(TAG, "Can't handle <Set Menu Language> of " + iso3Language);
337            return false;
338        } catch (UnsupportedEncodingException e) {
339            return false;
340        }
341    }
342
343    @Override
344    @ServiceThreadOnly
345    protected void sendStandby(int deviceId) {
346        assertRunOnServiceThread();
347
348        // Playback device can send <Standby> to TV only. Ignore the parameter.
349        int targetAddress = Constants.ADDR_TV;
350        mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby(mAddress, targetAddress));
351    }
352
353    @Override
354    @ServiceThreadOnly
355    protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) {
356        super.disableDevice(initiatedByCec, callback);
357
358        assertRunOnServiceThread();
359        if (!initiatedByCec && mIsActiveSource) {
360            mService.sendCecCommand(HdmiCecMessageBuilder.buildInactiveSource(
361                    mAddress, mService.getPhysicalAddress()));
362        }
363        setActiveSource(false);
364        checkIfPendingActionsCleared();
365    }
366
367    @Override
368    protected void dump(final IndentingPrintWriter pw) {
369        super.dump(pw);
370        pw.println("mIsActiveSource: " + mIsActiveSource);
371        pw.println("mAutoTvOff:" + mAutoTvOff);
372    }
373
374    // Wrapper interface over PowerManager.WakeLock
375    private interface ActiveWakeLock {
376        void acquire();
377        void release();
378        boolean isHeld();
379    }
380
381    private class SystemWakeLock implements ActiveWakeLock {
382        private final WakeLock mWakeLock;
383        public SystemWakeLock() {
384            mWakeLock = mService.getPowerManager().newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
385            mWakeLock.setReferenceCounted(false);
386        }
387
388        @Override
389        public void acquire() {
390            mWakeLock.acquire();
391            HdmiLogger.debug("active source: %b. Wake lock acquired", mIsActiveSource);
392        }
393
394        @Override
395        public void release() {
396            mWakeLock.release();
397            HdmiLogger.debug("Wake lock released");
398        }
399
400        @Override
401        public boolean isHeld() {
402            return mWakeLock.isHeld();
403        }
404    }
405}
406