Add SystemAudioInitiationActionFromAvr

To handle initiating the System Audio Control feature from an Amplifier
See CEC 13.15 for details.

Bug: 80297602
Test: make; local tests
Change-Id: I4772b6878bc1da816eea6c8e8b423c330315b1a8
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecFeatureAction.java b/services/core/java/com/android/server/hdmi/HdmiCecFeatureAction.java
index d26be57..11faa56 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecFeatureAction.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecFeatureAction.java
@@ -256,6 +256,10 @@
         return (HdmiCecLocalDeviceTv) mSource;
     }
 
+    protected final HdmiCecLocalDeviceAudioSystem audioSystem() {
+        return (HdmiCecLocalDeviceAudioSystem) mSource;
+    }
+
     protected final int getSourceAddress() {
         return mSource.getDeviceInfo().getLogicalAddress();
     }
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java
index 2ea46c7..37516d0 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java
@@ -18,7 +18,6 @@
 import android.hardware.hdmi.HdmiDeviceInfo;
 import android.media.AudioManager;
 import android.os.SystemProperties;
-import android.provider.Settings.Global;
 import com.android.internal.annotations.GuardedBy;
 import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly;
 
@@ -252,6 +251,12 @@
         }
     }
 
+    protected boolean isSystemAudioActivated() {
+        synchronized (mLock) {
+            return mSystemAudioActivated;
+        }
+    }
+
     /** Reports if System Audio Mode is supported by the connected TV */
     interface TvSystemAudioModeSupportedCallback {
 
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java b/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java
index 37f96142..f1cb246 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java
@@ -277,6 +277,16 @@
     }
 
     /**
+     * Build <Request Active Source> command.
+     *
+     * @param src source address of command
+     * @return newly created {@link HdmiCecMessage}
+     */
+    static HdmiCecMessage buildRequestActiveSource(int src) {
+        return buildCommand(src, Constants.ADDR_BROADCAST, Constants.MESSAGE_REQUEST_ACTIVE_SOURCE);
+    }
+
+    /**
      * Build <Active Source> command.
      *
      * @param src source address of command
diff --git a/services/core/java/com/android/server/hdmi/SystemAudioInitiationActionFromAvr.java b/services/core/java/com/android/server/hdmi/SystemAudioInitiationActionFromAvr.java
index bc0bad1..d4932f9 100644
--- a/services/core/java/com/android/server/hdmi/SystemAudioInitiationActionFromAvr.java
+++ b/services/core/java/com/android/server/hdmi/SystemAudioInitiationActionFromAvr.java
@@ -13,13 +13,23 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package com.android.server.hdmi;
 
-import android.util.Log;
+import android.hardware.tv.cec.V1_0.SendMessageResult;
+import com.android.internal.annotations.VisibleForTesting;
 
+/**
+ * Feature action that handles System Audio Mode initiated by AVR devices.
+ */
 public class SystemAudioInitiationActionFromAvr extends HdmiCecFeatureAction {
-    private static final String TAG = "SystemAudioInitFromAvr";
+
+    // State that waits for <Active Source> once send <Request Active Source>.
+    private static final int STATE_WAITING_FOR_ACTIVE_SOURCE = 1;
+    @VisibleForTesting
+    static final int MAX_RETRY_COUNT = 5;
+
+    private int mSendRequestActiveSourceRetryCount = 0;
+    private int mSendSetSystemAudioModeRetryCount = 0;
 
     SystemAudioInitiationActionFromAvr(HdmiCecLocalDevice source) {
         super(source);
@@ -27,18 +37,100 @@
 
     @Override
     boolean start() {
-        Log.i(TAG, "start");
-        return false;
+        if (audioSystem().mActiveSource.physicalAddress == Constants.INVALID_PHYSICAL_ADDRESS) {
+            mState = STATE_WAITING_FOR_ACTIVE_SOURCE;
+            addTimer(mState, HdmiConfig.TIMEOUT_MS);
+            sendRequestActiveSource();
+        } else {
+            queryTvSystemAudioModeSupport();
+        }
+        return true;
     }
 
     @Override
     boolean processCommand(HdmiCecMessage cmd) {
-        Log.i(TAG, "processCommand. cmd = " + cmd);
+        switch (cmd.getOpcode()) {
+            case Constants.MESSAGE_ACTIVE_SOURCE:
+                // received <Active Source>
+                if (mState != STATE_WAITING_FOR_ACTIVE_SOURCE) {
+                    return false;
+                }
+                mActionTimer.clearTimerMessage();
+                int physicalAddress = HdmiUtils.twoBytesToInt(cmd.getParams());
+                if (physicalAddress != getSourcePath()) {
+                    audioSystem().setActiveSource(cmd.getSource(), physicalAddress);
+                }
+                queryTvSystemAudioModeSupport();
+                return true;
+        }
         return false;
     }
 
     @Override
     void handleTimerEvent(int state) {
-        Log.i(TAG, "handleTimerEvent. state = " + state);
+        if (mState != state) {
+            return;
+        }
+
+        switch (mState) {
+            case STATE_WAITING_FOR_ACTIVE_SOURCE:
+                handleActiveSourceTimeout();
+                break;
+        }
+    }
+
+    protected void sendRequestActiveSource() {
+        sendCommand(HdmiCecMessageBuilder.buildRequestActiveSource(getSourceAddress()),
+                result -> {
+                    if (result != SendMessageResult.SUCCESS) {
+                        if (mSendRequestActiveSourceRetryCount < MAX_RETRY_COUNT) {
+                            mSendRequestActiveSourceRetryCount++;
+                            sendRequestActiveSource();
+                        } else {
+                            audioSystem().setSystemAudioMode(false);
+                            finish();
+                        }
+                    }
+                });
+    }
+
+    protected void sendSetSystemAudioMode(boolean on, int dest) {
+        sendCommand(HdmiCecMessageBuilder.buildSetSystemAudioMode(getSourceAddress(),
+                dest, on), result -> {
+                    if (result != SendMessageResult.SUCCESS) {
+                        if (mSendSetSystemAudioModeRetryCount < MAX_RETRY_COUNT) {
+                            mSendSetSystemAudioModeRetryCount++;
+                            sendSetSystemAudioMode(on, dest);
+                        } else {
+                            audioSystem().setSystemAudioMode(false);
+                            finish();
+                        }
+                    }
+                });
+    }
+
+    private void handleActiveSourceTimeout() {
+        HdmiLogger.debug("Cannot get active source.");
+        audioSystem().setSystemAudioMode(false);
+        finish();
+    }
+
+    private void queryTvSystemAudioModeSupport() {
+        audioSystem().queryTvSystemAudioModeSupport(
+                supported -> {
+                    if (supported) {
+                        if (audioSystem().setSystemAudioMode(true)) {
+                            sendSetSystemAudioMode(true, Constants.ADDR_BROADCAST);
+                        }
+                        finish();
+                    } else {
+                        audioSystem().setSystemAudioMode(false);
+                        finish();
+                    }
+                });
+    }
+
+    private void switchToRelevantInputForDeviceAt(int physicalAddress) {
+        // TODO(shubang): implement this method
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioInitiationActionFromAvrTest.java b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioInitiationActionFromAvrTest.java
new file mode 100644
index 0000000..ceac0ec6
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioInitiationActionFromAvrTest.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2018 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 static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.annotation.Nullable;
+import android.hardware.hdmi.HdmiDeviceInfo;
+import android.hardware.tv.cec.V1_0.SendMessageResult;
+import android.media.AudioManager;
+import android.os.Looper;
+import android.os.test.TestLooper;
+import android.support.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link SystemAudioInitiationActionFromAvr}
+ */
+@SmallTest
+@RunWith(JUnit4.class)
+public class SystemAudioInitiationActionFromAvrTest {
+
+    private HdmiCecLocalDeviceAudioSystem mHdmiCecLocalDeviceAudioSystem;
+    private TestLooper mTestLooper = new TestLooper();
+
+    private boolean mShouldDispatchActiveSource;
+    private boolean mTvSystemAudioModeSupport;
+    private int mTryCountBeforeSucceed;
+    private HdmiDeviceInfo mDeviceInfoForTests;
+
+    private int mMsgRequestActiveSourceCount;
+    private int mMsgSetSystemAudioModeCount;
+    private int mQueryTvSystemAudioModeSupportCount;
+
+    @Before
+    public void SetUp() {
+        mDeviceInfoForTests = new HdmiDeviceInfo(1001, 1234);
+        HdmiControlService hdmiControlService = new HdmiControlService(null) {
+
+            @Override
+            void sendCecCommand(HdmiCecMessage command,
+                    @Nullable SendMessageCallback callback) {
+                switch (command.getOpcode()) {
+                    case Constants.MESSAGE_REQUEST_ACTIVE_SOURCE:
+                        mMsgRequestActiveSourceCount++;
+                        if (mTryCountBeforeSucceed >= mMsgRequestActiveSourceCount
+                                && callback != null) {
+                            callback.onSendCompleted(SendMessageResult.NACK);
+                            break;
+                        }
+                        if (mShouldDispatchActiveSource) {
+                            mHdmiCecLocalDeviceAudioSystem.dispatchMessage(
+                                    HdmiCecMessageBuilder.buildActiveSource(
+                                            Constants.ADDR_TV, 1002));
+                        }
+                        break;
+                    case Constants.MESSAGE_SET_SYSTEM_AUDIO_MODE:
+                        mMsgSetSystemAudioModeCount++;
+                        if (mTryCountBeforeSucceed >= mMsgSetSystemAudioModeCount
+                                && callback != null) {
+                            callback.onSendCompleted(SendMessageResult.NACK);
+                        }
+                        break;
+                    default:
+                        throw new IllegalArgumentException("Unexpected message");
+                }
+            }
+
+            @Override
+            AudioManager getAudioManager() {
+                return new AudioManager() {
+
+                    @Override
+                    public int setHdmiSystemAudioSupported(boolean on) {
+                        return 0;
+                    }
+                };
+            }
+
+            @Override
+            boolean isPowerStandby() {
+                return false;
+            }
+
+            @Override
+            boolean isAddressAllocated() {
+                return true;
+            }
+        };
+        mHdmiCecLocalDeviceAudioSystem =
+                new HdmiCecLocalDeviceAudioSystem(hdmiControlService) {
+                    @Override
+                    void queryTvSystemAudioModeSupport(
+                            TvSystemAudioModeSupportedCallback callback) {
+                        mQueryTvSystemAudioModeSupportCount++;
+                        if (callback != null) {
+                            callback.onResult(mTvSystemAudioModeSupport);
+                        }
+                    }
+
+                    @Override
+                    HdmiDeviceInfo getDeviceInfo() {
+                        return mDeviceInfoForTests;
+                    }
+                };
+        mHdmiCecLocalDeviceAudioSystem.init();
+        Looper looper = mTestLooper.getLooper();
+        hdmiControlService.setIoLooper(looper);
+    }
+
+    @Test
+    public void testNoActiveSourceMessageReceived() {
+        resetTestVariables();
+        mShouldDispatchActiveSource = false;
+
+        assertThat(mHdmiCecLocalDeviceAudioSystem.mActiveSource.physicalAddress)
+                .isEqualTo(Constants.INVALID_PHYSICAL_ADDRESS);
+
+        mHdmiCecLocalDeviceAudioSystem.addAndStartAction(
+                new SystemAudioInitiationActionFromAvr(mHdmiCecLocalDeviceAudioSystem));
+        mTestLooper.dispatchAll();
+
+        assertThat(mMsgRequestActiveSourceCount).isEqualTo(1);
+        assertThat(mMsgSetSystemAudioModeCount).isEqualTo(0);
+        assertThat(mQueryTvSystemAudioModeSupportCount).isEqualTo(0);
+        assertFalse(mHdmiCecLocalDeviceAudioSystem.isSystemAudioActivated());
+
+        assertThat(mHdmiCecLocalDeviceAudioSystem.mActiveSource.physicalAddress)
+                .isEqualTo(Constants.INVALID_PHYSICAL_ADDRESS);
+    }
+
+    @Test
+    public void testTvNotSupport() {
+        resetTestVariables();
+        mShouldDispatchActiveSource = true;
+        mTvSystemAudioModeSupport = false;
+
+        mHdmiCecLocalDeviceAudioSystem.addAndStartAction(
+                new SystemAudioInitiationActionFromAvr(mHdmiCecLocalDeviceAudioSystem));
+        mTestLooper.dispatchAll();
+
+        assertThat(mMsgRequestActiveSourceCount).isEqualTo(1);
+        assertThat(mMsgSetSystemAudioModeCount).isEqualTo(0);
+        assertThat(mQueryTvSystemAudioModeSupportCount).isEqualTo(1);
+        assertFalse(mHdmiCecLocalDeviceAudioSystem.isSystemAudioActivated());
+    }
+
+    @Test
+    public void testTvSupport() {
+        resetTestVariables();
+        mShouldDispatchActiveSource = true;
+        mTvSystemAudioModeSupport = true;
+
+        mHdmiCecLocalDeviceAudioSystem.addAndStartAction(
+                new SystemAudioInitiationActionFromAvr(mHdmiCecLocalDeviceAudioSystem));
+        mTestLooper.dispatchAll();
+
+        assertThat(mMsgRequestActiveSourceCount).isEqualTo(1);
+        assertThat(mMsgSetSystemAudioModeCount).isEqualTo(1);
+        assertThat(mQueryTvSystemAudioModeSupportCount).isEqualTo(1);
+        assertTrue(mHdmiCecLocalDeviceAudioSystem.isSystemAudioActivated());
+
+        assertThat(mHdmiCecLocalDeviceAudioSystem.mActiveSource.physicalAddress).isEqualTo(1002);
+
+    }
+
+    @Test
+    public void testKnownActiveSource() {
+        resetTestVariables();
+        mTvSystemAudioModeSupport = true;
+        mHdmiCecLocalDeviceAudioSystem.mActiveSource.physicalAddress = 1001;
+
+        mHdmiCecLocalDeviceAudioSystem.addAndStartAction(
+                new SystemAudioInitiationActionFromAvr(mHdmiCecLocalDeviceAudioSystem));
+        mTestLooper.dispatchAll();
+
+        assertThat(mMsgRequestActiveSourceCount).isEqualTo(0);
+        assertThat(mMsgSetSystemAudioModeCount).isEqualTo(1);
+        assertThat(mQueryTvSystemAudioModeSupportCount).isEqualTo(1);
+        assertTrue(mHdmiCecLocalDeviceAudioSystem.isSystemAudioActivated());
+    }
+
+    @Test
+    public void testRetry() {
+        resetTestVariables();
+        mTvSystemAudioModeSupport = true;
+        mShouldDispatchActiveSource = true;
+        mTryCountBeforeSucceed = 3;
+        assertThat(mTryCountBeforeSucceed)
+                .isAtMost(SystemAudioInitiationActionFromAvr.MAX_RETRY_COUNT);
+        assertThat(mHdmiCecLocalDeviceAudioSystem.mActiveSource.physicalAddress)
+                .isEqualTo(Constants.INVALID_PHYSICAL_ADDRESS);
+
+        mHdmiCecLocalDeviceAudioSystem.addAndStartAction(
+                new SystemAudioInitiationActionFromAvr(mHdmiCecLocalDeviceAudioSystem));
+        mTestLooper.dispatchAll();
+
+        assertThat(mMsgRequestActiveSourceCount).isEqualTo(4);
+        assertThat(mMsgSetSystemAudioModeCount).isEqualTo(4);
+        assertThat(mQueryTvSystemAudioModeSupportCount).isEqualTo(1);
+        assertTrue(mHdmiCecLocalDeviceAudioSystem.isSystemAudioActivated());
+    }
+
+    private void resetTestVariables() {
+        mMsgRequestActiveSourceCount = 0;
+        mMsgSetSystemAudioModeCount = 0;
+        mQueryTvSystemAudioModeSupportCount = 0;
+        mTryCountBeforeSucceed = 0;
+        mHdmiCecLocalDeviceAudioSystem.mActiveSource.physicalAddress =
+                Constants.INVALID_PHYSICAL_ADDRESS;
+    }
+}