diff --git a/packages/SystemUI/res/drawable/hearing.xml b/packages/SystemUI/res/drawable/hearing.xml new file mode 100644 index 0000000000000..02f5f92ec5fed --- /dev/null +++ b/packages/SystemUI/res/drawable/hearing.xml @@ -0,0 +1,24 @@ + + + + diff --git a/packages/SystemUI/src/com/android/systemui/volume/CsdWarningDialog.java b/packages/SystemUI/src/com/android/systemui/volume/CsdWarningDialog.java index 35af44442ca92..e3ed2b405fb00 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/CsdWarningDialog.java +++ b/packages/SystemUI/src/com/android/systemui/volume/CsdWarningDialog.java @@ -16,20 +16,34 @@ package com.android.systemui.volume; +import static android.app.PendingIntent.FLAG_IMMUTABLE; + import android.annotation.StringRes; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.media.AudioManager; -import android.os.CountDownTimer; +import android.provider.Settings; import android.util.Log; import android.view.KeyEvent; import android.view.WindowManager; import com.android.internal.annotations.GuardedBy; +import com.android.internal.messages.nano.SystemMessageProto; +import com.android.systemui.R; +import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.statusbar.phone.SystemUIDialog; +import com.android.systemui.util.NotificationChannels; +import com.android.systemui.util.concurrency.DelayableExecutor; + +import dagger.assisted.Assisted; +import dagger.assisted.AssistedFactory; +import dagger.assisted.AssistedInject; /** * A class that implements the four Computed Sound Dose-related warnings defined in {@link AudioManager}: @@ -53,34 +67,58 @@ import com.android.systemui.statusbar.phone.SystemUIDialog; * communication between the native audio framework that implements the dose computation and the * audio service. */ -public abstract class CsdWarningDialog extends SystemUIDialog +public class CsdWarningDialog extends SystemUIDialog implements DialogInterface.OnDismissListener, DialogInterface.OnClickListener { private static final String TAG = Util.logTag(CsdWarningDialog.class); private static final int KEY_CONFIRM_ALLOWED_AFTER_MS = 1000; // milliseconds // time after which action is taken when the user hasn't ack'd or dismissed the dialog - private static final int NO_ACTION_TIMEOUT_MS = 5000; + public static final int NO_ACTION_TIMEOUT_MS = 5000; private final Context mContext; private final AudioManager mAudioManager; private final @AudioManager.CsdWarning int mCsdWarning; private final Object mTimerLock = new Object(); + /** * Timer to keep track of how long the user has before an action (here volume reduction) is * taken on their behalf. */ @GuardedBy("mTimerLock") - private final CountDownTimer mNoUserActionTimer; + private Runnable mNoUserActionRunnable; + private Runnable mCancelScheduledNoUserActionRunnable = null; + + private final DelayableExecutor mDelayableExecutor; + private NotificationManager mNotificationManager; + private Runnable mOnCleanup; private long mShowTime; - public CsdWarningDialog(@AudioManager.CsdWarning int csdWarning, Context context, - AudioManager audioManager) { + /** + * To inject dependencies and allow for easier testing + */ + @AssistedFactory + public interface Factory { + /** + * Create a dialog object + */ + CsdWarningDialog create(int csdWarning, Runnable onCleanup); + } + + @AssistedInject + public CsdWarningDialog(@Assisted @AudioManager.CsdWarning int csdWarning, Context context, + AudioManager audioManager, NotificationManager notificationManager, + @Background DelayableExecutor delayableExecutor, @Assisted Runnable onCleanup) { super(context); mCsdWarning = csdWarning; mContext = context; mAudioManager = audioManager; + mNotificationManager = notificationManager; + mOnCleanup = onCleanup; + + mDelayableExecutor = delayableExecutor; + getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ERROR); setShowForAllUsers(true); setMessage(mContext.getString(getStringForWarning(csdWarning))); @@ -95,25 +133,24 @@ public abstract class CsdWarningDialog extends SystemUIDialog Context.RECEIVER_EXPORTED_UNAUDITED); if (csdWarning == AudioManager.CSD_WARNING_DOSE_REACHED_1X) { - mNoUserActionTimer = new CountDownTimer(NO_ACTION_TIMEOUT_MS, NO_ACTION_TIMEOUT_MS) { - @Override - public void onTick(long millisUntilFinished) { } - - @Override - public void onFinish() { - if (mCsdWarning == AudioManager.CSD_WARNING_DOSE_REACHED_1X) { - // unlike on the 5x dose repeat, level is only reduced to RS1 - // when the warning is not acknowledged quick enough - mAudioManager.lowerVolumeToRs1(); - } + mNoUserActionRunnable = () -> { + if (mCsdWarning == AudioManager.CSD_WARNING_DOSE_REACHED_1X) { + // unlike on the 5x dose repeat, level is only reduced to RS1 when the warning + // is not acknowledged quickly enough + mAudioManager.lowerVolumeToRs1(); + sendNotification(); } }; } else { - mNoUserActionTimer = null; + mNoUserActionRunnable = null; } } - protected abstract void cleanUp(); + private void cleanUp() { + if (mOnCleanup != null) { + mOnCleanup.run(); + } + } // NOT overriding onKeyDown as we're not allowing a dismissal on any key other than // VOLUME_DOWN, and for this, we don't need to track if it's the start of a new @@ -153,12 +190,9 @@ public abstract class CsdWarningDialog extends SystemUIDialog super.onStart(); mShowTime = System.currentTimeMillis(); synchronized (mTimerLock) { - if (mNoUserActionTimer != null) { - new Thread(() -> { - synchronized (mTimerLock) { - mNoUserActionTimer.start(); - } - }).start(); + if (mNoUserActionRunnable != null) { + mCancelScheduledNoUserActionRunnable = mDelayableExecutor.executeDelayed( + mNoUserActionRunnable, NO_ACTION_TIMEOUT_MS); } } } @@ -166,8 +200,8 @@ public abstract class CsdWarningDialog extends SystemUIDialog @Override protected void onStop() { synchronized (mTimerLock) { - if (mNoUserActionTimer != null) { - mNoUserActionTimer.cancel(); + if (mCancelScheduledNoUserActionRunnable != null) { + mCancelScheduledNoUserActionRunnable.run(); } } } @@ -212,4 +246,32 @@ public abstract class CsdWarningDialog extends SystemUIDialog Log.e(TAG, "Invalid CSD warning event " + csdWarning, new Exception()); return com.android.internal.R.string.csd_dose_reached_warning; } + + + /** + * In case user did not respond to the dialog, they still need to know volume was lowered. + */ + private void sendNotification() { + Intent intent = new Intent(Settings.ACTION_SOUND_SETTINGS); + PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, + FLAG_IMMUTABLE); + + String text = mContext.getString(R.string.csd_system_lowered_text); + String title = mContext.getString(R.string.csd_lowered_title); + + Notification.Builder builder = + new Notification.Builder(mContext, NotificationChannels.ALERTS) + .setSmallIcon(R.drawable.hearing) + .setContentTitle(title) + .setContentText(text) + .setContentIntent(pendingIntent) + .setStyle(new Notification.BigTextStyle().bigText(text)) + .setVisibility(Notification.VISIBILITY_PUBLIC) + .setLocalOnly(true) + .setAutoCancel(true) + .setCategory(Notification.CATEGORY_SYSTEM); + + mNotificationManager.notify(SystemMessageProto.SystemMessage.NOTE_CSD_LOWER_AUDIO, + builder.build()); + } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 95cc12a48bb20..3c007f99a6546 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -263,6 +263,7 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, private final ConfigurationController mConfigurationController; private final MediaOutputDialogFactory mMediaOutputDialogFactory; private final VolumePanelFactory mVolumePanelFactory; + private final CsdWarningDialog.Factory mCsdWarningDialogFactory; private final ActivityStarter mActivityStarter; private boolean mShowing; @@ -311,6 +312,7 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, InteractionJankMonitor interactionJankMonitor, DeviceConfigProxy deviceConfigProxy, Executor executor, + CsdWarningDialog.Factory csdWarningDialogFactory, DumpManager dumpManager) { mContext = new ContextThemeWrapper(context, R.style.volume_dialog_theme); @@ -322,6 +324,7 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, mConfigurationController = configurationController; mMediaOutputDialogFactory = mediaOutputDialogFactory; mVolumePanelFactory = volumePanelFactory; + mCsdWarningDialogFactory = csdWarningDialogFactory; mActivityStarter = activityStarter; mShowActiveStreamOnly = showActiveStreamOnly(); mHasSeenODICaptionsTooltip = @@ -2084,21 +2087,21 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, rescheduleTimeoutH(); } - private void showCsdWarningH(int csdWarning, int durationMs) { + @VisibleForTesting void showCsdWarningH(int csdWarning, int durationMs) { synchronized (mSafetyWarningLock) { + if (mCsdDialog != null) { return; } - mCsdDialog = new CsdWarningDialog(csdWarning, - mContext, mController.getAudioManager()) { - @Override - protected void cleanUp() { - synchronized (mSafetyWarningLock) { - mCsdDialog = null; - } - recheckH(null); + + final Runnable cleanUp = () -> { + synchronized (mSafetyWarningLock) { + mCsdDialog = null; } + recheckH(null); }; + + mCsdDialog = mCsdWarningDialogFactory.create(csdWarning, cleanUp); mCsdDialog.show(); } recheckH(null); diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java index 0ab6c690e1e14..14d3ca334073e 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java @@ -30,17 +30,18 @@ import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.util.DeviceConfigProxy; +import com.android.systemui.volume.CsdWarningDialog; import com.android.systemui.volume.VolumeComponent; import com.android.systemui.volume.VolumeDialogComponent; import com.android.systemui.volume.VolumeDialogImpl; import com.android.systemui.volume.VolumePanelFactory; -import java.util.concurrent.Executor; - import dagger.Binds; import dagger.Module; import dagger.Provides; +import java.util.concurrent.Executor; + /** Dagger Module for code in the volume package. */ @Module @@ -63,6 +64,7 @@ public interface VolumeModule { InteractionJankMonitor interactionJankMonitor, DeviceConfigProxy deviceConfigProxy, @Main Executor executor, + CsdWarningDialog.Factory csdFactory, DumpManager dumpManager) { VolumeDialogImpl impl = new VolumeDialogImpl( context, @@ -76,6 +78,7 @@ public interface VolumeModule { interactionJankMonitor, deviceConfigProxy, executor, + csdFactory, dumpManager); impl.setStreamImportant(AudioManager.STREAM_SYSTEM, false); impl.setAutomute(true); diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/CsdWarningDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/CsdWarningDialogTest.java new file mode 100644 index 0000000000000..9cf3e443320d2 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/CsdWarningDialogTest.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2023 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.volume; + +import static android.media.AudioManager.CSD_WARNING_DOSE_REACHED_1X; +import static android.media.AudioManager.CSD_WARNING_DOSE_REPEATED_5X; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.app.Notification; +import android.app.NotificationManager; +import android.media.AudioManager; +import android.test.suitebuilder.annotation.SmallTest; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import com.android.internal.messages.nano.SystemMessageProto; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.util.concurrency.FakeExecutor; +import com.android.systemui.util.time.FakeSystemClock; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class CsdWarningDialogTest extends SysuiTestCase { + + private NotificationManager mNotificationManager; + private AudioManager mAudioManager; + + @Before + public void setup() { + mNotificationManager = mock(NotificationManager.class); + getContext().addMockSystemService(NotificationManager.class, mNotificationManager); + + mAudioManager = mock(AudioManager.class); + getContext().addMockSystemService(AudioManager.class, mAudioManager); + } + + @Test + public void create1XCsdDialogAndWait_sendsNotification() { + FakeExecutor executor = new FakeExecutor(new FakeSystemClock()); + // instantiate directly instead of via factory; we don't want executor to be @Background + CsdWarningDialog dialog = new CsdWarningDialog(CSD_WARNING_DOSE_REACHED_1X, mContext, + mAudioManager, mNotificationManager, executor, null); + + dialog.show(); + executor.advanceClockToLast(); + executor.runAllReady(); + dialog.dismiss(); + + verify(mNotificationManager).notify( + eq(SystemMessageProto.SystemMessage.NOTE_CSD_LOWER_AUDIO), any(Notification.class)); + } + + @Test + public void create5XCsdDiSalogAndWait_willNotSendNotification() { + FakeExecutor executor = new FakeExecutor(new FakeSystemClock()); + CsdWarningDialog dialog = new CsdWarningDialog(CSD_WARNING_DOSE_REPEATED_5X, mContext, + mAudioManager, mNotificationManager, executor, null); + + dialog.show(); + executor.advanceClockToLast(); + executor.runAllReady(); + dialog.dismiss(); + + verify(mNotificationManager, never()).notify( + eq(SystemMessageProto.SystemMessage.NOTE_CSD_LOWER_AUDIO), any(Notification.class)); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java index d419095921b86..eb2688894cb0a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java @@ -102,6 +102,15 @@ public class VolumeDialogImplTest extends SysuiTestCase { InteractionJankMonitor mInteractionJankMonitor; @Mock private DumpManager mDumpManager; + @Mock CsdWarningDialog mCsdWarningDialog; + + private final CsdWarningDialog.Factory mCsdWarningDialogFactory = + new CsdWarningDialog.Factory() { + @Override + public CsdWarningDialog create(int warningType, Runnable onCleanup) { + return mCsdWarningDialog; + } + }; @Before public void setup() throws Exception { @@ -124,6 +133,7 @@ public class VolumeDialogImplTest extends SysuiTestCase { mInteractionJankMonitor, mDeviceConfigProxy, mExecutor, + mCsdWarningDialogFactory, mDumpManager ); mDialog.init(0, null); @@ -352,6 +362,14 @@ public class VolumeDialogImplTest extends SysuiTestCase { mDialog.getDialogView().animate().cancel(); } + @Test + public void showCsdWarning_dialogShown() { + mDialog.showCsdWarningH(AudioManager.CSD_WARNING_DOSE_REACHED_1X, + CsdWarningDialog.NO_ACTION_TIMEOUT_MS); + + verify(mCsdWarningDialog).show(); + } + /* @Test public void testContentDescriptions() { diff --git a/proto/src/system_messages.proto b/proto/src/system_messages.proto index 47027342974db..21d09792f1c74 100644 --- a/proto/src/system_messages.proto +++ b/proto/src/system_messages.proto @@ -402,5 +402,7 @@ message SystemMessage { // Package: android NOTE_ALL_MANAGED_SUBSCRIPTIONS_AND_MANAGED_PROFILE_OFF = 1006; + // Notify the user that audio was lowered based on Calculated Sound Dose (CSD) + NOTE_CSD_LOWER_AUDIO = 1007; } }