diff options
author | Jinsuk Kim <jinsukkim@google.com> | 2014-05-13 16:36:15 +0900 |
---|---|---|
committer | Jinsuk Kim <jinsukkim@google.com> | 2014-05-22 11:15:51 +0900 |
commit | 78d695d8ba532214b02e7f18e0ccf89cf099163d (patch) | |
tree | c9d0b207e995dad311af7d270a881358ddda6891 /services | |
parent | e1a2e26a03e76279c9b03bdd81f92df70f8707e7 (diff) | |
download | frameworks_base-78d695d8ba532214b02e7f18e0ccf89cf099163d.zip frameworks_base-78d695d8ba532214b02e7f18e0ccf89cf099163d.tar.gz frameworks_base-78d695d8ba532214b02e7f18e0ccf89cf099163d.tar.bz2 |
Add feature actions for HDMI-CEC playback device
- OneTouchPlayAction
- DevicePowerStatusAction
- addHotplugEventListener
- removeHotplugEventListener
Change-Id: Ia7f31507ca62127efbacbbfe07ab43ba1f9bd4cf
Diffstat (limited to 'services')
4 files changed, 410 insertions, 2 deletions
diff --git a/services/core/java/com/android/server/hdmi/DevicePowerStatusAction.java b/services/core/java/com/android/server/hdmi/DevicePowerStatusAction.java new file mode 100644 index 0000000..63c2182 --- /dev/null +++ b/services/core/java/com/android/server/hdmi/DevicePowerStatusAction.java @@ -0,0 +1,105 @@ +package com.android.server.hdmi; + +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.hardware.hdmi.IHdmiControlCallback; +import android.hardware.hdmi.HdmiCec; +import android.hardware.hdmi.HdmiCecMessage; +import android.os.RemoteException; +import android.util.Slog; + +/** + * Feature action that queries the power status of other device. + * + * This action is initiated via {@link HdmiControlManager#queryDisplayStatus()} from + * the Android system working as playback device to get the power status of TV device. + * + * <p>Package-private, accessed by {@link HdmiControlService} only. + */ + +final class DevicePowerStatusAction extends FeatureAction { + private static final String TAG = "DevicePowerStatusAction"; + + // State in which the action is waiting for <Report Power Status>. + private static final int STATE_WAITING_FOR_REPORT_POWER_STATUS = 1; + + private final int mTargetAddress; + private final IHdmiControlCallback mCallback; + + static DevicePowerStatusAction create(HdmiControlService service, int sourceAddress, + int targetAddress, IHdmiControlCallback callback) { + if (service == null || callback == null) { + Slog.e(TAG, "Wrong arguments"); + return null; + } + return new DevicePowerStatusAction(service, sourceAddress, targetAddress, callback); + } + + private DevicePowerStatusAction(HdmiControlService service, int sourceAddress, + int targetAddress, IHdmiControlCallback callback) { + super(service, sourceAddress); + mTargetAddress = targetAddress; + mCallback = callback; + } + + @Override + boolean start() { + queryDevicePowerStatus(); + mState = STATE_WAITING_FOR_REPORT_POWER_STATUS; + addTimer(mState, FeatureAction.TIMEOUT_MS); + return true; + } + + private void queryDevicePowerStatus() { + mService.sendCecCommand( + HdmiCecMessageBuilder.buildGiveDevicePowerStatus(mSourceAddress, mTargetAddress)); + } + + @Override + boolean processCommand(HdmiCecMessage cmd) { + if (mState != STATE_WAITING_FOR_REPORT_POWER_STATUS) { + return false; + } + if (cmd.getOpcode() == HdmiCec.MESSAGE_REPORT_POWER_STATUS) { + int status = cmd.getParams()[0]; + invokeCallback(status); + finish(); + return true; + } + return false; + } + + @Override + void handleTimerEvent(int state) { + if (mState != state) { + return; + } + if (state == STATE_WAITING_FOR_REPORT_POWER_STATUS) { + // Got no response from TV. Report status 'unknown'. + invokeCallback(HdmiCec.POWER_STATUS_UNKNOWN); + finish(); + } + } + + private void invokeCallback(int result) { + try { + mCallback.onComplete(result); + } catch (RemoteException e) { + Slog.e(TAG, "Callback failed:" + e); + } + } +} diff --git a/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java b/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java index e47b90d..be270b9 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java @@ -230,6 +230,40 @@ public class HdmiCecMessageBuilder { } /** + * Build <Text View On> command. + * + * @param src source address of command + * @param dest destination address of command + * @return newly created {@link HdmiCecMessage} + */ + static HdmiCecMessage buildTextViewOn(int src, int dest) { + return buildCommand(src, dest, HdmiCec.MESSAGE_TEXT_VIEW_ON); + } + + /** + * Build <Active Source> command. + * + * @param src source address of command + * @param physicalAddress physical address of the device to become active + * @return newly created {@link HdmiCecMessage} + */ + static HdmiCecMessage buildActiveSource(int src, int physicalAddress) { + return buildCommand(src, HdmiCec.ADDR_BROADCAST, HdmiCec.MESSAGE_ACTIVE_SOURCE, + physicalAddressToParam(physicalAddress)); + } + + /** + * Build <Give Device Power Status> command. + * + * @param src source address of command + * @param dest destination address of command + * @return newly created {@link HdmiCecMessage} + */ + static HdmiCecMessage buildGiveDevicePowerStatus(int src, int dest) { + return buildCommand(src, dest, HdmiCec.MESSAGE_GIVE_DEVICE_POWER_STATUS); + } + + /** * Build a {@link HdmiCecMessage} without extra parameter. * * @param src source address of command @@ -253,4 +287,11 @@ public class HdmiCecMessageBuilder { private static HdmiCecMessage buildCommand(int src, int dest, int opcode, byte[] params) { return new HdmiCecMessage(src, dest, opcode, params); } + + private static byte[] physicalAddressToParam(int physicalAddress) { + return new byte[] { + (byte) (physicalAddress >> 8), + (byte) (physicalAddress & 0xFF) + }; + } } diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index 963e818..09153b9 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -18,18 +18,30 @@ package com.android.server.hdmi; import android.annotation.Nullable; import android.content.Context; +import android.hardware.hdmi.IHdmiControlCallback; +import android.hardware.hdmi.IHdmiControlService; +import android.hardware.hdmi.IHdmiHotplugEventListener; import android.hardware.hdmi.HdmiCec; import android.hardware.hdmi.HdmiCecDeviceInfo; import android.hardware.hdmi.HdmiCecMessage; import android.os.Handler; import android.os.HandlerThread; +import android.os.IBinder; import android.os.Looper; +import android.os.RemoteException; import android.util.Slog; +import android.util.SparseArray; + +import com.android.internal.annotations.GuardedBy; import com.android.server.SystemService; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.Iterator; import java.util.LinkedList; +import java.util.List; import java.util.Locale; /** @@ -39,6 +51,9 @@ import java.util.Locale; public final class HdmiControlService extends SystemService { private static final String TAG = "HdmiControlService"; + // TODO: Rename the permission to HDMI_CONTROL. + private static final String PERMISSION = "android.permission.HDMI_CEC"; + // A thread to handle synchronous IO of CEC and MHL control service. // Since all of CEC and MHL HAL interfaces processed in short time (< 200ms) // and sparse call it shares a thread to handle IO operations. @@ -48,6 +63,21 @@ public final class HdmiControlService extends SystemService { // Note that access to this collection should happen in service thread. private final LinkedList<FeatureAction> mActions = new LinkedList<>(); + // Used to synchronize the access to the service. + private final Object mLock = new Object(); + + // Type of logical devices hosted in the system. + @GuardedBy("mLock") + private final int[] mLocalDevices; + + // List of listeners registered by callers that want to get notified of + // hotplug events. + private final ArrayList<IHdmiHotplugEventListener> mHotplugEventListeners = new ArrayList<>(); + + // List of records for hotplug event listener to handle the the caller killed in action. + private final ArrayList<HotplugEventListenerRecord> mHotplugEventListenerRecords = + new ArrayList<>(); + @Nullable private HdmiCecController mCecController; @@ -63,6 +93,8 @@ public final class HdmiControlService extends SystemService { public HdmiControlService(Context context) { super(context); + mLocalDevices = getContext().getResources().getIntArray( + com.android.internal.R.array.config_hdmiCecLogicalDeviceType); } @Override @@ -70,8 +102,7 @@ public final class HdmiControlService extends SystemService { mIoThread.start(); mCecController = HdmiCecController.create(this); if (mCecController != null) { - mCecController.initializeLocalDevices(getContext().getResources() - .getIntArray(com.android.internal.R.array.config_hdmiCecLogicalDeviceType)); + mCecController.initializeLocalDevices(mLocalDevices); } else { Slog.i(TAG, "Device does not support HDMI-CEC."); } @@ -80,6 +111,9 @@ public final class HdmiControlService extends SystemService { if (mMhlController == null) { Slog.i(TAG, "Device does not support MHL-control."); } + + // TODO: Publish the BinderService + // publishBinderService(Context.HDMI_CONTROL_SERVICE, new BinderService()); } /** @@ -306,4 +340,95 @@ public final class HdmiControlService extends SystemService { Slog.w(TAG, "Failed to respond to <Get Menu Language>: " + message.toString()); } } + + // Record class that monitors the event of the caller of being killed. Used to clean up + // the listener list and record list accordingly. + private final class HotplugEventListenerRecord implements IBinder.DeathRecipient { + private final IHdmiHotplugEventListener mListener; + + public HotplugEventListenerRecord(IHdmiHotplugEventListener listener) { + mListener = listener; + } + + @Override + public void binderDied() { + synchronized (mLock) { + mHotplugEventListenerRecords.remove(this); + mHotplugEventListeners.remove(mListener); + } + } + } + + private void enforceAccessPermission() { + getContext().enforceCallingOrSelfPermission(PERMISSION, TAG); + } + + private final class BinderService extends IHdmiControlService.Stub { + @Override + public int[] getSupportedTypes() { + enforceAccessPermission(); + synchronized (mLock) { + return mLocalDevices; + } + } + + @Override + public void oneTouchPlay(IHdmiControlCallback callback) { + enforceAccessPermission(); + // TODO: Post a message for HdmiControlService#oneTouchPlay() + } + + @Override + public void queryDisplayStatus(IHdmiControlCallback callback) { + enforceAccessPermission(); + // TODO: Post a message for HdmiControlService#queryDisplayStatus() + } + + @Override + public void addHotplugEventListener(IHdmiHotplugEventListener listener) { + enforceAccessPermission(); + // TODO: Post a message for HdmiControlService#addHotplugEventListener() + } + + @Override + public void removeHotplugEventListener(IHdmiHotplugEventListener listener) { + enforceAccessPermission(); + // TODO: Post a message for HdmiControlService#removeHotplugEventListener() + } + } + + private void oneTouchPlay(IHdmiControlCallback callback) { + // TODO: Create a new action + } + + private void queryDisplayStatus(IHdmiControlCallback callback) { + // TODO: Create a new action + } + + private void addHotplugEventListener(IHdmiHotplugEventListener listener) { + HotplugEventListenerRecord record = new HotplugEventListenerRecord(listener); + try { + listener.asBinder().linkToDeath(record, 0); + } catch (RemoteException e) { + Slog.w(TAG, "Listener already died"); + return; + } + synchronized (mLock) { + mHotplugEventListenerRecords.add(record); + mHotplugEventListeners.add(listener); + } + } + + private void removeHotplugEventListener(IHdmiHotplugEventListener listener) { + synchronized (mLock) { + for (HotplugEventListenerRecord record : mHotplugEventListenerRecords) { + if (record.mListener.asBinder() == listener.asBinder()) { + listener.asBinder().unlinkToDeath(record, 0); + mHotplugEventListenerRecords.remove(record); + break; + } + } + mHotplugEventListeners.remove(listener); + } + } } diff --git a/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java b/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java new file mode 100644 index 0000000..69fad13 --- /dev/null +++ b/services/core/java/com/android/server/hdmi/OneTouchPlayAction.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.hdmi; + +import android.hardware.hdmi.IHdmiControlCallback; +import android.hardware.hdmi.HdmiCec; +import android.hardware.hdmi.HdmiCecMessage; +import android.os.RemoteException; +import android.util.Slog; + +/** + * Feature action that performs one touch play against TV/Display device. + * + * This action is initiated via {@link HdmiControlManager#oneTouchPlay()} from + * the Android system working as playback device to turn on the TV, and switch the input. + * + * <p>Package-private, accessed by {@link HdmiControlService} only. + */ + +public final class OneTouchPlayAction extends FeatureAction { + private static final String TAG = "OneTouchPlayAction"; + + // State in which the action is waiting for <Report Power Status>. In normal situation + // source device can simply send <Text|Image View On> and <Active Source> in succession + // since the standard requires that the TV/Display should buffer the <Active Source> + // if the TV is brought of out standby state. + // + // But there are TV's that fail to buffer the <Active Source> while getting out of + // standby mode, and do not accept the command until their power status becomes 'ON'. + // For a workaround, we send <Give Device Power Status> commands periodically to make sure + // the device switches its status to 'ON'. Then we send additional <Active Source>. + private static final int STATE_WAITING_FOR_REPORT_POWER_STATUS = 1; + + // The maximum number of times we send <Give Device Power Status> before we give up. + // We wait up to RESPONSE_TIMEOUT_MS * LOOP_COUNTER_MAX = 20 seconds. + private static final int LOOP_COUNTER_MAX = 10; + + private final int mSourcePath; + private final int mTargetAddress; + private final IHdmiControlCallback mCallback; + + private int mPowerStatusCounter = 0; + + // Factory method. Ensures arguments are valid. + static OneTouchPlayAction create(HdmiControlService service, int sourceAddress, + int sourcePath, int targetAddress, IHdmiControlCallback callback) { + if (service == null || callback == null) { + Slog.e(TAG, "Wrong arguments"); + return null; + } + return new OneTouchPlayAction(service, sourceAddress, sourcePath, targetAddress, callback); + } + + private OneTouchPlayAction(HdmiControlService service, int sourceAddress, int sourcePath, + int targetAddress, IHdmiControlCallback callback) { + super(service, sourceAddress); + mSourcePath = sourcePath; + mTargetAddress = targetAddress; + mCallback = callback; + } + + @Override + boolean start() { + mService.sendCecCommand( + HdmiCecMessageBuilder.buildTextViewOn(mSourceAddress, mTargetAddress)); + broadcastActiveSource(); + queryDevicePowerStatus(); + mState = STATE_WAITING_FOR_REPORT_POWER_STATUS; + addTimer(mState, FeatureAction.TIMEOUT_MS); + return true; + } + + private void broadcastActiveSource() { + mService.sendCecCommand( + HdmiCecMessageBuilder.buildActiveSource(mSourceAddress, mSourcePath)); + } + + private void queryDevicePowerStatus() { + mService.sendCecCommand( + HdmiCecMessageBuilder.buildGiveDevicePowerStatus(mSourceAddress, mTargetAddress)); + } + + @Override + boolean processCommand(HdmiCecMessage cmd) { + if (mState != STATE_WAITING_FOR_REPORT_POWER_STATUS) { + return false; + } + if (cmd.getOpcode() == HdmiCec.MESSAGE_REPORT_POWER_STATUS) { + int status = cmd.getParams()[0]; + if (status == HdmiCec.POWER_STATUS_ON) { + broadcastActiveSource(); + invokeCallback(HdmiCec.RESULT_SUCCESS); + finish(); + } + return true; + } + return false; + } + + @Override + void handleTimerEvent(int state) { + if (mState != state) { + return; + } + if (state == STATE_WAITING_FOR_REPORT_POWER_STATUS) { + if (mPowerStatusCounter++ < LOOP_COUNTER_MAX) { + queryDevicePowerStatus(); + addTimer(mState, FeatureAction.TIMEOUT_MS); + } else { + // Couldn't wake up the TV for whatever reason. Report failure. + invokeCallback(HdmiCec.RESULT_TIMEOUT); + finish(); + } + } + } + + private void invokeCallback(int result) { + try { + mCallback.onComplete(result); + } catch (RemoteException e) { + Slog.e(TAG, "Callback failed:" + e); + } + } +} |