Merge "Sound Dose Notification" into udc-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
886d308fee
24
packages/SystemUI/res/drawable/hearing.xml
Normal file
24
packages/SystemUI/res/drawable/hearing.xml
Normal file
@@ -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>
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user