Skip to content

Commit c34dcc1

Browse files
author
Eric Laurent
committed
headphone volume limitation
Limit music volume when headphones or headset are inserted. Display warning message when user wants to increase the volume above a platform specific volume and request user acknowledgement before proceeding. TODO: exact wording of the warning message must be defined by UX. Change-Id: I00f429f602534c6d8783126b929371c4d432e6e2
1 parent b45965f commit c34dcc1

File tree

5 files changed

+239
-1
lines changed

5 files changed

+239
-1
lines changed

core/java/android/view/VolumePanel.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import com.android.internal.R;
2020

21+
import android.app.AlertDialog;
2122
import android.app.Dialog;
2223
import android.content.DialogInterface.OnDismissListener;
2324
import android.content.BroadcastReceiver;
@@ -92,6 +93,7 @@ public class VolumePanel extends Handler implements OnSeekBarChangeListener, Vie
9293
private static final int MSG_REMOTE_VOLUME_CHANGED = 8;
9394
private static final int MSG_REMOTE_VOLUME_UPDATE_IF_SHOWN = 9;
9495
private static final int MSG_SLIDER_VISIBILITY_CHANGED = 10;
96+
private static final int MSG_DISPLAY_SAFE_VOLUME_WARNING = 11;
9597

9698
// Pseudo stream type for master volume
9799
private static final int STREAM_MASTER = -100;
@@ -211,6 +213,31 @@ private class StreamControl {
211213
private ToneGenerator mToneGenerators[];
212214
private Vibrator mVibrator;
213215

216+
private static AlertDialog sConfirmSafeVolumeDialog;
217+
218+
private static class WarningDialogReceiver extends BroadcastReceiver
219+
implements DialogInterface.OnDismissListener {
220+
private Context mContext;
221+
private Dialog mDialog;
222+
223+
WarningDialogReceiver(Context context, Dialog dialog) {
224+
mContext = context;
225+
mDialog = dialog;
226+
IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
227+
context.registerReceiver(this, filter);
228+
}
229+
230+
@Override
231+
public void onReceive(Context context, Intent intent) {
232+
mDialog.cancel();
233+
}
234+
235+
public void onDismiss(DialogInterface unused) {
236+
mContext.unregisterReceiver(this);
237+
}
238+
}
239+
240+
214241
public VolumePanel(final Context context, AudioService volumeService) {
215242
mContext = context;
216243
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
@@ -528,6 +555,10 @@ public void postMasterMuteChanged(int flags) {
528555
postMuteChanged(STREAM_MASTER, flags);
529556
}
530557

558+
public void postDisplaySafeVolumeWarning() {
559+
obtainMessage(MSG_DISPLAY_SAFE_VOLUME_WARNING, 0, 0).sendToTarget();
560+
}
561+
531562
/**
532563
* Override this if you have other work to do when the volume changes (for
533564
* example, vibrating, playing a sound, etc.). Make sure to call through to
@@ -796,6 +827,32 @@ synchronized protected void onSliderVisibilityChanged(int streamType, int visibl
796827
}
797828
}
798829

830+
protected void onDisplaySafeVolumeWarning() {
831+
if (sConfirmSafeVolumeDialog != null) {
832+
sConfirmSafeVolumeDialog.dismiss();
833+
}
834+
sConfirmSafeVolumeDialog = new AlertDialog.Builder(mContext)
835+
.setTitle(android.R.string.dialog_alert_title)
836+
.setMessage(com.android.internal.R.string.safe_media_volume_warning)
837+
.setPositiveButton(com.android.internal.R.string.yes,
838+
new DialogInterface.OnClickListener() {
839+
public void onClick(DialogInterface dialog, int which) {
840+
mAudioService.disableSafeMediaVolume();
841+
}
842+
})
843+
.setNegativeButton(com.android.internal.R.string.no, null)
844+
.setIconAttribute(android.R.attr.alertDialogIcon)
845+
.create();
846+
847+
final WarningDialogReceiver warning = new WarningDialogReceiver(mContext,
848+
sConfirmSafeVolumeDialog);
849+
850+
sConfirmSafeVolumeDialog.setOnDismissListener(warning);
851+
sConfirmSafeVolumeDialog.getWindow().setType(
852+
WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
853+
sConfirmSafeVolumeDialog.show();
854+
}
855+
799856
/**
800857
* Lock on this VolumePanel instance as long as you use the returned ToneGenerator.
801858
*/
@@ -910,6 +967,10 @@ public void handleMessage(Message msg) {
910967
case MSG_SLIDER_VISIBILITY_CHANGED:
911968
onSliderVisibilityChanged(msg.arg1, msg.arg2);
912969
break;
970+
971+
case MSG_DISPLAY_SAFE_VOLUME_WARNING:
972+
onDisplaySafeVolumeWarning();
973+
break;
913974
}
914975
}
915976

core/res/res/values/config.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -931,4 +931,10 @@
931931
Devices with screens that deviate too far from their assigned density
932932
bucket should consider tuning this value in a device-specific overlay. -->
933933
<dimen name="config_minScalingSpan">25mm</dimen>
934+
935+
<!-- Safe headphone volume index. When music stream volume is below this index
936+
the SPL on headphone output is compliant to EN 60950 requirements for portable music
937+
players. -->
938+
<integer name="config_safe_media_volume_index">10</integer>
939+
934940
</resources>

core/res/res/values/strings.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3880,6 +3880,12 @@
38803880
Try again in <xliff:g id="number">%d</xliff:g> seconds.
38813881
</string>
38823882

3883+
<!-- Message shown in dialog when user is attempting to set the music volume above the
3884+
recommended maximum level for headphones -->
3885+
<string name="safe_media_volume_warning" product="default">
3886+
"Raise volume above the recommended level?"
3887+
</string>
3888+
38833889
<string name="kg_temp_back_string"> &lt; </string> <!-- TODO: remove this -->
38843890

38853891
</resources>

core/res/res/values/symbols.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@
279279
<java-symbol type="integer" name="config_soundEffectVolumeDb" />
280280
<java-symbol type="integer" name="config_lockSoundVolumeDb" />
281281
<java-symbol type="integer" name="config_multiuserMaximumUsers" />
282+
<java-symbol type="integer" name="config_safe_media_volume_index" />
282283

283284
<java-symbol type="color" name="tab_indicator_text_v4" />
284285

@@ -810,6 +811,7 @@
810811
<java-symbol type="string" name="default_audio_route_name_dock_speakers" />
811812
<java-symbol type="string" name="default_audio_route_name_hdmi" />
812813
<java-symbol type="string" name="default_audio_route_category_name" />
814+
<java-symbol type="string" name="safe_media_volume_warning" />
813815

814816
<java-symbol type="plurals" name="abbrev_in_num_days" />
815817
<java-symbol type="plurals" name="abbrev_in_num_hours" />

media/java/android/media/AudioService.java

Lines changed: 164 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ public class AudioService extends IAudioService.Stub implements OnFinished {
153153
// end of messages handled under wakelock
154154
private static final int MSG_SET_RSX_CONNECTION_STATE = 23; // change remote submix connection
155155
private static final int MSG_SET_FORCE_RSX_USE = 24; // force remote submix audio routing
156+
private static final int MSG_CHECK_MUSIC_ACTIVE = 25;
156157

157158
// flags for MSG_PERSIST_VOLUME indicating if current and/or last audible volume should be
158159
// persisted
@@ -430,6 +431,8 @@ public AudioService(Context context) {
430431
mContentResolver = context.getContentResolver();
431432
mVoiceCapable = mContext.getResources().getBoolean(
432433
com.android.internal.R.bool.config_voice_capable);
434+
mSafeMediaVolumeIndex = mContext.getResources().getInteger(
435+
com.android.internal.R.integer.config_safe_media_volume_index) * 10;
433436

434437
PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
435438
mMediaEventWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "handleMediaEvent");
@@ -454,6 +457,10 @@ public AudioService(Context context) {
454457
updateStreamVolumeAlias(false /*updateVolumes*/);
455458
createStreamStates();
456459

460+
synchronized (mSafeMediaVolumeEnabled) {
461+
enforceSafeMediaVolume();
462+
}
463+
457464
mMediaServerOk = true;
458465

459466
// Call setRingerModeInt() to apply correct mute
@@ -738,6 +745,11 @@ public void adjustStreamVolume(int streamType, int direction, int flags) {
738745
// convert one UI step (+/-1) into a number of internal units on the stream alias
739746
int step = rescaleIndex(10, streamType, streamTypeAlias);
740747

748+
if ((direction == AudioManager.ADJUST_RAISE) &&
749+
!checkSafeMediaVolume(streamTypeAlias, aliasIndex + step, device)) {
750+
return;
751+
}
752+
741753
// If either the client forces allowing ringer modes for this adjustment,
742754
// or the stream type is one that is affected by ringer modes
743755
if (((flags & AudioManager.FLAG_ALLOW_RINGER_MODES) != 0) ||
@@ -815,12 +827,17 @@ public void setStreamVolume(int streamType, int index, int flags) {
815827
VolumeStreamState streamState = mStreamStates[mStreamVolumeAlias[streamType]];
816828

817829
final int device = getDeviceForStream(streamType);
830+
818831
// get last audible index if stream is muted, current index otherwise
819832
final int oldIndex = streamState.getIndex(device,
820833
(streamState.muteCount() != 0) /* lastAudible */);
821834

822835
index = rescaleIndex(index * 10, streamType, mStreamVolumeAlias[streamType]);
823836

837+
if (!checkSafeMediaVolume(mStreamVolumeAlias[streamType], index, device)) {
838+
return;
839+
}
840+
824841
// setting volume on master stream type also controls silent mode
825842
if (((flags & AudioManager.FLAG_ALLOW_RINGER_MODES) != 0) ||
826843
(mStreamVolumeAlias[streamType] == getMasterStreamType())) {
@@ -1681,6 +1698,10 @@ public void reloadAudioSettings() {
16811698

16821699
checkAllAliasStreamVolumes();
16831700

1701+
synchronized (mSafeMediaVolumeEnabled) {
1702+
enforceSafeMediaVolume();
1703+
}
1704+
16841705
// apply new ringer mode
16851706
setRingerModeInt(getRingerMode(), false);
16861707
}
@@ -2138,6 +2159,33 @@ private void onSetRsxConnectionState(int available, int address) {
21382159
String.valueOf(address) /*device_address*/);
21392160
}
21402161

2162+
private void onCheckMusicActive() {
2163+
synchronized (mSafeMediaVolumeEnabled) {
2164+
if (!mSafeMediaVolumeEnabled) {
2165+
int device = getDeviceForStream(AudioSystem.STREAM_MUSIC);
2166+
2167+
if ((device & mSafeMediaVolumeDevices) != 0) {
2168+
sendMsg(mAudioHandler,
2169+
MSG_CHECK_MUSIC_ACTIVE,
2170+
SENDMSG_REPLACE,
2171+
device,
2172+
0,
2173+
null,
2174+
MUSIC_ACTIVE_POLL_PERIOD_MS);
2175+
if (AudioSystem.isStreamActive(AudioSystem.STREAM_MUSIC, 0)) {
2176+
// Approximate cumulative active music time
2177+
mMusicActiveMs += MUSIC_ACTIVE_POLL_PERIOD_MS;
2178+
if (mMusicActiveMs > UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX) {
2179+
setSafeMediaVolumeEnabled(true);
2180+
mMusicActiveMs = 0;
2181+
mVolumePanel.postDisplaySafeVolumeWarning();
2182+
}
2183+
}
2184+
}
2185+
}
2186+
}
2187+
}
2188+
21412189
///////////////////////////////////////////////////////////////////////////
21422190
// Internal methods
21432191
///////////////////////////////////////////////////////////////////////////
@@ -2397,6 +2445,14 @@ private int getDeviceForStream(int stream) {
23972445
public void setWiredDeviceConnectionState(int device, int state, String name) {
23982446
synchronized (mConnectedDevices) {
23992447
int delay = checkSendBecomingNoisyIntent(device, state);
2448+
if ((device & mSafeMediaVolumeDevices) != 0) {
2449+
setSafeMediaVolumeEnabled(state != 0);
2450+
// insert delay to allow new volume to apply before switching to headphones
2451+
if ((delay < SAFE_VOLUME_DELAY_MS) && (state != 0)) {
2452+
delay = SAFE_VOLUME_DELAY_MS;
2453+
}
2454+
}
2455+
24002456
queueMsgUnderWakeLock(mAudioHandler,
24012457
MSG_SET_WIRED_DEVICE_CONNECTION_STATE,
24022458
device,
@@ -3168,6 +3224,10 @@ public void handleMessage(Message msg) {
31683224
case MSG_SET_RSX_CONNECTION_STATE:
31693225
onSetRsxConnectionState(msg.arg1/*available*/, msg.arg2/*address*/);
31703226
break;
3227+
3228+
case MSG_CHECK_MUSIC_ACTIVE:
3229+
onCheckMusicActive();
3230+
break;
31713231
}
31723232
}
31733233
}
@@ -4426,7 +4486,7 @@ private void dumpRCCStack(PrintWriter pw) {
44264486
" -- vol: " + rcse.mPlaybackVolume +
44274487
" -- volMax: " + rcse.mPlaybackVolumeMax +
44284488
" -- volObs: " + rcse.mRemoteVolumeObs);
4429-
4489+
44304490
}
44314491
}
44324492
synchronized (mMainRemote) {
@@ -5415,6 +5475,109 @@ public AudioRoutesInfo startWatchingRoutes(IAudioRoutesObserver observer) {
54155475
}
54165476
}
54175477

5478+
5479+
//==========================================================================================
5480+
// Safe media volume management.
5481+
// MUSIC stream volume level is limited when headphones are connected according to safety
5482+
// regulation. When the user attempts to raise the volume above the limit, a warning is
5483+
// displayed and the user has to acknowlegde before the volume is actually changed.
5484+
// The volume index corresponding to the limit is stored in config_safe_media_volume_index
5485+
// property. Platforms with a different limit must set this property accordingly in their
5486+
// overlay.
5487+
//==========================================================================================
5488+
5489+
// mSafeMediaVolumeEnabled indicates whether the media volume is limited over headphones.
5490+
// It is true by default when headphones or a headset are inserted and can be overriden by
5491+
// calling AudioService.disableSafeMediaVolume() (when user opts out).
5492+
private Boolean mSafeMediaVolumeEnabled = new Boolean(false);
5493+
// mSafeMediaVolumeIndex is the cached value of config_safe_media_volume_index property
5494+
private final int mSafeMediaVolumeIndex;
5495+
// mSafeMediaVolumeDevices lists the devices for which safe media volume is enforced,
5496+
private final int mSafeMediaVolumeDevices = AudioSystem.DEVICE_OUT_WIRED_HEADSET |
5497+
AudioSystem.DEVICE_OUT_WIRED_HEADPHONE;
5498+
// mMusicActiveMs is the cumulative time of music activity since safe volume was disabled.
5499+
// When this time reaches UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX, the safe media volume is re-enabled
5500+
// automatically. mMusicActiveMs is rounded to a multiple of MUSIC_ACTIVE_POLL_PERIOD_MS.
5501+
private int mMusicActiveMs;
5502+
private static final int UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX = (20 * 3600 * 1000); // 20 hours
5503+
private static final int MUSIC_ACTIVE_POLL_PERIOD_MS = 60000; // 1 minute polling interval
5504+
private static final int SAFE_VOLUME_DELAY_MS = 500; // 500ms before switching to headphones
5505+
5506+
private void setSafeMediaVolumeEnabled(boolean on) {
5507+
synchronized (mSafeMediaVolumeEnabled) {
5508+
if (on && !mSafeMediaVolumeEnabled) {
5509+
enforceSafeMediaVolume();
5510+
} else if (!on && mSafeMediaVolumeEnabled) {
5511+
mMusicActiveMs = 0;
5512+
sendMsg(mAudioHandler,
5513+
MSG_CHECK_MUSIC_ACTIVE,
5514+
SENDMSG_REPLACE,
5515+
0,
5516+
0,
5517+
null,
5518+
MUSIC_ACTIVE_POLL_PERIOD_MS);
5519+
}
5520+
mSafeMediaVolumeEnabled = on;
5521+
}
5522+
}
5523+
5524+
private void enforceSafeMediaVolume() {
5525+
VolumeStreamState streamState = mStreamStates[AudioSystem.STREAM_MUSIC];
5526+
boolean lastAudible = (streamState.muteCount() != 0);
5527+
int devices = mSafeMediaVolumeDevices;
5528+
int i = 0;
5529+
5530+
while (devices != 0) {
5531+
int device = 1 << i++;
5532+
if ((device & devices) == 0) {
5533+
continue;
5534+
}
5535+
int index = streamState.getIndex(device, lastAudible);
5536+
if (index > mSafeMediaVolumeIndex) {
5537+
if (lastAudible) {
5538+
streamState.setLastAudibleIndex(mSafeMediaVolumeIndex, device);
5539+
sendMsg(mAudioHandler,
5540+
MSG_PERSIST_VOLUME,
5541+
SENDMSG_QUEUE,
5542+
PERSIST_LAST_AUDIBLE,
5543+
device,
5544+
streamState,
5545+
PERSIST_DELAY);
5546+
} else {
5547+
streamState.setIndex(mSafeMediaVolumeIndex, device, true);
5548+
sendMsg(mAudioHandler,
5549+
MSG_SET_DEVICE_VOLUME,
5550+
SENDMSG_QUEUE,
5551+
device,
5552+
0,
5553+
streamState,
5554+
0);
5555+
}
5556+
}
5557+
devices &= ~device;
5558+
}
5559+
}
5560+
5561+
private boolean checkSafeMediaVolume(int streamType, int index, int device) {
5562+
synchronized (mSafeMediaVolumeEnabled) {
5563+
if (mSafeMediaVolumeEnabled &&
5564+
(mStreamVolumeAlias[streamType] == AudioSystem.STREAM_MUSIC) &&
5565+
((device & mSafeMediaVolumeDevices) != 0) &&
5566+
(index > mSafeMediaVolumeIndex)) {
5567+
mVolumePanel.postDisplaySafeVolumeWarning();
5568+
return false;
5569+
}
5570+
return true;
5571+
}
5572+
}
5573+
5574+
public void disableSafeMediaVolume() {
5575+
synchronized (mSafeMediaVolumeEnabled) {
5576+
setSafeMediaVolumeEnabled(false);
5577+
}
5578+
}
5579+
5580+
54185581
@Override
54195582
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
54205583
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG);

0 commit comments

Comments
 (0)