Merge "Sound Dose Notification" into udc-dev

This commit is contained in:
Behnam Heydarshahi
2023-04-11 13:54:57 +00:00
committed by Android (Google) Code Review
7 changed files with 241 additions and 38 deletions

View 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>

View File

@@ -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());
}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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));
}
}

View File

@@ -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() {

View File

@@ -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;
}
}