summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Behnam Heydarshahi <bhnm@google.com> 2023-03-20 21:08:23 +0000
committer Behnam Heydarshahi <bhnm@google.com> 2023-04-10 16:06:24 +0000
commitf893890093a1ead4645f2fd487696d0fc718521e (patch)
tree61d8c00c0e3a06b192371cd90f24399c0fb42eab
parent839d062102d453e12984e35353e9fb551d68ba6f (diff)
Sound Dose Notification
Send a notification when SoundDose dialog is ignored by user to inform them that the audio was lowered by the system. CsdWarningDialog now uses DelayedExecutor instead of CountDownTimer. Implement tests for CsdWarningDialog and its client. Bug: b/267464531 Test: atest VolumeDialogImplTest CsdWarningDialogTest Change-Id: I9c0ad6b410e0eb009b6a1ea032c7f618a4f4791a
-rw-r--r--packages/SystemUI/res/drawable/hearing.xml24
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/CsdWarningDialog.java116
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java21
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java7
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/volume/CsdWarningDialogTest.java91
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java18
-rw-r--r--proto/src/system_messages.proto2
7 files changed, 241 insertions, 38 deletions
diff --git a/packages/SystemUI/res/drawable/hearing.xml b/packages/SystemUI/res/drawable/hearing.xml
new file mode 100644
index 000000000000..02f5f92ec5fe
--- /dev/null
+++ b/packages/SystemUI/res/drawable/hearing.xml
@@ -0,0 +1,24 @@
+<!-- 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48"
+ android:viewportHeight="48"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M34.4,43.95Q31.55,43.95 29.45,42.4Q27.35,40.85 26.35,38.3Q25.35,35.75 24.375,34.325Q23.4,32.9 20.7,30.75Q17.4,28.1 15.95,25.1Q14.5,22.1 14.5,17.8Q14.5,11.8 18.3,7.975Q22.1,4.15 28.1,4.15Q34,4.15 37.875,7.825Q41.75,11.5 42,17.2H39Q38.75,12.8 35.725,9.975Q32.7,7.15 28.1,7.15Q23.6,7.15 20.55,10.225Q17.5,13.3 17.5,17.8Q17.5,21.4 18.9,24.025Q20.3,26.65 23.55,29.1Q25.5,30.55 26.675,32.25Q27.85,33.95 28.9,36.45Q29.75,38.55 31.125,39.75Q32.5,40.95 34.4,40.95Q36.15,40.95 37.425,39.75Q38.7,38.55 38.95,36.8H41.95Q41.7,39.8 39.55,41.875Q37.4,43.95 34.4,43.95ZM11.95,32.9Q9.1,29.75 7.55,25.825Q6,21.9 6,17.6Q6,13.35 7.475,9.375Q8.95,5.4 11.95,2.35L14.2,4.35Q11.6,7 10.3,10.425Q9,13.85 9,17.6Q9,21.3 10.325,24.725Q11.65,28.15 14.2,30.85ZM28.1,22.45Q26.15,22.45 24.8,21.1Q23.45,19.75 23.45,17.8Q23.45,15.85 24.8,14.45Q26.15,13.05 28.1,13.05Q30.05,13.05 31.45,14.45Q32.85,15.85 32.85,17.8Q32.85,19.75 31.45,21.1Q30.05,22.45 28.1,22.45Z"/>
+</vector>
diff --git a/packages/SystemUI/src/com/android/systemui/volume/CsdWarningDialog.java b/packages/SystemUI/src/com/android/systemui/volume/CsdWarningDialog.java
index 35af44442ca9..e3ed2b405fb0 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 95cc12a48bb2..3c007f99a654 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 0ab6c690e1e1..14d3ca334073 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 000000000000..9cf3e443320d
--- /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 d419095921b8..eb2688894cb0 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 47027342974d..21d09792f1c7 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;
}
}