diff options
| author | 2019-07-11 20:15:29 +0000 | |
|---|---|---|
| committer | 2019-07-11 20:15:29 +0000 | |
| commit | fb64399ec3d0421e4b65e67b7ca35816544f44a1 (patch) | |
| tree | 3c20c1d864c579ef100d87901881e2a455ad423e | |
| parent | 3f8cdda0497f8dc4fbba56892777327f9bee9273 (diff) | |
| parent | 251c595896e9a27c3fb70a61541c479f435442e0 (diff) | |
Merge changes Ie5a1684a,I30a7d897
* changes:
Refactor SoundEffectsHelper for asynchronous loading
AudioService: factor out sound effects handling
| -rw-r--r-- | services/core/java/com/android/server/audio/AudioService.java | 468 | ||||
| -rw-r--r-- | services/core/java/com/android/server/audio/SoundEffectsHelper.java | 521 |
2 files changed, 559 insertions, 430 deletions
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 89e97d2419d8..6c57be8bbadf 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -54,8 +54,6 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.UserInfo; import android.content.res.Configuration; -import android.content.res.Resources; -import android.content.res.XmlResourceParser; import android.database.ContentObserver; import android.hardware.hdmi.HdmiAudioSystemClient; import android.hardware.hdmi.HdmiControlManager; @@ -82,11 +80,7 @@ import android.media.IRingtonePlayer; import android.media.IVolumeController; import android.media.MediaExtractor; import android.media.MediaFormat; -import android.media.MediaPlayer; -import android.media.MediaPlayer.OnCompletionListener; -import android.media.MediaPlayer.OnErrorListener; import android.media.PlayerBase; -import android.media.SoundPool; import android.media.VolumePolicy; import android.media.audiofx.AudioEffect; import android.media.audiopolicy.AudioMix; @@ -102,7 +96,6 @@ import android.net.Uri; import android.os.Binder; import android.os.Build; import android.os.Bundle; -import android.os.Environment; import android.os.Handler; import android.os.IBinder; import android.os.Looper; @@ -137,7 +130,6 @@ import android.widget.Toast; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.DumpUtils; import com.android.internal.util.Preconditions; -import com.android.internal.util.XmlUtils; import com.android.server.EventLogTags; import com.android.server.LocalServices; import com.android.server.SystemService; @@ -146,15 +138,11 @@ import com.android.server.audio.AudioServiceEvents.VolumeEvent; import com.android.server.pm.UserManagerService; import com.android.server.wm.ActivityTaskManagerInternal; -import org.xmlpull.v1.XmlPullParserException; - -import java.io.File; import java.io.FileDescriptor; import java.io.IOException; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -294,19 +282,6 @@ public class AudioService extends IAudioService.Stub // protects mRingerMode private final Object mSettingsLock = new Object(); - private SoundPool mSoundPool; - private final Object mSoundEffectsLock = new Object(); - private static final int NUM_SOUNDPOOL_CHANNELS = 4; - - /* Sound effect file names */ - private static final String SOUND_EFFECTS_PATH = "/media/audio/ui/"; - private static final List<String> SOUND_EFFECT_FILES = new ArrayList<String>(); - - /* Sound effect file name mapping sound effect id (AudioManager.FX_xxx) to - * file index in SOUND_EFFECT_FILES[] (first column) and indicating if effect - * uses soundpool (second column) */ - private final int[][] SOUND_EFFECT_FILES_MAP = new int[AudioManager.NUM_SOUND_EFFECTS][2]; - /** Maximum volume index values for audio streams */ protected static int[] MAX_STREAM_VOLUME = new int[] { 5, // STREAM_VOICE_CALL @@ -453,6 +428,9 @@ public class AudioService extends IAudioService.Stub * @see System#MUTE_STREAMS_AFFECTED */ private int mMuteAffectedStreams; + @NonNull + private SoundEffectsHelper mSfxHelper; + /** * NOTE: setVibrateSetting(), getVibrateSetting(), shouldVibrate() are deprecated. * mVibrateSetting is just maintained during deprecation period but vibration policy is @@ -493,14 +471,6 @@ public class AudioService extends IAudioService.Stub private boolean mSystemReady; // true if Intent.ACTION_USER_SWITCHED has ever been received private boolean mUserSwitchedReceived; - // listener for SoundPool sample load completion indication - private SoundPoolCallback mSoundPoolCallBack; - // thread for SoundPool listener - private SoundPoolListenerThread mSoundPoolListenerThread; - // message looper for SoundPool listener - private Looper mSoundPoolLooper = null; - // volume applied to sound played with playSoundEffect() - private static int sSoundEffectVolumeDb; // previous volume adjustment direction received by checkForRingerModeChange() private int mPrevVolDirection = AudioManager.ADJUST_SAME; // mVolumeControlStream is set by VolumePanel to temporarily force the stream type which volume @@ -642,6 +612,8 @@ public class AudioService extends IAudioService.Stub PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE); mAudioEventWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "handleAudioEvent"); + mSfxHelper = new SoundEffectsHelper(mContext); + mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); mHasVibrator = mVibrator == null ? false : mVibrator.hasVibrator(); @@ -732,9 +704,6 @@ public class AudioService extends IAudioService.Stub MAX_STREAM_VOLUME[AudioSystem.STREAM_SYSTEM]; } - sSoundEffectVolumeDb = context.getResources().getInteger( - com.android.internal.R.integer.config_soundEffectVolumeDb); - createAudioSystemThread(); AudioSystem.setErrorCallback(mAudioSystemCallback); @@ -3369,104 +3338,30 @@ public class AudioService extends IAudioService.Stub //========================================================================================== // Sound Effects //========================================================================================== + private static final class LoadSoundEffectReply + implements SoundEffectsHelper.OnEffectsLoadCompleteHandler { + private static final int SOUND_EFFECTS_LOADING = 1; + private static final int SOUND_EFFECTS_LOADED = 0; + private static final int SOUND_EFFECTS_ERROR = -1; + private static final int SOUND_EFFECTS_LOAD_TIMEOUT_MS = 5000; - private static final String TAG_AUDIO_ASSETS = "audio_assets"; - private static final String ATTR_VERSION = "version"; - private static final String TAG_GROUP = "group"; - private static final String ATTR_GROUP_NAME = "name"; - private static final String TAG_ASSET = "asset"; - private static final String ATTR_ASSET_ID = "id"; - private static final String ATTR_ASSET_FILE = "file"; - - private static final String ASSET_FILE_VERSION = "1.0"; - private static final String GROUP_TOUCH_SOUNDS = "touch_sounds"; - - private static final int SOUND_EFFECTS_LOAD_TIMEOUT_MS = 5000; + private int mStatus = SOUND_EFFECTS_LOADING; - class LoadSoundEffectReply { - public int mStatus = 1; - }; - - private void loadTouchSoundAssetDefaults() { - SOUND_EFFECT_FILES.add("Effect_Tick.ogg"); - for (int i = 0; i < AudioManager.NUM_SOUND_EFFECTS; i++) { - SOUND_EFFECT_FILES_MAP[i][0] = 0; - SOUND_EFFECT_FILES_MAP[i][1] = -1; - } - } - - private void loadTouchSoundAssets() { - XmlResourceParser parser = null; - - // only load assets once. - if (!SOUND_EFFECT_FILES.isEmpty()) { - return; + @Override + public synchronized void run(boolean success) { + mStatus = success ? SOUND_EFFECTS_LOADED : SOUND_EFFECTS_ERROR; + notify(); } - loadTouchSoundAssetDefaults(); - - try { - parser = mContext.getResources().getXml(com.android.internal.R.xml.audio_assets); - - XmlUtils.beginDocument(parser, TAG_AUDIO_ASSETS); - String version = parser.getAttributeValue(null, ATTR_VERSION); - boolean inTouchSoundsGroup = false; - - if (ASSET_FILE_VERSION.equals(version)) { - while (true) { - XmlUtils.nextElement(parser); - String element = parser.getName(); - if (element == null) { - break; - } - if (element.equals(TAG_GROUP)) { - String name = parser.getAttributeValue(null, ATTR_GROUP_NAME); - if (GROUP_TOUCH_SOUNDS.equals(name)) { - inTouchSoundsGroup = true; - break; - } - } - } - while (inTouchSoundsGroup) { - XmlUtils.nextElement(parser); - String element = parser.getName(); - if (element == null) { - break; - } - if (element.equals(TAG_ASSET)) { - String id = parser.getAttributeValue(null, ATTR_ASSET_ID); - String file = parser.getAttributeValue(null, ATTR_ASSET_FILE); - int fx; - - try { - Field field = AudioManager.class.getField(id); - fx = field.getInt(null); - } catch (Exception e) { - Log.w(TAG, "Invalid touch sound ID: "+id); - continue; - } - - int i = SOUND_EFFECT_FILES.indexOf(file); - if (i == -1) { - i = SOUND_EFFECT_FILES.size(); - SOUND_EFFECT_FILES.add(file); - } - SOUND_EFFECT_FILES_MAP[fx][0] = i; - } else { - break; - } + public synchronized boolean waitForLoaded(int attempts) { + while ((mStatus == SOUND_EFFECTS_LOADING) && (attempts-- > 0)) { + try { + wait(SOUND_EFFECTS_LOAD_TIMEOUT_MS); + } catch (InterruptedException e) { + Log.w(TAG, "Interrupted while waiting sound pool loaded."); } } - } catch (Resources.NotFoundException e) { - Log.w(TAG, "audio assets file not found", e); - } catch (XmlPullParserException e) { - Log.w(TAG, "XML parser exception reading touch sound assets", e); - } catch (IOException e) { - Log.w(TAG, "I/O exception reading touch sound assets", e); - } finally { - if (parser != null) { - parser.close(); - } + return mStatus == SOUND_EFFECTS_LOADED; } } @@ -3496,20 +3391,9 @@ public class AudioService extends IAudioService.Stub * This method must be called at first when sound effects are enabled */ public boolean loadSoundEffects() { - int attempts = 3; LoadSoundEffectReply reply = new LoadSoundEffectReply(); - - synchronized (reply) { - sendMsg(mAudioHandler, MSG_LOAD_SOUND_EFFECTS, SENDMSG_QUEUE, 0, 0, reply, 0); - while ((reply.mStatus == 1) && (attempts-- > 0)) { - try { - reply.wait(SOUND_EFFECTS_LOAD_TIMEOUT_MS); - } catch (InterruptedException e) { - Log.w(TAG, "loadSoundEffects Interrupted while waiting sound pool loaded."); - } - } - } - return (reply.mStatus == 0); + sendMsg(mAudioHandler, MSG_LOAD_SOUND_EFFECTS, SENDMSG_QUEUE, 0, 0, reply, 0); + return reply.waitForLoaded(3 /*attempts*/); } /** @@ -3529,61 +3413,6 @@ public class AudioService extends IAudioService.Stub sendMsg(mAudioHandler, MSG_UNLOAD_SOUND_EFFECTS, SENDMSG_QUEUE, 0, 0, null, 0); } - class SoundPoolListenerThread extends Thread { - public SoundPoolListenerThread() { - super("SoundPoolListenerThread"); - } - - @Override - public void run() { - - Looper.prepare(); - mSoundPoolLooper = Looper.myLooper(); - - synchronized (mSoundEffectsLock) { - if (mSoundPool != null) { - mSoundPoolCallBack = new SoundPoolCallback(); - mSoundPool.setOnLoadCompleteListener(mSoundPoolCallBack); - } - mSoundEffectsLock.notify(); - } - Looper.loop(); - } - } - - private final class SoundPoolCallback implements - android.media.SoundPool.OnLoadCompleteListener { - - int mStatus = 1; // 1 means neither error nor last sample loaded yet - List<Integer> mSamples = new ArrayList<Integer>(); - - public int status() { - return mStatus; - } - - public void setSamples(int[] samples) { - for (int i = 0; i < samples.length; i++) { - // do not wait ack for samples rejected upfront by SoundPool - if (samples[i] > 0) { - mSamples.add(samples[i]); - } - } - } - - public void onLoadComplete(SoundPool soundPool, int sampleId, int status) { - synchronized (mSoundEffectsLock) { - int i = mSamples.indexOf(sampleId); - if (i >= 0) { - mSamples.remove(i); - } - if ((status != 0) || mSamples. isEmpty()) { - mStatus = status; - mSoundEffectsLock.notify(); - } - } - } - } - /** @see AudioManager#reloadAudioSettings() */ public void reloadAudioSettings() { readAudioSettings(false /*userSwitch*/); @@ -5104,230 +4933,6 @@ public class AudioService extends IAudioService.Stub Settings.Global.putInt(mContentResolver, Settings.Global.MODE_RINGER, ringerMode); } - private String getSoundEffectFilePath(int effectType) { - String filePath = Environment.getProductDirectory() + SOUND_EFFECTS_PATH - + SOUND_EFFECT_FILES.get(SOUND_EFFECT_FILES_MAP[effectType][0]); - if (!new File(filePath).isFile()) { - filePath = Environment.getRootDirectory() + SOUND_EFFECTS_PATH - + SOUND_EFFECT_FILES.get(SOUND_EFFECT_FILES_MAP[effectType][0]); - } - return filePath; - } - - private boolean onLoadSoundEffects() { - int status; - - synchronized (mSoundEffectsLock) { - if (!mSystemReady) { - Log.w(TAG, "onLoadSoundEffects() called before boot complete"); - return false; - } - - if (mSoundPool != null) { - return true; - } - - loadTouchSoundAssets(); - - mSoundPool = new SoundPool.Builder() - .setMaxStreams(NUM_SOUNDPOOL_CHANNELS) - .setAudioAttributes(new AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .build()) - .build(); - mSoundPoolCallBack = null; - mSoundPoolListenerThread = new SoundPoolListenerThread(); - mSoundPoolListenerThread.start(); - int attempts = 3; - while ((mSoundPoolCallBack == null) && (attempts-- > 0)) { - try { - // Wait for mSoundPoolCallBack to be set by the other thread - mSoundEffectsLock.wait(SOUND_EFFECTS_LOAD_TIMEOUT_MS); - } catch (InterruptedException e) { - Log.w(TAG, "Interrupted while waiting sound pool listener thread."); - } - } - - if (mSoundPoolCallBack == null) { - Log.w(TAG, "onLoadSoundEffects() SoundPool listener or thread creation error"); - if (mSoundPoolLooper != null) { - mSoundPoolLooper.quit(); - mSoundPoolLooper = null; - } - mSoundPoolListenerThread = null; - mSoundPool.release(); - mSoundPool = null; - return false; - } - /* - * poolId table: The value -1 in this table indicates that corresponding - * file (same index in SOUND_EFFECT_FILES[] has not been loaded. - * Once loaded, the value in poolId is the sample ID and the same - * sample can be reused for another effect using the same file. - */ - int[] poolId = new int[SOUND_EFFECT_FILES.size()]; - for (int fileIdx = 0; fileIdx < SOUND_EFFECT_FILES.size(); fileIdx++) { - poolId[fileIdx] = -1; - } - /* - * Effects whose value in SOUND_EFFECT_FILES_MAP[effect][1] is -1 must be loaded. - * If load succeeds, value in SOUND_EFFECT_FILES_MAP[effect][1] is > 0: - * this indicates we have a valid sample loaded for this effect. - */ - - int numSamples = 0; - for (int effect = 0; effect < AudioManager.NUM_SOUND_EFFECTS; effect++) { - // Do not load sample if this effect uses the MediaPlayer - if (SOUND_EFFECT_FILES_MAP[effect][1] == 0) { - continue; - } - if (poolId[SOUND_EFFECT_FILES_MAP[effect][0]] == -1) { - String filePath = getSoundEffectFilePath(effect); - int sampleId = mSoundPool.load(filePath, 0); - if (sampleId <= 0) { - Log.w(TAG, "Soundpool could not load file: "+filePath); - } else { - SOUND_EFFECT_FILES_MAP[effect][1] = sampleId; - poolId[SOUND_EFFECT_FILES_MAP[effect][0]] = sampleId; - numSamples++; - } - } else { - SOUND_EFFECT_FILES_MAP[effect][1] = - poolId[SOUND_EFFECT_FILES_MAP[effect][0]]; - } - } - // wait for all samples to be loaded - if (numSamples > 0) { - mSoundPoolCallBack.setSamples(poolId); - - attempts = 3; - status = 1; - while ((status == 1) && (attempts-- > 0)) { - try { - mSoundEffectsLock.wait(SOUND_EFFECTS_LOAD_TIMEOUT_MS); - status = mSoundPoolCallBack.status(); - } catch (InterruptedException e) { - Log.w(TAG, "Interrupted while waiting sound pool callback."); - } - } - } else { - status = -1; - } - - if (mSoundPoolLooper != null) { - mSoundPoolLooper.quit(); - mSoundPoolLooper = null; - } - mSoundPoolListenerThread = null; - if (status != 0) { - Log.w(TAG, - "onLoadSoundEffects(), Error "+status+ " while loading samples"); - for (int effect = 0; effect < AudioManager.NUM_SOUND_EFFECTS; effect++) { - if (SOUND_EFFECT_FILES_MAP[effect][1] > 0) { - SOUND_EFFECT_FILES_MAP[effect][1] = -1; - } - } - - mSoundPool.release(); - mSoundPool = null; - } - } - return (status == 0); - } - - /** - * Unloads samples from the sound pool. - * This method can be called to free some memory when - * sound effects are disabled. - */ - private void onUnloadSoundEffects() { - synchronized (mSoundEffectsLock) { - if (mSoundPool == null) { - return; - } - - int[] poolId = new int[SOUND_EFFECT_FILES.size()]; - for (int fileIdx = 0; fileIdx < SOUND_EFFECT_FILES.size(); fileIdx++) { - poolId[fileIdx] = 0; - } - - for (int effect = 0; effect < AudioManager.NUM_SOUND_EFFECTS; effect++) { - if (SOUND_EFFECT_FILES_MAP[effect][1] <= 0) { - continue; - } - if (poolId[SOUND_EFFECT_FILES_MAP[effect][0]] == 0) { - mSoundPool.unload(SOUND_EFFECT_FILES_MAP[effect][1]); - SOUND_EFFECT_FILES_MAP[effect][1] = -1; - poolId[SOUND_EFFECT_FILES_MAP[effect][0]] = -1; - } - } - mSoundPool.release(); - mSoundPool = null; - } - } - - private void onPlaySoundEffect(int effectType, int volume) { - synchronized (mSoundEffectsLock) { - - onLoadSoundEffects(); - - if (mSoundPool == null) { - return; - } - float volFloat; - // use default if volume is not specified by caller - if (volume < 0) { - volFloat = (float)Math.pow(10, (float)sSoundEffectVolumeDb/20); - } else { - volFloat = volume / 1000.0f; - } - - if (SOUND_EFFECT_FILES_MAP[effectType][1] > 0) { - mSoundPool.play(SOUND_EFFECT_FILES_MAP[effectType][1], - volFloat, volFloat, 0, 0, 1.0f); - } else { - MediaPlayer mediaPlayer = new MediaPlayer(); - try { - String filePath = getSoundEffectFilePath(effectType); - mediaPlayer.setDataSource(filePath); - mediaPlayer.setAudioStreamType(AudioSystem.STREAM_SYSTEM); - mediaPlayer.prepare(); - mediaPlayer.setVolume(volFloat); - mediaPlayer.setOnCompletionListener(new OnCompletionListener() { - public void onCompletion(MediaPlayer mp) { - cleanupPlayer(mp); - } - }); - mediaPlayer.setOnErrorListener(new OnErrorListener() { - public boolean onError(MediaPlayer mp, int what, int extra) { - cleanupPlayer(mp); - return true; - } - }); - mediaPlayer.start(); - } catch (IOException ex) { - Log.w(TAG, "MediaPlayer IOException: "+ex); - } catch (IllegalArgumentException ex) { - Log.w(TAG, "MediaPlayer IllegalArgumentException: "+ex); - } catch (IllegalStateException ex) { - Log.w(TAG, "MediaPlayer IllegalStateException: "+ex); - } - } - } - } - - private void cleanupPlayer(MediaPlayer mp) { - if (mp != null) { - try { - mp.stop(); - mp.release(); - } catch (IllegalStateException ex) { - Log.w(TAG, "MediaPlayer IllegalStateException: "+ex); - } - } - } - private void onPersistSafeVolumeState(int state) { Settings.Global.putInt(mContentResolver, Settings.Global.AUDIO_SAFE_VOLUME_STATE, @@ -5374,24 +4979,25 @@ public class AudioService extends IAudioService.Stub break; case MSG_UNLOAD_SOUND_EFFECTS: - onUnloadSoundEffects(); + mSfxHelper.unloadSoundEffects(); break; case MSG_LOAD_SOUND_EFFECTS: - //FIXME: onLoadSoundEffects() should be executed in a separate thread as it - // can take several dozens of milliseconds to complete - boolean loaded = onLoadSoundEffects(); - if (msg.obj != null) { - LoadSoundEffectReply reply = (LoadSoundEffectReply)msg.obj; - synchronized (reply) { - reply.mStatus = loaded ? 0 : -1; - reply.notify(); + { + LoadSoundEffectReply reply = (LoadSoundEffectReply) msg.obj; + if (mSystemReady) { + mSfxHelper.loadSoundEffects(reply); + } else { + Log.w(TAG, "[schedule]loadSoundEffects() called before boot complete"); + if (reply != null) { + reply.run(false); } } + } break; case MSG_PLAY_SOUND_EFFECT: - onPlaySoundEffect(msg.arg1, msg.arg2); + mSfxHelper.playSoundEffect(msg.arg1, msg.arg2); break; case MSG_SET_FORCE_USE: @@ -6422,6 +6028,8 @@ public class AudioService extends IAudioService.Stub pw.println("\nAudioDeviceBroker:"); mDeviceBroker.dump(pw, " "); + pw.println("\nSoundEffects:"); + mSfxHelper.dump(pw, " "); pw.println("\n"); pw.println("\nEvent logs:"); diff --git a/services/core/java/com/android/server/audio/SoundEffectsHelper.java b/services/core/java/com/android/server/audio/SoundEffectsHelper.java new file mode 100644 index 000000000000..cf5bc8d88c73 --- /dev/null +++ b/services/core/java/com/android/server/audio/SoundEffectsHelper.java @@ -0,0 +1,521 @@ +/* + * Copyright (C) 2019 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.server.audio; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; +import android.media.AudioAttributes; +import android.media.AudioManager; +import android.media.AudioSystem; +import android.media.MediaPlayer; +import android.media.MediaPlayer.OnCompletionListener; +import android.media.MediaPlayer.OnErrorListener; +import android.media.SoundPool; +import android.os.Environment; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; +import android.util.PrintWriterPrinter; + +import com.android.internal.util.XmlUtils; + +import org.xmlpull.v1.XmlPullParserException; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +/** + * A helper class for managing sound effects loading / unloading + * used by AudioService. As its methods are called on the message handler thread + * of AudioService, the actual work is offloaded to a dedicated thread. + * This helps keeping AudioService responsive. + * @hide + */ +class SoundEffectsHelper { + private static final String TAG = "AS.SfxHelper"; + + private static final int NUM_SOUNDPOOL_CHANNELS = 4; + + /* Sound effect file names */ + private static final String SOUND_EFFECTS_PATH = "/media/audio/ui/"; + + private static final int EFFECT_NOT_IN_SOUND_POOL = 0; // SoundPool sample IDs > 0 + + private static final int MSG_LOAD_EFFECTS = 0; + private static final int MSG_UNLOAD_EFFECTS = 1; + private static final int MSG_PLAY_EFFECT = 2; + private static final int MSG_LOAD_EFFECTS_TIMEOUT = 3; + + interface OnEffectsLoadCompleteHandler { + void run(boolean success); + } + + private final AudioEventLogger mSfxLogger = new AudioEventLogger( + AudioManager.NUM_SOUND_EFFECTS + 10, "Sound Effects Loading"); + + private final Context mContext; + // default attenuation applied to sound played with playSoundEffect() + private final int mSfxAttenuationDb; + + // thread for doing all work + private SfxWorker mSfxWorker; + // thread's message handler + private SfxHandler mSfxHandler; + + private static final class Resource { + final String mFileName; + int mSampleId; + boolean mLoaded; // for effects in SoundPool + Resource(String fileName) { + mFileName = fileName; + mSampleId = EFFECT_NOT_IN_SOUND_POOL; + } + } + // All the fields below are accessed by the worker thread exclusively + private final List<Resource> mResources = new ArrayList<Resource>(); + private final int[] mEffects = new int[AudioManager.NUM_SOUND_EFFECTS]; // indexes in mResources + private SoundPool mSoundPool; + private SoundPoolLoader mSoundPoolLoader; + + SoundEffectsHelper(Context context) { + mContext = context; + mSfxAttenuationDb = mContext.getResources().getInteger( + com.android.internal.R.integer.config_soundEffectVolumeDb); + startWorker(); + } + + /*package*/ void loadSoundEffects(OnEffectsLoadCompleteHandler onComplete) { + sendMsg(MSG_LOAD_EFFECTS, 0, 0, onComplete, 0); + } + + /** + * Unloads samples from the sound pool. + * This method can be called to free some memory when + * sound effects are disabled. + */ + /*package*/ void unloadSoundEffects() { + sendMsg(MSG_UNLOAD_EFFECTS, 0, 0, null, 0); + } + + /*package*/ void playSoundEffect(int effect, int volume) { + sendMsg(MSG_PLAY_EFFECT, effect, volume, null, 0); + } + + /*package*/ void dump(PrintWriter pw, String prefix) { + if (mSfxHandler != null) { + pw.println(prefix + "Message handler (watch for unhandled messages):"); + mSfxHandler.dump(new PrintWriterPrinter(pw), " "); + } else { + pw.println(prefix + "Message handler is null"); + } + pw.println(prefix + "Default attenuation (dB): " + mSfxAttenuationDb); + mSfxLogger.dump(pw); + } + + private void startWorker() { + mSfxWorker = new SfxWorker(); + mSfxWorker.start(); + synchronized (this) { + while (mSfxHandler == null) { + try { + wait(); + } catch (InterruptedException e) { + Log.w(TAG, "Interrupted while waiting " + mSfxWorker.getName() + " to start"); + } + } + } + } + + private void sendMsg(int msg, int arg1, int arg2, Object obj, int delayMs) { + mSfxHandler.sendMessageDelayed(mSfxHandler.obtainMessage(msg, arg1, arg2, obj), delayMs); + } + + private void logEvent(String msg) { + mSfxLogger.log(new AudioEventLogger.StringEvent(msg)); + } + + // All the methods below run on the worker thread + private void onLoadSoundEffects(OnEffectsLoadCompleteHandler onComplete) { + if (mSoundPoolLoader != null) { + // Loading is ongoing. + mSoundPoolLoader.addHandler(onComplete); + return; + } + if (mSoundPool != null) { + if (onComplete != null) { + onComplete.run(true /*success*/); + } + return; + } + + logEvent("effects loading started"); + mSoundPool = new SoundPool.Builder() + .setMaxStreams(NUM_SOUNDPOOL_CHANNELS) + .setAudioAttributes(new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .build()) + .build(); + loadTouchSoundAssets(); + + mSoundPoolLoader = new SoundPoolLoader(); + mSoundPoolLoader.addHandler(new OnEffectsLoadCompleteHandler() { + @Override + public void run(boolean success) { + mSoundPoolLoader = null; + if (!success) { + Log.w(TAG, "onLoadSoundEffects(), Error while loading samples"); + onUnloadSoundEffects(); + } + } + }); + mSoundPoolLoader.addHandler(onComplete); + + int resourcesToLoad = 0; + for (Resource res : mResources) { + String filePath = getResourceFilePath(res); + int sampleId = mSoundPool.load(filePath, 0); + if (sampleId > 0) { + res.mSampleId = sampleId; + res.mLoaded = false; + resourcesToLoad++; + } else { + logEvent("effect " + filePath + " rejected by SoundPool"); + Log.w(TAG, "SoundPool could not load file: " + filePath); + } + } + + if (resourcesToLoad > 0) { + sendMsg(MSG_LOAD_EFFECTS_TIMEOUT, 0, 0, null, SOUND_EFFECTS_LOAD_TIMEOUT_MS); + } else { + logEvent("effects loading completed, no effects to load"); + mSoundPoolLoader.onComplete(true /*success*/); + } + } + + void onUnloadSoundEffects() { + if (mSoundPool == null) { + return; + } + if (mSoundPoolLoader != null) { + mSoundPoolLoader.addHandler(new OnEffectsLoadCompleteHandler() { + @Override + public void run(boolean success) { + onUnloadSoundEffects(); + } + }); + } + + logEvent("effects unloading started"); + for (Resource res : mResources) { + if (res.mSampleId != EFFECT_NOT_IN_SOUND_POOL) { + mSoundPool.unload(res.mSampleId); + } + } + mSoundPool.release(); + mSoundPool = null; + logEvent("effects unloading completed"); + } + + void onPlaySoundEffect(int effect, int volume) { + float volFloat; + // use default if volume is not specified by caller + if (volume < 0) { + volFloat = (float) Math.pow(10, (float) mSfxAttenuationDb / 20); + } else { + volFloat = volume / 1000.0f; + } + + Resource res = mResources.get(mEffects[effect]); + if (res.mSampleId != EFFECT_NOT_IN_SOUND_POOL && res.mLoaded) { + mSoundPool.play(res.mSampleId, volFloat, volFloat, 0, 0, 1.0f); + } else { + MediaPlayer mediaPlayer = new MediaPlayer(); + try { + String filePath = getResourceFilePath(res); + mediaPlayer.setDataSource(filePath); + mediaPlayer.setAudioStreamType(AudioSystem.STREAM_SYSTEM); + mediaPlayer.prepare(); + mediaPlayer.setVolume(volFloat); + mediaPlayer.setOnCompletionListener(new OnCompletionListener() { + public void onCompletion(MediaPlayer mp) { + cleanupPlayer(mp); + } + }); + mediaPlayer.setOnErrorListener(new OnErrorListener() { + public boolean onError(MediaPlayer mp, int what, int extra) { + cleanupPlayer(mp); + return true; + } + }); + mediaPlayer.start(); + } catch (IOException ex) { + Log.w(TAG, "MediaPlayer IOException: " + ex); + } catch (IllegalArgumentException ex) { + Log.w(TAG, "MediaPlayer IllegalArgumentException: " + ex); + } catch (IllegalStateException ex) { + Log.w(TAG, "MediaPlayer IllegalStateException: " + ex); + } + } + } + + private static void cleanupPlayer(MediaPlayer mp) { + if (mp != null) { + try { + mp.stop(); + mp.release(); + } catch (IllegalStateException ex) { + Log.w(TAG, "MediaPlayer IllegalStateException: " + ex); + } + } + } + + private static final String TAG_AUDIO_ASSETS = "audio_assets"; + private static final String ATTR_VERSION = "version"; + private static final String TAG_GROUP = "group"; + private static final String ATTR_GROUP_NAME = "name"; + private static final String TAG_ASSET = "asset"; + private static final String ATTR_ASSET_ID = "id"; + private static final String ATTR_ASSET_FILE = "file"; + + private static final String ASSET_FILE_VERSION = "1.0"; + private static final String GROUP_TOUCH_SOUNDS = "touch_sounds"; + + private static final int SOUND_EFFECTS_LOAD_TIMEOUT_MS = 15000; + + private String getResourceFilePath(Resource res) { + String filePath = Environment.getProductDirectory() + SOUND_EFFECTS_PATH + res.mFileName; + if (!new File(filePath).isFile()) { + filePath = Environment.getRootDirectory() + SOUND_EFFECTS_PATH + res.mFileName; + } + return filePath; + } + + private void loadTouchSoundAssetDefaults() { + int defaultResourceIdx = mResources.size(); + mResources.add(new Resource("Effect_Tick.ogg")); + for (int i = 0; i < mEffects.length; i++) { + mEffects[i] = defaultResourceIdx; + } + } + + private void loadTouchSoundAssets() { + XmlResourceParser parser = null; + + // only load assets once. + if (!mResources.isEmpty()) { + return; + } + + loadTouchSoundAssetDefaults(); + + try { + parser = mContext.getResources().getXml(com.android.internal.R.xml.audio_assets); + + XmlUtils.beginDocument(parser, TAG_AUDIO_ASSETS); + String version = parser.getAttributeValue(null, ATTR_VERSION); + boolean inTouchSoundsGroup = false; + + if (ASSET_FILE_VERSION.equals(version)) { + while (true) { + XmlUtils.nextElement(parser); + String element = parser.getName(); + if (element == null) { + break; + } + if (element.equals(TAG_GROUP)) { + String name = parser.getAttributeValue(null, ATTR_GROUP_NAME); + if (GROUP_TOUCH_SOUNDS.equals(name)) { + inTouchSoundsGroup = true; + break; + } + } + } + while (inTouchSoundsGroup) { + XmlUtils.nextElement(parser); + String element = parser.getName(); + if (element == null) { + break; + } + if (element.equals(TAG_ASSET)) { + String id = parser.getAttributeValue(null, ATTR_ASSET_ID); + String file = parser.getAttributeValue(null, ATTR_ASSET_FILE); + int fx; + + try { + Field field = AudioManager.class.getField(id); + fx = field.getInt(null); + } catch (Exception e) { + Log.w(TAG, "Invalid touch sound ID: " + id); + continue; + } + + mEffects[fx] = findOrAddResourceByFileName(file); + } else { + break; + } + } + } + } catch (Resources.NotFoundException e) { + Log.w(TAG, "audio assets file not found", e); + } catch (XmlPullParserException e) { + Log.w(TAG, "XML parser exception reading touch sound assets", e); + } catch (IOException e) { + Log.w(TAG, "I/O exception reading touch sound assets", e); + } finally { + if (parser != null) { + parser.close(); + } + } + } + + private int findOrAddResourceByFileName(String fileName) { + for (int i = 0; i < mResources.size(); i++) { + if (mResources.get(i).mFileName.equals(fileName)) { + return i; + } + } + int result = mResources.size(); + mResources.add(new Resource(fileName)); + return result; + } + + private Resource findResourceBySampleId(int sampleId) { + for (Resource res : mResources) { + if (res.mSampleId == sampleId) { + return res; + } + } + return null; + } + + private class SfxWorker extends Thread { + SfxWorker() { + super("AS.SfxWorker"); + } + + @Override + public void run() { + Looper.prepare(); + synchronized (SoundEffectsHelper.this) { + mSfxHandler = new SfxHandler(); + SoundEffectsHelper.this.notify(); + } + Looper.loop(); + } + } + + private class SfxHandler extends Handler { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_LOAD_EFFECTS: + onLoadSoundEffects((OnEffectsLoadCompleteHandler) msg.obj); + break; + case MSG_UNLOAD_EFFECTS: + onUnloadSoundEffects(); + break; + case MSG_PLAY_EFFECT: + onLoadSoundEffects(new OnEffectsLoadCompleteHandler() { + @Override + public void run(boolean success) { + if (success) { + onPlaySoundEffect(msg.arg1 /*effect*/, msg.arg2 /*volume*/); + } + } + }); + break; + case MSG_LOAD_EFFECTS_TIMEOUT: + if (mSoundPoolLoader != null) { + mSoundPoolLoader.onTimeout(); + } + break; + } + } + } + + private class SoundPoolLoader implements + android.media.SoundPool.OnLoadCompleteListener { + + private List<OnEffectsLoadCompleteHandler> mLoadCompleteHandlers = + new ArrayList<OnEffectsLoadCompleteHandler>(); + + SoundPoolLoader() { + // SoundPool use the current Looper when creating its message handler. + // Since SoundPoolLoader is created on the SfxWorker thread, SoundPool's + // message handler ends up running on it (it's OK to have multiple + // handlers on the same Looper). Thus, onLoadComplete gets executed + // on the worker thread. + mSoundPool.setOnLoadCompleteListener(this); + } + + void addHandler(OnEffectsLoadCompleteHandler handler) { + if (handler != null) { + mLoadCompleteHandlers.add(handler); + } + } + + @Override + public void onLoadComplete(SoundPool soundPool, int sampleId, int status) { + if (status == 0) { + int remainingToLoad = 0; + for (Resource res : mResources) { + if (res.mSampleId == sampleId && !res.mLoaded) { + logEvent("effect " + res.mFileName + " loaded"); + res.mLoaded = true; + } + if (res.mSampleId != EFFECT_NOT_IN_SOUND_POOL && !res.mLoaded) { + remainingToLoad++; + } + } + if (remainingToLoad == 0) { + onComplete(true); + } + } else { + Resource res = findResourceBySampleId(sampleId); + String filePath; + if (res != null) { + filePath = getResourceFilePath(res); + } else { + filePath = "with unknown sample ID " + sampleId; + } + logEvent("effect " + filePath + " loading failed, status " + status); + Log.w(TAG, "onLoadSoundEffects(), Error " + status + " while loading sample " + + filePath); + onComplete(false); + } + } + + void onTimeout() { + onComplete(false); + } + + void onComplete(boolean success) { + mSoundPool.setOnLoadCompleteListener(null); + for (OnEffectsLoadCompleteHandler handler : mLoadCompleteHandlers) { + handler.run(success); + } + logEvent("effects loading " + (success ? "completed" : "failed")); + } + } +} |