diff --git a/packages/SystemUI/res/values-television/config.xml b/packages/SystemUI/res/values-television/config.xml
index 0c8c5c443c4ad..dec83d95f0be6 100644
--- a/packages/SystemUI/res/values-television/config.xml
+++ b/packages/SystemUI/res/values-television/config.xml
@@ -43,6 +43,7 @@
- com.android.systemui.statusbar.notification.InstantAppNotifier
- com.android.systemui.toast.ToastUI
- com.android.systemui.wmshell.WMShell
+ - com.android.systemui.media.systemsounds.HomeSoundEffectController
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java
index 9f6c19bdf06fe..55359ea708735 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java
@@ -27,6 +27,7 @@ import com.android.systemui.biometrics.AuthController;
import com.android.systemui.globalactions.GlobalActionsComponent;
import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.keyguard.dagger.KeyguardModule;
+import com.android.systemui.media.systemsounds.HomeSoundEffectController;
import com.android.systemui.power.PowerUI;
import com.android.systemui.recents.Recents;
import com.android.systemui.recents.RecentsModule;
@@ -178,4 +179,10 @@ public abstract class SystemUIBinder {
@IntoMap
@ClassKey(WMShell.class)
public abstract SystemUI bindWMShell(WMShell sysui);
+
+ /** Inject into HomeSoundEffectController. */
+ @Binds
+ @IntoMap
+ @ClassKey(HomeSoundEffectController.class)
+ public abstract SystemUI bindHomeSoundEffectController(HomeSoundEffectController sysui);
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/systemsounds/HomeSoundEffectController.java b/packages/SystemUI/src/com/android/systemui/media/systemsounds/HomeSoundEffectController.java
new file mode 100644
index 0000000000000..dd3d02a86672a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/systemsounds/HomeSoundEffectController.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2020 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.systemui.media.systemsounds;
+
+import android.app.ActivityManager;
+import android.app.WindowConfiguration;
+import android.content.Context;
+import android.media.AudioManager;
+
+import com.android.systemui.SystemUI;
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.shared.system.TaskStackChangeListeners;
+
+import javax.inject.Inject;
+
+/**
+ * If a sound effect is defined for {@link android.media.AudioManager#FX_HOME}, a sound is played
+ * when the home task moves to front and the last task that moved to front was not the home task.
+ */
+@SysUISingleton
+public class HomeSoundEffectController extends SystemUI {
+
+ private final AudioManager mAudioManager;
+ private final TaskStackChangeListeners mTaskStackChangeListeners;
+ // Initialize true because home sound should not be played when the system boots.
+ private boolean mIsLastTaskHome = true;
+
+ @Inject
+ public HomeSoundEffectController(
+ Context context,
+ AudioManager audioManager,
+ TaskStackChangeListeners taskStackChangeListeners) {
+ super(context);
+ mAudioManager = audioManager;
+ mTaskStackChangeListeners = taskStackChangeListeners;
+ }
+
+ @Override
+ public void start() {
+ if (mAudioManager.isHomeSoundEffectEnabled()) {
+ mTaskStackChangeListeners.registerTaskStackListener(
+ new TaskStackChangeListener() {
+ @Override
+ public void onTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) {
+ handleHomeTaskMovedToFront(taskInfo);
+ }
+ });
+ }
+ }
+
+ private boolean isHomeTask(ActivityManager.RunningTaskInfo taskInfo) {
+ return taskInfo.getActivityType() == WindowConfiguration.ACTIVITY_TYPE_HOME;
+ }
+
+ /**
+ * To enable a home sound, check if the home app moves to front.
+ */
+ private void handleHomeTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) {
+ boolean isCurrentTaskHome = isHomeTask(taskInfo);
+ // If the last task is home we don't want to play the home sound. This avoids playing
+ // the home sound after FallbackHome transitions to Home
+ if (!mIsLastTaskHome && isCurrentTaskHome) {
+ mAudioManager.playSoundEffect(AudioManager.FX_HOME);
+ }
+ mIsLastTaskHome = isCurrentTaskHome;
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/systemsounds/HomeSoundEffectControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/systemsounds/HomeSoundEffectControllerTest.java
new file mode 100644
index 0000000000000..3a77f7eec7f91
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/systemsounds/HomeSoundEffectControllerTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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.systemui.media.systemsounds;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.app.ActivityManager;
+import android.app.WindowConfiguration;
+import android.content.Context;
+import android.media.AudioManager;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.shared.system.TaskStackChangeListeners;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class HomeSoundEffectControllerTest extends SysuiTestCase {
+
+ private @Mock Context mContext;
+ private @Mock AudioManager mAudioManager;
+ private @Mock TaskStackChangeListeners mTaskStackChangeListeners;
+ private @Mock ActivityManager.RunningTaskInfo mStandardActivityTaskInfo;
+ private @Mock ActivityManager.RunningTaskInfo mHomeActivityTaskInfo;
+
+ private HomeSoundEffectController mController;
+ private TaskStackChangeListener mTaskStackChangeListener;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ doReturn(WindowConfiguration.ACTIVITY_TYPE_STANDARD).when(
+ mStandardActivityTaskInfo).getActivityType();
+ doReturn(WindowConfiguration.ACTIVITY_TYPE_HOME).when(
+ mHomeActivityTaskInfo).getActivityType();
+
+ mController = new HomeSoundEffectController(mContext, mAudioManager,
+ mTaskStackChangeListeners);
+ }
+
+ @Test
+ public void testHomeSoundEffectNotPlayedWhenHomeFirstMovesToFront() {
+ // When HomeSoundEffectController is started and the home sound effect is enabled,
+ startController(true /* isHomeSoundEffectEnabled */);
+
+ // And the home task moves to the front,
+ mTaskStackChangeListener.onTaskMovedToFront(mHomeActivityTaskInfo);
+
+ // Then no home sound effect should be played.
+ verify(mAudioManager, never()).playSoundEffect(AudioManager.FX_HOME);
+ }
+
+ @Test
+ public void testHomeSoundEffectPlayedWhenEnabled() {
+ // When HomeSoundEffectController is started and the home sound effect is enabled,
+ startController(true /* isHomeSoundEffectEnabled */);
+
+ // And first a task different from the home task moves to front,
+ mTaskStackChangeListener.onTaskMovedToFront(mStandardActivityTaskInfo);
+
+ // And the home task moves to the front,
+ mTaskStackChangeListener.onTaskMovedToFront(mHomeActivityTaskInfo);
+
+ // Then the home sound effect should be played.
+ verify(mAudioManager).playSoundEffect(AudioManager.FX_HOME);
+ }
+
+ @Test
+ public void testHomeSoundEffectNotPlayedTwiceInRow() {
+ // When HomeSoundEffectController is started and the home sound effect is enabled,
+ startController(true /* isHomeSoundEffectEnabled */);
+
+ // And first a task different from the home task moves to front,
+ mTaskStackChangeListener.onTaskMovedToFront(mStandardActivityTaskInfo);
+
+ // And the home task moves to the front,
+ mTaskStackChangeListener.onTaskMovedToFront(mHomeActivityTaskInfo);
+
+ // Then the home sound effect should be played.
+ verify(mAudioManager).playSoundEffect(AudioManager.FX_HOME);
+
+ // If the home task moves to front a second time in a row,
+ mTaskStackChangeListener.onTaskMovedToFront(mHomeActivityTaskInfo);
+
+ // Then no home sound effect should be played.
+ verify(mAudioManager, times(1)).playSoundEffect(AudioManager.FX_HOME);
+ }
+
+ @Test
+ public void testHomeSoundEffectNotPlayedWhenNonHomeTaskMovesToFront() {
+ // When HomeSoundEffectController is started and the home sound effect is enabled,
+ startController(true /* isHomeSoundEffectEnabled */);
+
+ // And a standard, non-home task, moves to the front,
+ mTaskStackChangeListener.onTaskMovedToFront(mStandardActivityTaskInfo);
+
+ // Then no home sound effect should be played.
+ verify(mAudioManager, never()).playSoundEffect(AudioManager.FX_HOME);
+ }
+
+ @Test
+ public void testHomeSoundEffectDisabled() {
+ // When HomeSoundEffectController is started and the home sound effect is disabled,
+ startController(false /* isHomeSoundEffectEnabled */);
+
+ // Then no TaskStackListener should be registered
+ verify(mTaskStackChangeListeners, never()).registerTaskStackListener(
+ any(TaskStackChangeListener.class));
+ }
+
+ /**
+ * Sets {@link AudioManager#isHomeSoundEffectEnabled()} and starts HomeSoundEffectController.
+ * If the home sound effect is enabled, the registered TaskStackChangeListener is extracted.
+ */
+ private void startController(boolean isHomeSoundEffectEnabled) {
+ // Configure home sound effect to be enabled
+ doReturn(isHomeSoundEffectEnabled).when(mAudioManager).isHomeSoundEffectEnabled();
+
+ mController.start();
+
+ if (isHomeSoundEffectEnabled) {
+ // Construct controller. Save the TaskStackListener for injecting events.
+ final ArgumentCaptor listenerCaptor =
+ ArgumentCaptor.forClass(TaskStackChangeListener.class);
+ verify(mTaskStackChangeListeners).registerTaskStackListener(listenerCaptor.capture());
+ mTaskStackChangeListener = listenerCaptor.getValue();
+ }
+ }
+}