diff options
author | Jean-Michel Trivi <jmtrivi@google.com> | 2014-11-25 12:53:41 -0800 |
---|---|---|
committer | Jean-Michel Trivi <jmtrivi@google.com> | 2014-11-30 14:33:29 -0800 |
commit | 1b3541d5eedb332ea01066b4a78a2d06d5304044 (patch) | |
tree | 2920579a268983cae5fd35002a68f04a8eb235ad /media/java | |
parent | 563e61f5d06aca653894cecc2977bac70b08227c (diff) | |
download | frameworks_base-1b3541d5eedb332ea01066b4a78a2d06d5304044.zip frameworks_base-1b3541d5eedb332ea01066b4a78a2d06d5304044.tar.gz frameworks_base-1b3541d5eedb332ea01066b4a78a2d06d5304044.tar.bz2 |
AudioMix address and type, rule exclusion API, dynamic source
- Simplify API for defining an exclusion-based rule: don't define
an exclusion rule, add instead a way to exclude a rule.
- API for defining rules for dynamic sources (rule match on capture
preset).
- Verify mix type when creating AudioRecord or AudioTrack for a mix.
- Use hashcode of mix for generating the device address.
- AudioService dump prints info about registered policies.
- Annotate as SystemApi the audio policy-related APIs.
- Express mixing match and exclude rule constants as flags for
future-proofness
Bug 16006090
Bug 16009464
Change-Id: I0dabe71204501acaffea7ef0ddbbab9700e1bd87
Diffstat (limited to 'media/java')
-rw-r--r-- | media/java/android/media/AudioAttributes.java | 3 | ||||
-rw-r--r-- | media/java/android/media/AudioManager.java | 14 | ||||
-rw-r--r-- | media/java/android/media/AudioService.java | 29 | ||||
-rw-r--r-- | media/java/android/media/audiopolicy/AudioMix.java | 41 | ||||
-rw-r--r-- | media/java/android/media/audiopolicy/AudioMixingRule.java | 187 | ||||
-rw-r--r-- | media/java/android/media/audiopolicy/AudioPolicy.java | 130 | ||||
-rw-r--r-- | media/java/android/media/audiopolicy/AudioPolicyConfig.java | 59 |
7 files changed, 397 insertions, 66 deletions
diff --git a/media/java/android/media/AudioAttributes.java b/media/java/android/media/AudioAttributes.java index 20c4978..489f552 100644 --- a/media/java/android/media/AudioAttributes.java +++ b/media/java/android/media/AudioAttributes.java @@ -241,11 +241,11 @@ public final class AudioAttributes implements Parcelable { /** * @hide - * CANDIDATE FOR PUBLIC API * Return the capture preset. * @return one of the values that can be set in {@link Builder#setCapturePreset(int)} or a * negative value if none has been set. */ + @SystemApi public int getCapturePreset() { return mSource; } @@ -508,6 +508,7 @@ public final class AudioAttributes implements Parcelable { * {@link MediaRecorder.AudioSource#VOICE_COMMUNICATION}. * @return the same Builder instance. */ + @SystemApi public Builder setCapturePreset(int preset) { switch (preset) { case MediaRecorder.AudioSource.DEFAULT: diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index e61b563..a764954 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -2448,6 +2448,7 @@ public class AudioManager { * {@link #requestAudioFocus(OnAudioFocusChangeListener, AudioAttributes, int, int)} * @throws IllegalArgumentException */ + @SystemApi public int requestAudioFocus(OnAudioFocusChangeListener l, @NonNull AudioAttributes requestAttributes, int durationHint, @@ -2842,17 +2843,17 @@ public class AudioManager { /** * @hide - * CANDIDATE FOR PUBLIC API * Register the given {@link AudioPolicy}. * This call is synchronous and blocks until the registration process successfully completed * or failed to complete. - * @param policy the {@link AudioPolicy} to register. + * @param policy the non-null {@link AudioPolicy} to register. * @return {@link #ERROR} if there was an error communicating with the registration service * or if the user doesn't have the required * {@link android.Manifest.permission#MODIFY_AUDIO_ROUTING} permission, * {@link #SUCCESS} otherwise. */ - public int registerAudioPolicy(AudioPolicy policy) { + @SystemApi + public int registerAudioPolicy(@NonNull AudioPolicy policy) { if (policy == null) { throw new IllegalArgumentException("Illegal null AudioPolicy argument"); } @@ -2874,16 +2875,17 @@ public class AudioManager { /** * @hide - * CANDIDATE FOR PUBLIC API - * @param policy the {@link AudioPolicy} to unregister. + * @param policy the non-null {@link AudioPolicy} to unregister. */ - public void unregisterAudioPolicyAsync(AudioPolicy policy) { + @SystemApi + public void unregisterAudioPolicyAsync(@NonNull AudioPolicy policy) { if (policy == null) { throw new IllegalArgumentException("Illegal null AudioPolicy argument"); } IAudioService service = getService(); try { service.unregisterAudioPolicyAsync(policy.token()); + policy.setRegistration(null); } catch (RemoteException e) { Log.e(TAG, "Dead object in unregisterAudioPolicyAsync()", e); } diff --git a/media/java/android/media/AudioService.java b/media/java/android/media/AudioService.java index e5ae93c..73997af 100644 --- a/media/java/android/media/AudioService.java +++ b/media/java/android/media/AudioService.java @@ -5520,6 +5520,8 @@ public class AudioService extends IAudioService.Stub { pw.print(" mMusicActiveMs="); pw.println(mMusicActiveMs); pw.print(" mMcc="); pw.println(mMcc); pw.print(" mHasVibrator="); pw.println(mHasVibrator); + + dumpAudioPolicies(pw); } private static String safeMediaVolumeStateToString(Integer state) { @@ -5789,6 +5791,10 @@ public class AudioService extends IAudioService.Stub { } synchronized (mAudioPolicies) { try { + if (mAudioPolicies.containsKey(cb)) { + Slog.e(TAG, "Cannot re-register policy"); + return null; + } AudioPolicyProxy app = new AudioPolicyProxy(policyConfig, cb); cb.linkToDeath(app, 0/*flags*/); regId = app.connectMixes(); @@ -5809,6 +5815,7 @@ public class AudioService extends IAudioService.Stub { if (app == null) { Slog.w(TAG, "Trying to unregister unknown audio policy for pid " + Binder.getCallingPid() + " / uid " + Binder.getCallingUid()); + return; } else { cb.unlinkToDeath(app, 0/*flags*/); } @@ -5817,12 +5824,21 @@ public class AudioService extends IAudioService.Stub { // TODO implement clearing mix attribute matching info in native audio policy } + private void dumpAudioPolicies(PrintWriter pw) { + pw.println("\nAudio policies:"); + synchronized (mAudioPolicies) { + for(AudioPolicyProxy policy : mAudioPolicies.values()) { + pw.println(policy.toLogFriendlyString()); + } + } + } + //====================== // Audio policy proxy //====================== /** - * This internal class inherits from AudioPolicyConfig which contains all the mixes and - * their configurations. + * This internal class inherits from AudioPolicyConfig, each instance contains all the + * mixes of an AudioPolicy and their configurations. */ public class AudioPolicyProxy extends AudioPolicyConfig implements IBinder.DeathRecipient { private static final String TAG = "AudioPolicyProxy"; @@ -5830,7 +5846,7 @@ public class AudioService extends IAudioService.Stub { IBinder mToken; AudioPolicyProxy(AudioPolicyConfig config, IBinder token) { super(config); - setRegistration(new String(config.toString() + ":ap:" + mAudioPolicyCounter++)); + setRegistration(new String(config.hashCode() + ":ap:" + mAudioPolicyCounter++)); mToken = token; } @@ -5844,7 +5860,7 @@ public class AudioService extends IAudioService.Stub { String connectMixes() { updateMixes(AudioSystem.DEVICE_STATE_AVAILABLE); - return mRegistrationId; + return getRegistration(); } void disconnectMixes() { @@ -5855,8 +5871,9 @@ public class AudioService extends IAudioService.Stub { for (AudioMix mix : mMixes) { // TODO implement sending the mix attribute matching info to native audio policy if (DEBUG_AP) { - Log.v(TAG, "AudioPolicyProxy connect mix state=" + connectionState - + " addr=" + mix.getRegistration()); } + Log.v(TAG, "AudioPolicyProxy mix new connection state=" + connectionState + + " addr=" + mix.getRegistration()); + } AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_IN_REMOTE_SUBMIX, connectionState, mix.getRegistration()); diff --git a/media/java/android/media/audiopolicy/AudioMix.java b/media/java/android/media/audiopolicy/AudioMix.java index bb52682..1806662 100644 --- a/media/java/android/media/audiopolicy/AudioMix.java +++ b/media/java/android/media/audiopolicy/AudioMix.java @@ -17,21 +17,25 @@ package android.media.audiopolicy; import android.annotation.IntDef; +import android.annotation.SystemApi; import android.media.AudioFormat; import android.media.AudioSystem; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Objects; /** * @hide */ +@SystemApi public class AudioMix { private AudioMixingRule mRule; private AudioFormat mFormat; private int mRouteFlags; private String mRegistrationId; + private int mMixType = MIX_TYPE_INVALID; /** * All parameters are guaranteed valid through the Builder. @@ -41,20 +45,39 @@ public class AudioMix { mFormat = format; mRouteFlags = routeFlags; mRegistrationId = null; + mMixType = rule.getTargetMixType(); } /** * An audio mix behavior where the output of the mix is sent to the original destination of * the audio signal, i.e. an output device for an output mix, or a recording for an input mix. */ + @SystemApi public static final int ROUTE_FLAG_RENDER = 0x1; /** * An audio mix behavior where the output of the mix is rerouted back to the framework and - * is accessible for injection or capture through the {@link Audiotrack} and {@link AudioRecord} + * is accessible for injection or capture through the {@link AudioTrack} and {@link AudioRecord} * APIs. */ + @SystemApi public static final int ROUTE_FLAG_LOOP_BACK = 0x1 << 1; + /** + * @hide + * Invalid mix type, default value. + */ + public static final int MIX_TYPE_INVALID = -1; + /** + * @hide + * Mix type indicating playback streams are mixed. + */ + public static final int MIX_TYPE_PLAYERS = 0; + /** + * @hide + * Mix type indicating recording streams are mixed. + */ + public static final int MIX_TYPE_RECORDERS = 1; + int getRouteFlags() { return mRouteFlags; } @@ -67,6 +90,11 @@ public class AudioMix { return mRule; } + /** @hide */ + public int getMixType() { + return mMixType; + } + void setRegistration(String regId) { mRegistrationId = regId; } @@ -77,6 +105,12 @@ public class AudioMix { } /** @hide */ + @Override + public int hashCode() { + return Objects.hash(mRouteFlags, mRule, mMixType, mFormat); + } + + /** @hide */ @IntDef(flag = true, value = { ROUTE_FLAG_RENDER, ROUTE_FLAG_LOOP_BACK } ) @Retention(RetentionPolicy.SOURCE) @@ -86,6 +120,7 @@ public class AudioMix { * Builder class for {@link AudioMix} objects * */ + @SystemApi public static class Builder { private AudioMixingRule mRule = null; private AudioFormat mFormat = null; @@ -102,6 +137,7 @@ public class AudioMix { * @param rule a non-null {@link AudioMixingRule} instance. * @throws IllegalArgumentException */ + @SystemApi public Builder(AudioMixingRule rule) throws IllegalArgumentException { if (rule == null) { @@ -132,6 +168,7 @@ public class AudioMix { * @return the same Builder instance. * @throws IllegalArgumentException */ + @SystemApi public Builder setFormat(AudioFormat format) throws IllegalArgumentException { if (format == null) { @@ -148,6 +185,7 @@ public class AudioMix { * @return the same Builder instance. * @throws IllegalArgumentException */ + @SystemApi public Builder setRouteFlags(@RouteFlags int routeFlags) throws IllegalArgumentException { if (routeFlags == 0) { @@ -166,6 +204,7 @@ public class AudioMix { * @return a new {@link AudioMix} object * @throws IllegalArgumentException if no {@link AudioMixingRule} has been set. */ + @SystemApi public AudioMix build() throws IllegalArgumentException { if (mRule == null) { throw new IllegalArgumentException("Illegal null AudioMixingRule"); diff --git a/media/java/android/media/audiopolicy/AudioMixingRule.java b/media/java/android/media/audiopolicy/AudioMixingRule.java index 2e06a80..02b03d2 100644 --- a/media/java/android/media/audiopolicy/AudioMixingRule.java +++ b/media/java/android/media/audiopolicy/AudioMixingRule.java @@ -16,10 +16,13 @@ package android.media.audiopolicy; +import android.annotation.SystemApi; import android.media.AudioAttributes; +import android.os.Parcel; import java.util.ArrayList; import java.util.Iterator; +import java.util.Objects; /** @@ -35,44 +38,114 @@ import java.util.Iterator; * .build(); * </pre> */ +@SystemApi public class AudioMixingRule { - private AudioMixingRule(ArrayList<AttributeMatchCriterion> criteria) { + private AudioMixingRule(int mixType, ArrayList<AttributeMatchCriterion> criteria) { mCriteria = criteria; + mTargetMixType = mixType; } /** - * A rule requiring the usage information of the {@link AudioAttributes} to match + * A rule requiring the usage information of the {@link AudioAttributes} to match. */ + @SystemApi public static final int RULE_MATCH_ATTRIBUTE_USAGE = 0x1; /** - * A rule requiring the usage information of the {@link AudioAttributes} to differ + * A rule requiring the capture preset information of the {@link AudioAttributes} to match. */ - public static final int RULE_EXCLUDE_ATTRIBUTE_USAGE = 0x1 << 1; + @SystemApi + public static final int RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET = 0x1 << 1; + + private final static int RULE_EXCLUSION_MASK = 0x8000; + /** + * @hide + * A rule requiring the usage information of the {@link AudioAttributes} to differ. + */ + public static final int RULE_EXCLUDE_ATTRIBUTE_USAGE = + RULE_EXCLUSION_MASK | RULE_MATCH_ATTRIBUTE_USAGE; + /** + * @hide + * A rule requiring the capture preset information of the {@link AudioAttributes} to differ. + */ + public static final int RULE_EXCLUDE_ATTRIBUTE_CAPTURE_PRESET = + RULE_EXCLUSION_MASK | RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET; static final class AttributeMatchCriterion { AudioAttributes mAttr; int mRule; + /** input parameters must be valid */ AttributeMatchCriterion(AudioAttributes attributes, int rule) { mAttr = attributes; mRule = rule; } + + @Override + public int hashCode() { + return Objects.hash(mAttr, mRule); + } + + void writeToParcel(Parcel dest) { + dest.writeInt(mRule); + if ((mRule == RULE_MATCH_ATTRIBUTE_USAGE) || (mRule == RULE_EXCLUDE_ATTRIBUTE_USAGE)) { + dest.writeInt(mAttr.getUsage()); + } else { + // capture preset rule + dest.writeInt(mAttr.getCapturePreset()); + } + } } - private ArrayList<AttributeMatchCriterion> mCriteria; + private final int mTargetMixType; + int getTargetMixType() { return mTargetMixType; } + private final ArrayList<AttributeMatchCriterion> mCriteria; ArrayList<AttributeMatchCriterion> getCriteria() { return mCriteria; } + @Override + public int hashCode() { + return Objects.hash(mTargetMixType, mCriteria); + } + + private static boolean isValidSystemApiRule(int rule) { + switch(rule) { + case RULE_MATCH_ATTRIBUTE_USAGE: + case RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET: + return true; + default: + return false; + } + } + + private static boolean isValidIntRule(int rule) { + switch(rule) { + case RULE_MATCH_ATTRIBUTE_USAGE: + case RULE_EXCLUDE_ATTRIBUTE_USAGE: + case RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET: + case RULE_EXCLUDE_ATTRIBUTE_CAPTURE_PRESET: + return true; + default: + return false; + } + } + + private static boolean isPlayerRule(int rule) { + return ((rule == RULE_MATCH_ATTRIBUTE_USAGE) + || (rule == RULE_EXCLUDE_ATTRIBUTE_USAGE)); + } + /** * Builder class for {@link AudioMixingRule} objects - * */ + @SystemApi public static class Builder { private ArrayList<AttributeMatchCriterion> mCriteria; + private int mTargetMixType = AudioMix.MIX_TYPE_INVALID; /** * Constructs a new Builder with no rules. */ + @SystemApi public Builder() { mCriteria = new ArrayList<AttributeMatchCriterion>(); } @@ -81,18 +154,80 @@ public class AudioMixingRule { * Add a rule for the selection of which streams are mixed together. * @param attrToMatch a non-null AudioAttributes instance for which a contradictory * rule hasn't been set yet. - * @param rule one of {@link AudioMixingRule#RULE_EXCLUDE_ATTRIBUTE_USAGE}, - * {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_USAGE}. + * @param rule {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_USAGE} or + * {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET}. * @return the same Builder instance. * @throws IllegalArgumentException */ + @SystemApi public Builder addRule(AudioAttributes attrToMatch, int rule) throws IllegalArgumentException { + if (!isValidSystemApiRule(rule)) { + throw new IllegalArgumentException("Illegal rule value " + rule); + } + return addRuleInt(attrToMatch, rule); + } + + /** + * Add a rule by exclusion for the selection of which streams are mixed together. + * <br>For instance the following code + * <br><pre> + * AudioAttributes mediaAttr = new AudioAttributes.Builder() + * .setUsage(AudioAttributes.USAGE_MEDIA) + * .build(); + * AudioMixingRule noMediaRule = new AudioMixingRule.Builder() + * .excludeRule(mediaAttr, AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE) + * .build(); + * </pre> + * <br>will create a rule which maps to any usage value, except USAGE_MEDIA. + * @param attrToMatch a non-null AudioAttributes instance for which a contradictory + * rule hasn't been set yet. + * @param rule {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_USAGE} or + * {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET}. + * @return the same Builder instance. + * @throws IllegalArgumentException + */ + @SystemApi + public Builder excludeRule(AudioAttributes attrToMatch, int rule) + throws IllegalArgumentException { + if (!isValidSystemApiRule(rule)) { + throw new IllegalArgumentException("Illegal rule value " + rule); + } + return addRuleInt(attrToMatch, rule | RULE_EXCLUSION_MASK); + } + + /** + * Add or exclude a rule for the selection of which streams are mixed together. + * @param attrToMatch a non-null AudioAttributes instance for which a contradictory + * rule hasn't been set yet. + * @param rule one of {@link AudioMixingRule#RULE_EXCLUDE_ATTRIBUTE_USAGE}, + * {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_USAGE}, + * {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET} or + * {@link AudioMixingRule#RULE_EXCLUDE_ATTRIBUTE_CAPTURE_PRESET}. + * @return the same Builder instance. + * @throws IllegalArgumentException + */ + Builder addRuleInt(AudioAttributes attrToMatch, int rule) + throws IllegalArgumentException { if (attrToMatch == null) { throw new IllegalArgumentException("Illegal null AudioAttributes argument"); } - if ((rule != RULE_MATCH_ATTRIBUTE_USAGE) && (rule != RULE_EXCLUDE_ATTRIBUTE_USAGE)) { + if (!isValidIntRule(rule)) { throw new IllegalArgumentException("Illegal rule value " + rule); + } else { + // as rules are added to the Builder, we verify they are consistent with the type + // of mix being built. When adding the first rule, the mix type is MIX_TYPE_INVALID. + if (mTargetMixType == AudioMix.MIX_TYPE_INVALID) { + if (isPlayerRule(rule)) { + mTargetMixType = AudioMix.MIX_TYPE_PLAYERS; + } else { + mTargetMixType = AudioMix.MIX_TYPE_RECORDERS; + } + } else if (((mTargetMixType == AudioMix.MIX_TYPE_PLAYERS) && !isPlayerRule(rule)) + || ((mTargetMixType == AudioMix.MIX_TYPE_RECORDERS) && isPlayerRule(rule))) + { + throw new IllegalArgumentException("Incompatible rule for mix"); + } } synchronized (mCriteria) { Iterator<AttributeMatchCriterion> crIterator = mCriteria.iterator(); @@ -111,6 +246,19 @@ public class AudioMixingRule { + attrToMatch); } } + } else if ((rule == RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET) + || (rule == RULE_EXCLUDE_ATTRIBUTE_CAPTURE_PRESET)) { + // "capture preset"-base rule + if (criterion.mAttr.getCapturePreset() == attrToMatch.getCapturePreset()) { + if (criterion.mRule == rule) { + // rule already exists, we're done + return this; + } else { + // criterion already exists with a another rule, it is incompatible + throw new IllegalArgumentException("Contradictory rule exists for " + + attrToMatch); + } + } } } // rule didn't exist, add it @@ -119,13 +267,32 @@ public class AudioMixingRule { return this; } + Builder addRuleFromParcel(Parcel in) throws IllegalArgumentException { + int rule = in.readInt(); + AudioAttributes attr; + if ((rule == RULE_MATCH_ATTRIBUTE_USAGE) || (rule == RULE_EXCLUDE_ATTRIBUTE_USAGE)) { + int usage = in.readInt(); + attr = new AudioAttributes.Builder() + .setUsage(usage).build(); + } else if ((rule == RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET) + || (rule == RULE_EXCLUDE_ATTRIBUTE_CAPTURE_PRESET)) { + int preset = in.readInt(); + attr = new AudioAttributes.Builder() + .setInternalCapturePreset(preset).build(); + } else { + in.readInt(); // assume there was in int value to read as for now they come in pair + throw new IllegalArgumentException("Illegal rule value " + rule + " in parcel"); + } + return addRuleInt(attr, rule); + } + /** * Combines all of the matching and exclusion rules that have been set and return a new * {@link AudioMixingRule} object. * @return a new {@link AudioMixingRule} object */ public AudioMixingRule build() { - return new AudioMixingRule(mCriteria); + return new AudioMixingRule(mTargetMixType, mCriteria); } } } diff --git a/media/java/android/media/audiopolicy/AudioPolicy.java b/media/java/android/media/audiopolicy/AudioPolicy.java index e9dc3af..44d2430 100644 --- a/media/java/android/media/audiopolicy/AudioPolicy.java +++ b/media/java/android/media/audiopolicy/AudioPolicy.java @@ -17,17 +17,21 @@ package android.media.audiopolicy; import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.SystemApi; import android.content.Context; import android.content.pm.PackageManager; import android.media.AudioAttributes; import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioRecord; -import android.media.AudioSystem; import android.media.AudioTrack; import android.media.MediaRecorder; import android.os.Binder; +import android.os.Handler; import android.os.IBinder; +import android.os.Looper; +import android.os.Message; import android.util.Log; import android.util.Slog; @@ -39,21 +43,20 @@ import java.util.ArrayList; * @hide * AudioPolicy provides access to the management of audio routing and audio focus. */ +@SystemApi public class AudioPolicy { private static final String TAG = "AudioPolicy"; /** - * The status of an audio policy that cannot be used because it is invalid. - */ - public static final int POLICY_STATUS_INVALID = 0; - /** * The status of an audio policy that is valid but cannot be used because it is not registered. */ + @SystemApi public static final int POLICY_STATUS_UNREGISTERED = 1; /** * The status of an audio policy that is valid, successfully registered and thus active. */ + @SystemApi public static final int POLICY_STATUS_REGISTERED = 2; private int mStatus; @@ -72,22 +75,29 @@ public class AudioPolicy { /** * The parameter is guaranteed non-null through the Builder */ - private AudioPolicy(AudioPolicyConfig config, Context context) { + private AudioPolicy(AudioPolicyConfig config, Context context, Looper looper) { mConfig = config; - if (mConfig.mMixes.isEmpty()) { - mStatus = POLICY_STATUS_INVALID; + mStatus = POLICY_STATUS_UNREGISTERED; + mContext = context; + if (looper == null) { + looper = Looper.getMainLooper(); + } + if (looper != null) { + mEventHandler = new EventHandler(this, looper); } else { - mStatus = POLICY_STATUS_UNREGISTERED; + mEventHandler = null; + Log.e(TAG, "No event handler due to looper without a thread"); } - mContext = context; } /** * Builder class for {@link AudioPolicy} objects */ + @SystemApi public static class Builder { private ArrayList<AudioMix> mMixes; private Context mContext; + private Looper mLooper; /** * Constructs a new Builder with no audio mixes. @@ -104,7 +114,7 @@ public class AudioPolicy { * @return the same Builder instance. * @throws IllegalArgumentException */ - public Builder addMix(AudioMix mix) throws IllegalArgumentException { + public Builder addMix(@NonNull AudioMix mix) throws IllegalArgumentException { if (mix == null) { throw new IllegalArgumentException("Illegal null AudioMix argument"); } @@ -112,18 +122,41 @@ public class AudioPolicy { return this; } + /** + * Sets the {@link Looper} on which to run the event loop. + * @param looper a non-null specific Looper. + * @return the same Builder instance. + * @throws IllegalArgumentException + */ + public Builder setLooper(@NonNull Looper looper) throws IllegalArgumentException { + if (looper == null) { + throw new IllegalArgumentException("Illegal null Looper argument"); + } + mLooper = looper; + return this; + } + public AudioPolicy build() { - return new AudioPolicy(new AudioPolicyConfig(mMixes), mContext); + return new AudioPolicy(new AudioPolicyConfig(mMixes), mContext, mLooper); } } - /** @hide */ public void setRegistration(String regId) { mRegistrationId = regId; mConfig.setRegistration(regId); + if (regId != null) { + mStatus = POLICY_STATUS_REGISTERED; + } else { + mStatus = POLICY_STATUS_UNREGISTERED; + } + sendMsg(mEventHandler, MSG_POLICY_STATUS_CHANGE); } private boolean policyReadyToUse() { + if (mStatus != POLICY_STATUS_REGISTERED) { + Log.e(TAG, "Cannot use unregistered AudioPolicy"); + return false; + } if (mContext == null) { Log.e(TAG, "Cannot use AudioPolicy without context"); return false; @@ -155,11 +188,17 @@ public class AudioPolicy { { throw new IllegalArgumentException("Invalid AudioMix: not defined for loop back"); } - // TODO also check mix is defined for playback or recording, and matches forTrack argument + if (forTrack && (mix.getMixType() != AudioMix.MIX_TYPE_RECORDERS)) { + throw new IllegalArgumentException( + "Invalid AudioMix: not defined for being a recording source"); + } + if (!forTrack && (mix.getMixType() != AudioMix.MIX_TYPE_PLAYERS)) { + throw new IllegalArgumentException( + "Invalid AudioMix: not defined for capturing playback"); + } } /** - * @hide * Create an {@link AudioRecord} instance that is associated with the given {@link AudioMix}. * Audio buffers recorded through the created instance will contain the mix of the audio * streams that fed the given mixer. @@ -170,6 +209,7 @@ public class AudioPolicy { * with {@link AudioManager#registerAudioPolicy(AudioPolicy)}. * @throws IllegalArgumentException */ + @SystemApi public AudioRecord createAudioRecordSink(AudioMix mix) throws IllegalArgumentException { if (!policyReadyToUse()) { Log.e(TAG, "Cannot create AudioRecord sink for AudioMix"); @@ -186,7 +226,7 @@ public class AudioPolicy { AudioRecord ar = new AudioRecord( new AudioAttributes.Builder() .setInternalCapturePreset(MediaRecorder.AudioSource.REMOTE_SUBMIX) - .addTag(mix.getRegistration()) + .addTag(addressForTag(mix)) .build(), mixFormat, AudioRecord.getMinBufferSize(mix.getFormat().getSampleRate(), @@ -198,17 +238,17 @@ public class AudioPolicy { } /** - * @hide * Create an {@link AudioTrack} instance that is associated with the given {@link AudioMix}. * Audio buffers played through the created instance will be sent to the given mix * to be recorded through the recording APIs. * @param mix a non-null {@link AudioMix} instance whose routing flags was defined with * {@link AudioMix#ROUTE_FLAG_LOOP_BACK}, previously added to this policy. - * @returna new {@link AudioTrack} instance whose data format is the one defined in the + * @return a new {@link AudioTrack} instance whose data format is the one defined in the * {@link AudioMix}, or null if this policy was not successfully registered * with {@link AudioManager#registerAudioPolicy(AudioPolicy)}. * @throws IllegalArgumentException */ + @SystemApi public AudioTrack createAudioTrackSource(AudioMix mix) throws IllegalArgumentException { if (!policyReadyToUse()) { Log.e(TAG, "Cannot create AudioTrack source for AudioMix"); @@ -219,7 +259,7 @@ public class AudioPolicy { AudioTrack at = new AudioTrack( new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_VIRTUAL_SOURCE) - .addTag(mix.getRegistration()) + .addTag(addressForTag(mix)) .build(), mix.getFormat(), AudioTrack.getMinBufferSize(mix.getFormat().getSampleRate(), @@ -230,20 +270,63 @@ public class AudioPolicy { return at; } + @SystemApi public int getStatus() { return mStatus; } + @SystemApi public static abstract class AudioPolicyStatusListener { - void onStatusChange() {} - void onMixStateUpdate(AudioMix mix) {} + public void onStatusChange() {} + public void onMixStateUpdate(AudioMix mix) {} } - void setStatusListener(AudioPolicyStatusListener l) { + @SystemApi + synchronized public void setAudioPolicyStatusListener(AudioPolicyStatusListener l) { mStatusListener = l; } - /** @hide */ + synchronized private void onPolicyStatusChange() { + if (mStatusListener == null) { + return; + } + mStatusListener.onStatusChange(); + } + + //================================================== + // Event handling + private final EventHandler mEventHandler; + private final static int MSG_POLICY_STATUS_CHANGE = 0; + + private class EventHandler extends Handler { + public EventHandler(AudioPolicy ap, Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch(msg.what) { + case MSG_POLICY_STATUS_CHANGE: + onPolicyStatusChange(); + break; + default: + Log.e(TAG, "Unknown event " + msg.what); + } + } + } + + //========================================================== + // Utils + private static String addressForTag(AudioMix mix) { + return "addr=" + mix.getRegistration(); + } + + private static void sendMsg(Handler handler, int msg) { + if (handler != null) { + handler.sendEmptyMessage(msg); + } + } + public String toLogFriendlyString() { String textDump = new String("android.media.audiopolicy.AudioPolicy:\n"); textDump += "config=" + mConfig.toLogFriendlyString(); @@ -252,7 +335,6 @@ public class AudioPolicy { /** @hide */ @IntDef({ - POLICY_STATUS_INVALID, POLICY_STATUS_REGISTERED, POLICY_STATUS_UNREGISTERED }) diff --git a/media/java/android/media/audiopolicy/AudioPolicyConfig.java b/media/java/android/media/audiopolicy/AudioPolicyConfig.java index a9a4175..e2a20da 100644 --- a/media/java/android/media/audiopolicy/AudioPolicyConfig.java +++ b/media/java/android/media/audiopolicy/AudioPolicyConfig.java @@ -27,6 +27,7 @@ import android.os.Parcelable; import android.util.Log; import java.util.ArrayList; +import java.util.Objects; /** * @hide @@ -38,7 +39,7 @@ public class AudioPolicyConfig implements Parcelable { protected ArrayList<AudioMix> mMixes; - protected String mRegistrationId = null; + private String mRegistrationId = null; protected AudioPolicyConfig(AudioPolicyConfig conf) { mMixes = conf.mMixes; @@ -62,6 +63,11 @@ public class AudioPolicyConfig implements Parcelable { } @Override + public int hashCode() { + return Objects.hash(mMixes); + } + + @Override public int describeContents() { return 0; } @@ -80,8 +86,7 @@ public class AudioPolicyConfig implements Parcelable { final ArrayList<AttributeMatchCriterion> criteria = mix.getRule().getCriteria(); dest.writeInt(criteria.size()); for (AttributeMatchCriterion criterion : criteria) { - dest.writeInt(criterion.mRule); - dest.writeInt(criterion.mAttr.getUsage()); + criterion.writeToParcel(dest); } } } @@ -106,17 +111,7 @@ public class AudioPolicyConfig implements Parcelable { AudioMixingRule.Builder ruleBuilder = new AudioMixingRule.Builder(); for (int j = 0 ; j < nbRules ; j++) { // read the matching rules - int matchRule = in.readInt(); - if ((matchRule == AudioMixingRule.RULE_EXCLUDE_ATTRIBUTE_USAGE) - || (matchRule == AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE)) { - int usage = in.readInt(); - final AudioAttributes attr = new AudioAttributes.Builder() - .setUsage(usage).build(); - ruleBuilder.addRule(attr, matchRule); - } else { - Log.w(TAG, "Encountered unsupported rule, skipping"); - in.readInt(); - } + ruleBuilder.addRuleFromParcel(in); } mixBuilder.setMixingRule(ruleBuilder.build()); mMixes.add(mixBuilder.build()); @@ -140,7 +135,7 @@ public class AudioPolicyConfig implements Parcelable { public String toLogFriendlyString () { String textDump = new String("android.media.audiopolicy.AudioPolicyConfig:\n"); - textDump += mMixes.size() + " AudioMix:\n"; + textDump += mMixes.size() + " AudioMix: "+ mRegistrationId + "\n"; for(AudioMix mix : mMixes) { // write mix route flags textDump += "* route flags=0x" + Integer.toHexString(mix.getRouteFlags()) + "\n"; @@ -161,6 +156,14 @@ public class AudioPolicyConfig implements Parcelable { textDump += " match usage "; textDump += criterion.mAttr.usageToString(); break; + case AudioMixingRule.RULE_EXCLUDE_ATTRIBUTE_CAPTURE_PRESET: + textDump += " exclude capture preset "; + textDump += criterion.mAttr.getCapturePreset(); + break; + case AudioMixingRule.RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET: + textDump += " match capture preset "; + textDump += criterion.mAttr.getCapturePreset(); + break; default: textDump += "invalid rule!"; } @@ -170,12 +173,32 @@ public class AudioPolicyConfig implements Parcelable { return textDump; } - public void setRegistration(String regId) { - mRegistrationId = regId; + protected void setRegistration(String regId) { + final boolean currentRegNull = (mRegistrationId == null) || mRegistrationId.isEmpty(); + final boolean newRegNull = (regId == null) || regId.isEmpty(); + if (!currentRegNull && !newRegNull && !mRegistrationId.equals(regId)) { + Log.e(TAG, "Invalid registration transition from " + mRegistrationId + " to " + regId); + return; + } + mRegistrationId = regId == null ? "" : regId; int mixIndex = 0; for (AudioMix mix : mMixes) { - mix.setRegistration(mRegistrationId + "mix:" + mixIndex++); + if (!mRegistrationId.isEmpty()) { + mix.setRegistration(mRegistrationId + "mix" + mixTypeId(mix.getMixType()) + ":" + + mixIndex++); + } else { + mix.setRegistration(""); + } } } + private static String mixTypeId(int type) { + if (type == AudioMix.MIX_TYPE_PLAYERS) return "p"; + else if (type == AudioMix.MIX_TYPE_RECORDERS) return "r"; + else return "i"; + } + + protected String getRegistration() { + return mRegistrationId; + } } |