summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author TreeHugger Robot <treehugger-gerrit@google.com> 2019-07-11 20:15:29 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2019-07-11 20:15:29 +0000
commitfb64399ec3d0421e4b65e67b7ca35816544f44a1 (patch)
tree3c20c1d864c579ef100d87901881e2a455ad423e
parent3f8cdda0497f8dc4fbba56892777327f9bee9273 (diff)
parent251c595896e9a27c3fb70a61541c479f435442e0 (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.java468
-rw-r--r--services/core/java/com/android/server/audio/SoundEffectsHelper.java521
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"));
+ }
+ }
+}