Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 886d308f authored by Behnam Heydarshahi's avatar Behnam Heydarshahi Committed by Android (Google) Code Review
Browse files

Merge "Sound Dose Notification" into udc-dev

parents b7c6f01c f8938900
Loading
Loading
Loading
Loading
+24 −0
Original line number Diff line number Diff line
<!-- 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>
+89 −27
Original line number Diff line number Diff line
@@ -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() {
            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 quick enough
                    // 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());
    }
}
+12 −9
Original line number Diff line number Diff line
@@ -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() {

            final Runnable cleanUp = () -> {
                synchronized (mSafetyWarningLock) {
                    mCsdDialog = null;
                }
                recheckH(null);
                }
            };

            mCsdDialog = mCsdWarningDialogFactory.create(csdWarning, cleanUp);
            mCsdDialog.show();
        }
        recheckH(null);
+5 −2
Original line number Diff line number Diff line
@@ -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);
+91 −0
Original line number Diff line number Diff line
/*
 * 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));
    }
}
Loading