Hello in the year 2022!
In my word game app the approach is not to touch the AudioManager at all!
That means I never call the code below:
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
The reason is that the user would be unpleasantly surprised, if she would try to change the volume of sounds in the app - but the overall volume for phone calls and music would be changed as well.
So I just save a factor value between 0.1 and 1.0 into the shared prefs and then multiply the overall phone volume by that factor, when playing any sounds in my game.
In the settings fragment of my app I have a Material slider:
<com.google.android.material.slider.Slider
android:id="@+id/volumeFactorSlider"
android:layout_weight="8"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:contentDescription="Volume"
android:valueFrom="0.1"
android:valueTo="1.0" />

@Override
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
View v = inflater.inflate(R.layout.fragment_settings, container, false);
Slider volumeFactorSlider = v.findViewById(R.id.volumeFactorSlider);
float volumeFactor = prefs.getFloat(KEY_VOLUME, 1.0f);
// clamp the value read from shared prefs to avoid IllegalStateException
volumeFactor = Math.max(volumeFactorSlider.getValueFrom(), volumeFactor);
volumeFactor = Math.min(volumeFactor, volumeFactorSlider.getValueTo());
volumeFactorSlider.setValue(volumeFactor);
volumeFactorSlider.addOnSliderTouchListener(new Slider.OnSliderTouchListener() {
@Override
public void onStartTrackingTouch(@NonNull Slider slider) {
// ignore
}
@Override
public void onStopTrackingTouch(@NonNull Slider slider) {
prefs.edit().putFloat(KEY_VOLUME, slider.getValue()).apply();
}
});
return v;
}
And then in the game fragment where the sounds are played I use:
private SoundPool mSoundPool;
private float mVolumeFactor;
private int mShuffleSound;
private int mBonusSound;
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
mVolumeFactor = prefs.getFloat(KEY_VOLUME, 1.0f);
mSoundPool = new SoundPool(4, AudioManager.STREAM_MUSIC, 0);
mShuffleSound = mSoundPool.load(requireContext(), R.raw.shuffle, 1);
mBonusSound = mSoundPool.load(requireContext(), R.raw.magic, 1);
....
}
@Override
public void onDestroy() {
super.onDestroy();
mSoundPool.release();
mSoundPool = null;
}
Finally, when playing a sound I just multiply by the factor:
private void playSound(int soundId) {
mSoundPool.play(soundId, mVolumeFactor, mVolumeFactor, 1, 0, 1f);
}
For example, when shuffling letter tiles: playSound(mShuffleSound);