Adding CarAudioPowerListener

Listener handles initializing and registering with
CarPowerManagementService, and then enables and
disables CarAudioService accordingly.

Bug: 176258537
Test: atest CarAudioPowerListenerTest
Test: adb shell cmd car_service apply-power-policy
system_power_policy_suspend_to_ram and check dumpsys
for enabled status

Change-Id: I25c776a7d47ca384a756007ab304d4ba88a02240
diff --git a/service/src/com/android/car/audio/CarAudioPowerListener.java b/service/src/com/android/car/audio/CarAudioPowerListener.java
new file mode 100644
index 0000000..b946745
--- /dev/null
+++ b/service/src/com/android/car/audio/CarAudioPowerListener.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2021 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.car.audio;
+
+import static android.car.hardware.power.PowerComponent.AUDIO;
+
+import android.annotation.NonNull;
+import android.car.hardware.power.CarPowerPolicy;
+import android.car.hardware.power.CarPowerPolicyFilter;
+import android.car.hardware.power.ICarPowerPolicyListener;
+import android.util.Slog;
+
+import com.android.car.CarLocalServices;
+import com.android.car.CarLog;
+import com.android.car.power.CarPowerManagementService;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Objects;
+
+class CarAudioPowerListener {
+    private static final String TAG = CarLog.tagFor(CarAudioPowerListener.class);
+
+    private final Object mLock = new Object();
+    private final CarAudioService mCarAudioService;
+    private final CarPowerManagementService mCarPowerManagementService;
+
+    private final ICarPowerPolicyListener mChangeListener =
+            new ICarPowerPolicyListener.Stub() {
+                @Override
+                public void onPolicyChanged(CarPowerPolicy policy,
+                        CarPowerPolicy accumulatedPolicy) {
+                    synchronized (mLock) {
+                        if (mIsAudioEnabled != accumulatedPolicy.isComponentEnabled(AUDIO)) {
+                            updateAudioPowerStateLocked(accumulatedPolicy);
+                        }
+                    }
+                }
+            };
+
+    @GuardedBy("mLock")
+    private boolean mIsAudioEnabled;
+
+    static CarAudioPowerListener newCarAudioPowerListener(
+            @NonNull CarAudioService carAudioService) {
+        CarPowerManagementService carPowerService = CarLocalServices.getService(
+                CarPowerManagementService.class);
+        return new CarAudioPowerListener(carAudioService, carPowerService);
+    }
+
+    @VisibleForTesting
+    CarAudioPowerListener(@NonNull CarAudioService carAudioService,
+            CarPowerManagementService carPowerManagementService) {
+        mCarAudioService = Objects.requireNonNull(carAudioService);
+        mCarPowerManagementService = carPowerManagementService;
+    }
+
+    boolean isAudioEnabled() {
+        synchronized (mLock) {
+            return mIsAudioEnabled;
+        }
+    }
+
+    void startListeningForPolicyChanges() {
+        if (mCarPowerManagementService == null) {
+            Slog.w(TAG, "Cannot find CarPowerManagementService");
+            mCarAudioService.enableAudio();
+            return;
+        }
+
+        CarPowerPolicyFilter filter = new CarPowerPolicyFilter.Builder()
+                .setComponents(new int[]{AUDIO}).build();
+        mCarPowerManagementService.addPowerPolicyListener(filter, mChangeListener);
+        initializePowerState();
+    }
+
+    void stopListeningForPolicyChanges() {
+        if (mCarPowerManagementService == null) {
+            return;
+        }
+        mCarPowerManagementService.removePowerPolicyListener(mChangeListener);
+    }
+
+    private void initializePowerState() {
+        CarPowerPolicy policy = mCarPowerManagementService.getCurrentPowerPolicy();
+
+        if (policy == null) {
+            Slog.w(TAG, "Policy is null. Defaulting to enabled");
+            mCarAudioService.enableAudio();
+            return;
+        }
+
+        synchronized (mLock) {
+            updateAudioPowerStateLocked(policy);
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void updateAudioPowerStateLocked(CarPowerPolicy policy) {
+        mIsAudioEnabled = policy.isComponentEnabled(AUDIO);
+
+        if (mIsAudioEnabled) {
+            mCarAudioService.enableAudio();
+        } else {
+            mCarAudioService.disableAudio();
+        }
+    }
+}
diff --git a/service/src/com/android/car/audio/CarAudioService.java b/service/src/com/android/car/audio/CarAudioService.java
index 29e0885..b6291a4 100644
--- a/service/src/com/android/car/audio/CarAudioService.java
+++ b/service/src/com/android/car/audio/CarAudioService.java
@@ -189,6 +189,7 @@
     private OccupantZoneConfigChangeListener
             mOccupantZoneConfigChangeListener = new CarAudioOccupantConfigChangeListener();
     private CarAudioPlaybackCallback mCarAudioPlaybackCallback;
+    private CarAudioPowerListener mCarAudioPowerListener;
 
     public CarAudioService(Context context) {
         mContext = context;
@@ -229,6 +230,7 @@
                 setupDynamicRoutingLocked();
                 setupHalAudioFocusListenerLocked();
                 setupAudioConfigurationCallbackLocked();
+                setupPowerPolicyListener();
             } else {
                 Slog.i(CarLog.TAG_AUDIO, "Audio dynamic routing not enabled, run in legacy mode");
                 setupLegacyVolumeChangedListener();
@@ -240,6 +242,11 @@
         restoreMasterMuteState();
     }
 
+    private void setupPowerPolicyListener() {
+        mCarAudioPowerListener = CarAudioPowerListener.newCarAudioPowerListener(this);
+        mCarAudioPowerListener.startListeningForPolicyChanges();
+    }
+
     private void restoreMasterMuteState() {
         if (mUseCarVolumeGroupMuting) {
             return;
@@ -275,6 +282,10 @@
                 mAudioControlWrapper.unlinkToDeath();
                 mAudioControlWrapper = null;
             }
+
+            if (mCarAudioPowerListener != null) {
+                mCarAudioPowerListener.stopListeningForPolicyChanges();
+            }
         }
     }
 
@@ -282,9 +293,11 @@
     public void dump(IndentingPrintWriter writer) {
         writer.println("*CarAudioService*");
         writer.increaseIndent();
+
+        writer.println("Configurations:");
+        writer.increaseIndent();
         writer.printf("Run in legacy mode? %b\n", !mUseDynamicRouting);
         writer.printf("Persist master mute state? %b\n", mPersistMasterMuteState);
-        writer.printf("Master muted? %b\n", mAudioManager.isMasterMute());
         writer.printf("Use hal ducking signals %b\n", mUseHalDuckingSignals);
         writer.printf("Volume context priority list version: %d\n",
                 mAudioVolumeAdjustmentContextsVersion);
@@ -292,8 +305,18 @@
         if (mCarAudioConfigurationPath != null) {
             writer.printf("Car audio configuration path: %s\n", mCarAudioConfigurationPath);
         }
-        // Empty line for comfortable reading
+        writer.decreaseIndent();
         writer.println();
+
+        writer.println("Current State:");
+        writer.increaseIndent();
+        writer.printf("Master muted? %b\n", mAudioManager.isMasterMute());
+        if (mCarAudioPowerListener != null) {
+            writer.printf("Audio enabled? %b\n", mCarAudioPowerListener.isAudioEnabled());
+        }
+        writer.decreaseIndent();
+        writer.println();
+
         if (mUseDynamicRouting) {
             writer.printf("Volume Group Mute Enabled? %b\n", mUseCarVolumeGroupMuting);
             synchronized (mImplLock) {
@@ -344,6 +367,7 @@
                 writer.println("No HalAudioFocus instance\n");
             }
             if (mCarDucking != null) {
+                writer.println();
                 mCarDucking.dump(writer);
             }
             if (mCarVolumeGroupMuting != null) {
@@ -1209,6 +1233,20 @@
         return getCarAudioZone(zoneId).getInputAudioDevices();
     }
 
+    void disableAudio() {
+        // Todo (b/176258537) abandon focus and mute everything
+        if (Log.isLoggable(CarLog.TAG_AUDIO, Log.DEBUG)) {
+            Slog.d(CarLog.TAG_AUDIO, "Disabling audio");
+        }
+    }
+
+    void enableAudio() {
+        // Todo (b/176258537) resume focus and unmute appropriate things
+        if (Log.isLoggable(CarLog.TAG_AUDIO, Log.DEBUG)) {
+            Slog.d(CarLog.TAG_AUDIO, "Enabling audio");
+        }
+    }
+
     private void enforcePermission(String permissionName) {
         if (mContext.checkCallingOrSelfPermission(permissionName)
                 != PackageManager.PERMISSION_GRANTED) {
diff --git a/tests/carservice_unit_test/src/com/android/car/audio/CarAudioPowerListenerTest.java b/tests/carservice_unit_test/src/com/android/car/audio/CarAudioPowerListenerTest.java
new file mode 100644
index 0000000..7c2c580
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/audio/CarAudioPowerListenerTest.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2021 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.car.audio;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertThrows;
+
+import android.car.hardware.power.CarPowerPolicy;
+import android.car.hardware.power.CarPowerPolicyFilter;
+import android.car.hardware.power.ICarPowerPolicyListener;
+import android.car.hardware.power.PowerComponent;
+
+import com.android.car.power.CarPowerManagementService;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class CarAudioPowerListenerTest {
+    private static final String POLICY_ID = "POLICY_ID";
+    private static final int[] EMPTY_COMPONENTS = new int[0];
+    private static final int[] COMPONENTS_WITH_AUDIO = {PowerComponent.AUDIO};
+    private static final CarPowerPolicy ENABLED_POLICY = new CarPowerPolicy(POLICY_ID,
+            COMPONENTS_WITH_AUDIO, EMPTY_COMPONENTS);
+    private static final CarPowerPolicy DISABLED_POLICY = new CarPowerPolicy(POLICY_ID,
+            EMPTY_COMPONENTS, COMPONENTS_WITH_AUDIO);
+    private static final CarPowerPolicy EMPTY_POLICY = new CarPowerPolicy(POLICY_ID,
+            EMPTY_COMPONENTS, EMPTY_COMPONENTS);
+
+    @Mock
+    CarAudioService mMockCarAudioService;
+
+    @Mock
+    CarPowerManagementService mMockCarPowerService;
+
+    @Test
+    public void constructor_nullCarAudioService_throws() {
+        assertThrows(NullPointerException.class,
+                () -> new CarAudioPowerListener(null, mMockCarPowerService));
+    }
+
+    @Test
+    public void startListeningForPolicyChanges_withoutPowerService_enablesAudio() {
+        CarAudioPowerListener listener = new CarAudioPowerListener(mMockCarAudioService, null);
+
+        listener.startListeningForPolicyChanges();
+
+        verify(mMockCarAudioService).enableAudio();
+    }
+
+    @Test
+    public void startListeningForPolicyChanges_addsPowerPolicyListener() {
+        CarAudioPowerListener listener = new CarAudioPowerListener(mMockCarAudioService,
+                mMockCarPowerService);
+
+        listener.startListeningForPolicyChanges();
+
+        ArgumentCaptor<CarPowerPolicyFilter> captor = ArgumentCaptor.forClass(
+                CarPowerPolicyFilter.class);
+        verify(mMockCarPowerService).addPowerPolicyListener(
+                captor.capture(), any(ICarPowerPolicyListener.class));
+        assertThat(captor.getValue().components).asList().containsExactly(PowerComponent.AUDIO);
+
+    }
+
+    @Test
+    public void startListeningForPolicyChanges_withNullPolicy_enablesAudio() {
+        when(mMockCarPowerService.getCurrentPowerPolicy()).thenReturn(null);
+        CarAudioPowerListener listener = new CarAudioPowerListener(mMockCarAudioService,
+                mMockCarPowerService);
+
+        listener.startListeningForPolicyChanges();
+
+        verify(mMockCarAudioService).enableAudio();
+    }
+
+    @Test
+    public void startListeningForPolicyChanges_withPowerEnabled_enablesAudio() {
+        withAudioInitiallyEnabled();
+        CarAudioPowerListener listener = new CarAudioPowerListener(mMockCarAudioService,
+                mMockCarPowerService);
+
+        listener.startListeningForPolicyChanges();
+
+        verify(mMockCarAudioService).enableAudio();
+    }
+
+    @Test
+    public void startListeningForPolicyChanges_withPowerDisabled_disablesAudio() {
+        withAudioInitiallyDisabled();
+        CarAudioPowerListener listener = new CarAudioPowerListener(mMockCarAudioService,
+                mMockCarPowerService);
+
+        listener.startListeningForPolicyChanges();
+
+        verify(mMockCarAudioService).disableAudio();
+    }
+
+    @Test
+    public void onPolicyChange_withPowerSwitchingToEnabled_enablesAudio() throws Exception {
+        withAudioInitiallyDisabled();
+        ICarPowerPolicyListener changeListener = registerAndGetChangeListener();
+        verify(mMockCarAudioService, never()).enableAudio();
+
+        changeListener.onPolicyChanged(EMPTY_POLICY, ENABLED_POLICY);
+
+        verify(mMockCarAudioService).enableAudio();
+    }
+
+    @Test
+    public void onPolicyChange_withPowerRemainingEnabled_doesNothing() throws Exception {
+        withAudioInitiallyEnabled();
+        ICarPowerPolicyListener changeListener = registerAndGetChangeListener();
+        verify(mMockCarAudioService).enableAudio();
+
+        changeListener.onPolicyChanged(EMPTY_POLICY, ENABLED_POLICY);
+
+        verify(mMockCarAudioService).enableAudio();
+        verify(mMockCarAudioService, never()).disableAudio();
+    }
+
+    @Test
+    public void onPolicyChange_withPowerSwitchingToDisabled_disablesAudio() throws Exception {
+        withAudioInitiallyEnabled();
+        ICarPowerPolicyListener changeListener = registerAndGetChangeListener();
+        verify(mMockCarAudioService, never()).disableAudio();
+
+        changeListener.onPolicyChanged(EMPTY_POLICY, DISABLED_POLICY);
+
+        verify(mMockCarAudioService).disableAudio();
+    }
+
+    @Test
+    public void onPolicyChange_withPowerStayingDisabled_doesNothing() throws Exception {
+        withAudioInitiallyDisabled();
+        ICarPowerPolicyListener changeListener = registerAndGetChangeListener();
+        verify(mMockCarAudioService).disableAudio();
+
+        changeListener.onPolicyChanged(EMPTY_POLICY, DISABLED_POLICY);
+
+        verify(mMockCarAudioService).disableAudio();
+        verify(mMockCarAudioService, never()).enableAudio();
+    }
+
+    private void withAudioInitiallyEnabled() {
+        when(mMockCarPowerService.getCurrentPowerPolicy()).thenReturn(ENABLED_POLICY);
+    }
+
+    private void withAudioInitiallyDisabled() {
+        when(mMockCarPowerService.getCurrentPowerPolicy()).thenReturn(DISABLED_POLICY);
+    }
+
+    private ICarPowerPolicyListener registerAndGetChangeListener() {
+        CarAudioPowerListener listener = new CarAudioPowerListener(mMockCarAudioService,
+                mMockCarPowerService);
+        listener.startListeningForPolicyChanges();
+        ArgumentCaptor<ICarPowerPolicyListener> captor = ArgumentCaptor.forClass(
+                ICarPowerPolicyListener.class);
+        verify(mMockCarPowerService).addPowerPolicyListener(
+                any(), captor.capture());
+
+        return captor.getValue();
+    }
+}