am f1faf930: am 553aeed9: Merge "CEC: Buffer messages coming from unknown device" into lmp-mr1-dev automerge: cccafac

* commit 'f1faf93094c2e49782a3ba26c2d0e67b13199238':
  CEC: Buffer messages coming from unknown device
diff --git a/services/core/java/com/android/server/hdmi/DelayedMessageBuffer.java b/services/core/java/com/android/server/hdmi/DelayedMessageBuffer.java
new file mode 100644
index 0000000..68311de
--- /dev/null
+++ b/services/core/java/com/android/server/hdmi/DelayedMessageBuffer.java
@@ -0,0 +1,133 @@
+/*
+ * 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.HdmiDeviceInfo;
+import android.util.Slog;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+
+/**
+ * Buffer storage to keep incoming messages for later processing. Used to
+ * handle messages that arrive when the device is not ready. Useful when
+ * keeping the messages from a connected device which are not discovered yet.
+ */
+final class DelayedMessageBuffer {
+    private final ArrayList<HdmiCecMessage> mBuffer = new ArrayList<>();
+    private final HdmiCecLocalDevice mDevice;
+
+    DelayedMessageBuffer(HdmiCecLocalDevice device) {
+        mDevice = device;
+    }
+
+    /**
+     * Add a new message to the buffer. The buffer keeps selected messages in
+     * the order they are received.
+     *
+     * @param message {@link HdmiCecMessage} to add
+     */
+    void add(HdmiCecMessage message) {
+        boolean buffered = true;
+
+        // Note that all the messages are not handled in the same manner.
+        // For &lt;Active Source&gt; we keep the latest one only.
+        // TODO: This might not be the best way to choose the active source.
+        //       Devise a better way to pick up the best one.
+        switch (message.getOpcode()) {
+            case Constants.MESSAGE_ACTIVE_SOURCE:
+                removeActiveSource();
+                mBuffer.add(message);
+                break;
+            case Constants.MESSAGE_INITIATE_ARC:
+                mBuffer.add(message);
+                break;
+            default:
+                buffered = false;
+                break;
+        }
+        if (buffered) {
+            HdmiLogger.debug("Buffering message:" + message);
+        }
+    }
+
+    private void removeActiveSource() {
+        // Uses iterator to remove elements while looping through the list.
+        for (Iterator<HdmiCecMessage> iter = mBuffer.iterator(); iter.hasNext(); ) {
+            HdmiCecMessage message = iter.next();
+            if (message.getOpcode() == Constants.MESSAGE_ACTIVE_SOURCE) {
+                iter.remove();
+            }
+        }
+    }
+
+    void processAllMessages() {
+        for (HdmiCecMessage message : mBuffer) {
+            mDevice.onMessage(message);
+            HdmiLogger.debug("Processing message:" + message);
+        }
+        mBuffer.clear();
+    }
+
+    /**
+     * Process messages from a given logical device. Called by
+     * {@link NewDeviceAction} actions when they finish adding the device
+     * information.
+     * <p>&lt;Active Source&gt; is not processed in this method but processed
+     * separately via {@link #processActiveSource()}.
+     *
+     * @param address logical address of CEC device which the messages to process
+     *        are associated with
+     */
+    void processMessagesForDevice(int address) {
+        HdmiLogger.debug("Processing message for address:" + address);
+        for (Iterator<HdmiCecMessage> iter = mBuffer.iterator(); iter.hasNext(); ) {
+            HdmiCecMessage message = iter.next();
+            if (message.getOpcode() == Constants.MESSAGE_ACTIVE_SOURCE) {
+                continue;
+            }
+            if (message.getSource() == address) {
+                mDevice.onMessage(message);
+                HdmiLogger.debug("Processing message:" + message);
+                iter.remove();
+            }
+        }
+    }
+
+    /**
+     * Process &lt;Active Source&gt;.
+     *
+     * <p>The message has a dependency on TV input framework. Should be invoked
+     * after we get the callback
+     * {@link android.media.tv.TvInputManager.TvInputCallback#onInputAdded(String)}
+     * to ensure the processing of the message takes effect when transformed
+     * to input change callback.
+     *
+     * @param address logical address of the device to be the active source
+     */
+    void processActiveSource(int address) {
+        for (Iterator<HdmiCecMessage> iter = mBuffer.iterator(); iter.hasNext(); ) {
+            HdmiCecMessage message = iter.next();
+            if (message.getOpcode() == Constants.MESSAGE_ACTIVE_SOURCE
+                    && message.getSource() == address) {
+                mDevice.onMessage(message);
+                HdmiLogger.debug("Processing message:" + message);
+                iter.remove();
+            }
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/hdmi/DeviceDiscoveryAction.java b/services/core/java/com/android/server/hdmi/DeviceDiscoveryAction.java
index 2ec9778..da404c4 100644
--- a/services/core/java/com/android/server/hdmi/DeviceDiscoveryAction.java
+++ b/services/core/java/com/android/server/hdmi/DeviceDiscoveryAction.java
@@ -326,6 +326,8 @@
         Slog.v(TAG, "--------------------------------------------");
         mCallback.onDeviceDiscoveryDone(result);
         finish();
+        // Process any commands buffered while device discovery action was in progress.
+        tv().processAllDelayedMessages();
     }
 
     private void checkAndProceedStage() {
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 6536165..0cdb4d5 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -30,6 +30,8 @@
 import static android.hardware.hdmi.HdmiControlManager.TIMER_RECORDING_TYPE_DIGITAL;
 import static android.hardware.hdmi.HdmiControlManager.TIMER_RECORDING_TYPE_EXTERNAL;
 
+import android.annotation.Nullable;
+import android.content.Context;
 import android.hardware.hdmi.HdmiControlManager;
 import android.hardware.hdmi.HdmiDeviceInfo;
 import android.hardware.hdmi.HdmiRecordSources;
@@ -37,6 +39,9 @@
 import android.hardware.hdmi.IHdmiControlCallback;
 import android.media.AudioManager;
 import android.media.AudioSystem;
+import android.media.tv.TvInputInfo;
+import android.media.tv.TvInputManager;
+import android.media.tv.TvInputManager.TvInputCallback;
 import android.os.RemoteException;
 import android.os.SystemProperties;
 import android.provider.Settings.Global;
@@ -49,6 +54,7 @@
 import com.android.server.hdmi.DeviceDiscoveryAction.DeviceDiscoveryCallback;
 import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly;
 import com.android.server.hdmi.HdmiControlService.SendMessageCallback;
+import com.android.server.SystemService;
 
 import java.io.UnsupportedEncodingException;
 import java.util.ArrayList;
@@ -123,6 +129,26 @@
     // other CEC devices since they might not have logical address.
     private final ArraySet<Integer> mCecSwitches = new ArraySet<Integer>();
 
+    // Message buffer used to buffer selected messages to process later. <Active Source>
+    // from a source device, for instance, needs to be buffered if the device is not
+    // discovered yet. The buffered commands are taken out and when they are ready to
+    // handle.
+    private final DelayedMessageBuffer mDelayedMessageBuffer = new DelayedMessageBuffer(this);
+
+    // Defines the callback invoked when TV input framework is updated with input status.
+    // We are interested in the notification for HDMI input addition event, in order to
+    // process any CEC commands that arrived before the input is added.
+    private final TvInputCallback mTvInputCallback = new TvInputCallback() {
+        @Override
+        public void onInputAdded(String inputId) {
+            TvInputInfo tvInfo = mService.getTvInputManager().getTvInputInfo(inputId);
+            HdmiDeviceInfo info = tvInfo.getHdmiDeviceInfo();
+            if (info != null && info.isCecDevice()) {
+                mDelayedMessageBuffer.processActiveSource(info.getLogicalAddress());
+            }
+        }
+    };
+
     HdmiCecLocalDeviceTv(HdmiControlService service) {
         super(service, HdmiDeviceInfo.DEVICE_TV);
         mPrevPortId = Constants.INVALID_PORT_ID;
@@ -136,6 +162,7 @@
     @ServiceThreadOnly
     protected void onAddressAllocated(int logicalAddress, int reason) {
         assertRunOnServiceThread();
+        mService.registerTvInputCallback(mTvInputCallback);
         mService.sendCecCommand(HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
                 mAddress, mService.getPhysicalAddress(), mDeviceType));
         mService.sendCecCommand(HdmiCecMessageBuilder.buildDeviceVendorIdCommand(
@@ -407,7 +434,9 @@
         int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams());
         HdmiDeviceInfo info = getCecDeviceInfo(logicalAddress);
         if (info == null) {
-            handleNewDeviceAtTheTailOfActivePath(physicalAddress);
+            if (!handleNewDeviceAtTheTailOfActivePath(physicalAddress)) {
+                mDelayedMessageBuffer.add(message);
+            }
         } else {
             ActiveSource activeSource = ActiveSource.of(logicalAddress, physicalAddress);
             ActiveSourceHandler.create(this, null).process(activeSource, info.getDeviceType());
@@ -555,7 +584,7 @@
                 activeSource.physicalAddress, deviceType));
     }
 
-    private void handleNewDeviceAtTheTailOfActivePath(int path) {
+    private boolean handleNewDeviceAtTheTailOfActivePath(int path) {
         // Seq #22
         if (isTailOfActivePath(path, getActivePath())) {
             removeAction(RoutingControlAction.class);
@@ -564,7 +593,9 @@
             mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange(
                     mAddress, getActivePath(), newPath));
             addAndStartAction(new RoutingControlAction(this, newPath, false, null));
+            return true;
         }
+        return false;
     }
 
     /**
@@ -706,8 +737,10 @@
     @ServiceThreadOnly
     void onNewAvrAdded(HdmiDeviceInfo avr) {
         assertRunOnServiceThread();
-        addAndStartAction(new SystemAudioAutoInitiationAction(this, avr.getLogicalAddress()));
-        if (isArcFeatureEnabled()) {
+        if (getSystemAudioModeSetting()) {
+            addAndStartAction(new SystemAudioAutoInitiationAction(this, avr.getLogicalAddress()));
+        }
+        if (isArcFeatureEnabled() && !hasAction(SetArcTransmissionStateAction.class)) {
             startArcAction(true);
         }
     }
@@ -941,6 +974,11 @@
         assertRunOnServiceThread();
 
         if (!canStartArcUpdateAction(message.getSource(), true)) {
+            if (getAvrDeviceInfo() == null) {
+                // AVR may not have been discovered yet. Delay the message processing.
+                mDelayedMessageBuffer.add(message);
+                return true;
+            }
             mService.maySendFeatureAbortCommand(message, Constants.ABORT_REFUSED);
             if (!isConnectedToArcPort(message.getSource())) {
                 displayOsd(OSD_MESSAGE_ARC_CONNECTED_INVALID_PORT);
@@ -1436,6 +1474,7 @@
     protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) {
         super.disableDevice(initiatedByCec, callback);
         assertRunOnServiceThread();
+        mService.unregisterTvInputCallback(mTvInputCallback);
         // Remove any repeated working actions.
         // HotplugDetectionAction will be reinstated during the wake up process.
         // HdmiControlService.onWakeUp() -> initializeLocalDevices() ->
@@ -1714,6 +1753,18 @@
         mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby(mAddress, targetAddress));
     }
 
+    @ServiceThreadOnly
+    void processAllDelayedMessages() {
+        assertRunOnServiceThread();
+        mDelayedMessageBuffer.processAllMessages();
+    }
+
+    @ServiceThreadOnly
+    void processDelayedMessages(int address) {
+        assertRunOnServiceThread();
+        mDelayedMessageBuffer.processMessagesForDevice(address);
+    }
+
     @Override
     protected void dump(final IndentingPrintWriter pw) {
         super.dump(pw);
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 907a49a..8a25f62 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -48,6 +48,8 @@
 import android.hardware.hdmi.IHdmiSystemAudioModeChangeListener;
 import android.hardware.hdmi.IHdmiVendorCommandListener;
 import android.media.AudioManager;
+import android.media.tv.TvInputManager;
+import android.media.tv.TvInputManager.TvInputCallback;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Handler;
@@ -272,6 +274,9 @@
     @Nullable
     private HdmiMhlControllerStub mMhlController;
 
+    @Nullable
+    private TvInputManager mTvInputManager;
+
     // Last input port before switching to the MHL port. Should switch back to this port
     // when the mobile device sends the request one touch play with off.
     // Gets invalidated if we go to other port/input.
@@ -343,6 +348,28 @@
         }
     }
 
+    @Override
+    public void onBootPhase(int phase) {
+        if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
+            mTvInputManager = (TvInputManager) getContext().getSystemService(
+                    Context.TV_INPUT_SERVICE);
+        }
+    }
+
+    TvInputManager getTvInputManager() {
+        return mTvInputManager;
+    }
+
+    void registerTvInputCallback(TvInputCallback callback) {
+        if (mTvInputManager == null) return;
+        mTvInputManager.registerCallback(callback, mHandler);
+    }
+
+    void unregisterTvInputCallback(TvInputCallback callback) {
+        if (mTvInputManager == null) return;
+        mTvInputManager.unregisterCallback(callback);
+    }
+
     /**
      * Called when the initialization of local devices is complete.
      */
diff --git a/services/core/java/com/android/server/hdmi/NewDeviceAction.java b/services/core/java/com/android/server/hdmi/NewDeviceAction.java
index 998889b..64f0703 100644
--- a/services/core/java/com/android/server/hdmi/NewDeviceAction.java
+++ b/services/core/java/com/android/server/hdmi/NewDeviceAction.java
@@ -161,6 +161,9 @@
                 mDeviceType, mVendorId, mDisplayName);
         tv().addCecDevice(deviceInfo);
 
+        // Consume CEC messages we already got for this newly found device.
+        tv().processDelayedMessages(mDeviceLogicalAddress);
+
         if (HdmiUtils.getTypeFromAddress(mDeviceLogicalAddress)
                 == HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM) {
             tv().onNewAvrAdded(deviceInfo);