AudioPolicy: support for add/remove AudioMix without unregistering
System API for a registered AudioPolicy to attach or detach
AudioMix without having to unregister, and then registering
the new mix configuration.
Bug: 63906162
Test: AudioPolicyTest
Change-Id: Ib2fea8aa034d3f7b498e76dc1fc51c1ea508d3a2
diff --git a/api/system-current.txt b/api/system-current.txt
index 2f5af0d..f9640ba 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -2649,8 +2649,10 @@
}
public class AudioPolicy {
+ method public int attachMixes(java.util.List<android.media.audiopolicy.AudioMix>);
method public android.media.AudioRecord createAudioRecordSink(android.media.audiopolicy.AudioMix) throws java.lang.IllegalArgumentException;
method public android.media.AudioTrack createAudioTrackSource(android.media.audiopolicy.AudioMix) throws java.lang.IllegalArgumentException;
+ method public int detachMixes(java.util.List<android.media.audiopolicy.AudioMix>);
method public int getFocusDuckingBehavior();
method public int getStatus();
method public int setFocusDuckingBehavior(int) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException;
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 74e7c45..4af8850 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -181,6 +181,10 @@
oneway void unregisterAudioPolicyAsync(in IAudioPolicyCallback pcb);
+ int addMixForPolicy(in AudioPolicyConfig policyConfig, in IAudioPolicyCallback pcb);
+
+ int removeMixForPolicy(in AudioPolicyConfig policyConfig, in IAudioPolicyCallback pcb);
+
int setFocusPropertiesForPolicy(int duckingBehavior, in IAudioPolicyCallback pcb);
void setVolumePolicy(in VolumePolicy policy);
diff --git a/media/java/android/media/audiopolicy/AudioMix.java b/media/java/android/media/audiopolicy/AudioMix.java
index adeb834..39cdcf0 100644
--- a/media/java/android/media/audiopolicy/AudioMix.java
+++ b/media/java/android/media/audiopolicy/AudioMix.java
@@ -163,6 +163,19 @@
/** @hide */
@Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ final AudioMix that = (AudioMix) o;
+ return (this.mRouteFlags == that.mRouteFlags)
+ && (this.mRule == that.mRule)
+ && (this.mMixType == that.mMixType)
+ && (this.mFormat == that.mFormat);
+ }
+
+ /** @hide */
+ @Override
public int hashCode() {
return Objects.hash(mRouteFlags, mRule, mMixType, mFormat);
}
diff --git a/media/java/android/media/audiopolicy/AudioMixingRule.java b/media/java/android/media/audiopolicy/AudioMixingRule.java
index 5f12742..866b574 100644
--- a/media/java/android/media/audiopolicy/AudioMixingRule.java
+++ b/media/java/android/media/audiopolicy/AudioMixingRule.java
@@ -135,11 +135,31 @@
}
}
+ private static boolean areCriteriaEquivalent(ArrayList<AudioMixMatchCriterion> cr1,
+ ArrayList<AudioMixMatchCriterion> cr2) {
+ if (cr1 == null || cr2 == null) return false;
+ if (cr1 == cr2) return true;
+ if (cr1.size() != cr2.size()) return false;
+ //TODO iterate over rules to check they contain the same criterion
+ return (cr1.hashCode() == cr2.hashCode());
+ }
+
private final int mTargetMixType;
int getTargetMixType() { return mTargetMixType; }
private final ArrayList<AudioMixMatchCriterion> mCriteria;
ArrayList<AudioMixMatchCriterion> getCriteria() { return mCriteria; }
+ /** @hide */
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ final AudioMixingRule that = (AudioMixingRule) o;
+ return (this.mTargetMixType == that.mTargetMixType)
+ && (areCriteriaEquivalent(this.mCriteria, that.mCriteria));
+ }
+
@Override
public int hashCode() {
return Objects.hash(mTargetMixType, mCriteria);
diff --git a/media/java/android/media/audiopolicy/AudioPolicy.java b/media/java/android/media/audiopolicy/AudioPolicy.java
index 343bbda..11107e2 100644
--- a/media/java/android/media/audiopolicy/AudioPolicy.java
+++ b/media/java/android/media/audiopolicy/AudioPolicy.java
@@ -42,6 +42,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
+import java.util.List;
/**
* @hide
@@ -256,6 +257,89 @@
}
}
+ /**
+ * @hide
+ * Update the current configuration of the set of audio mixes by adding new ones, while
+ * keeping the policy registered.
+ * This method can only be called on a registered policy.
+ * @param mixes the list of {@link AudioMix} to add
+ * @return {@link AudioManager#SUCCESS} if the change was successful, {@link AudioManager#ERROR}
+ * otherwise.
+ */
+ @SystemApi
+ public int attachMixes(@NonNull List<AudioMix> mixes) {
+ if (mixes == null) {
+ throw new IllegalArgumentException("Illegal null list of AudioMix");
+ }
+ synchronized (mLock) {
+ if (mStatus != POLICY_STATUS_REGISTERED) {
+ throw new IllegalStateException("Cannot alter unregistered AudioPolicy");
+ }
+ final ArrayList<AudioMix> zeMixes = new ArrayList<AudioMix>(mixes.size());
+ for (AudioMix mix : mixes) {
+ if (mix == null) {
+ throw new IllegalArgumentException("Illegal null AudioMix in attachMixes");
+ } else {
+ zeMixes.add(mix);
+ }
+ }
+ final AudioPolicyConfig cfg = new AudioPolicyConfig(zeMixes);
+ IAudioService service = getService();
+ try {
+ final int status = service.addMixForPolicy(cfg, this.cb());
+ if (status == AudioManager.SUCCESS) {
+ mConfig.add(zeMixes);
+ }
+ return status;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in attachMixes", e);
+ return AudioManager.ERROR;
+ }
+ }
+ }
+
+ /**
+ * @hide
+ * Update the current configuration of the set of audio mixes by removing some, while
+ * keeping the policy registered.
+ * This method can only be called on a registered policy.
+ * @param mixes the list of {@link AudioMix} to remove
+ * @return {@link AudioManager#SUCCESS} if the change was successful, {@link AudioManager#ERROR}
+ * otherwise.
+ */
+ @SystemApi
+ public int detachMixes(@NonNull List<AudioMix> mixes) {
+ if (mixes == null) {
+ throw new IllegalArgumentException("Illegal null list of AudioMix");
+ }
+ synchronized (mLock) {
+ if (mStatus != POLICY_STATUS_REGISTERED) {
+ throw new IllegalStateException("Cannot alter unregistered AudioPolicy");
+ }
+ final ArrayList<AudioMix> zeMixes = new ArrayList<AudioMix>(mixes.size());
+ for (AudioMix mix : mixes) {
+ if (mix == null) {
+ throw new IllegalArgumentException("Illegal null AudioMix in detachMixes");
+ // TODO also check mix is currently contained in list of mixes
+ } else {
+ zeMixes.add(mix);
+ }
+ }
+ final AudioPolicyConfig cfg = new AudioPolicyConfig(zeMixes);
+ IAudioService service = getService();
+ try {
+ final int status = service.removeMixForPolicy(cfg, this.cb());
+ if (status == AudioManager.SUCCESS) {
+ mConfig.remove(zeMixes);
+ }
+ return status;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Dead object in detachMixes", e);
+ return AudioManager.ERROR;
+ }
+ }
+ }
+
public void setRegistration(String regId) {
synchronized (mLock) {
mRegistrationId = regId;
diff --git a/media/java/android/media/audiopolicy/AudioPolicyConfig.java b/media/java/android/media/audiopolicy/AudioPolicyConfig.java
index cafa5a8..f725cac 100644
--- a/media/java/android/media/audiopolicy/AudioPolicyConfig.java
+++ b/media/java/android/media/audiopolicy/AudioPolicyConfig.java
@@ -16,6 +16,7 @@
package android.media.audiopolicy;
+import android.annotation.NonNull;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioPatch;
@@ -24,6 +25,8 @@
import android.os.Parcelable;
import android.util.Log;
+import com.android.internal.annotations.GuardedBy;
+
import java.util.ArrayList;
import java.util.Objects;
@@ -35,11 +38,16 @@
private static final String TAG = "AudioPolicyConfig";
- protected ArrayList<AudioMix> mMixes;
+ protected final ArrayList<AudioMix> mMixes;
protected int mDuckingPolicy = AudioPolicy.FOCUS_POLICY_DUCKING_IN_APP;
private String mRegistrationId = null;
+ /** counter for the mixes that are / have been in the list of AudioMix
+ * e.g. register 4 mixes (counter is 3), remove 1 (counter is 3), add 1 (counter is 4)
+ */
+ private int mMixCounter = 0;
+
protected AudioPolicyConfig(AudioPolicyConfig conf) {
mMixes = conf.mMixes;
}
@@ -201,20 +209,39 @@
return;
}
mRegistrationId = regId == null ? "" : regId;
- int mixIndex = 0;
for (AudioMix mix : mMixes) {
- if (!mRegistrationId.isEmpty()) {
- if ((mix.getRouteFlags() & AudioMix.ROUTE_FLAG_LOOP_BACK) ==
- AudioMix.ROUTE_FLAG_LOOP_BACK) {
- mix.setRegistration(mRegistrationId + "mix" + mixTypeId(mix.getMixType()) + ":"
- + mixIndex++);
- } else if ((mix.getRouteFlags() & AudioMix.ROUTE_FLAG_RENDER) ==
- AudioMix.ROUTE_FLAG_RENDER) {
- mix.setRegistration(mix.mDeviceAddress);
- }
- } else {
- mix.setRegistration("");
+ setMixRegistration(mix);
+ }
+ }
+
+ private void setMixRegistration(@NonNull final AudioMix mix) {
+ if (!mRegistrationId.isEmpty()) {
+ if ((mix.getRouteFlags() & AudioMix.ROUTE_FLAG_LOOP_BACK) ==
+ AudioMix.ROUTE_FLAG_LOOP_BACK) {
+ mix.setRegistration(mRegistrationId + "mix" + mixTypeId(mix.getMixType()) + ":"
+ + mMixCounter);
+ } else if ((mix.getRouteFlags() & AudioMix.ROUTE_FLAG_RENDER) ==
+ AudioMix.ROUTE_FLAG_RENDER) {
+ mix.setRegistration(mix.mDeviceAddress);
}
+ } else {
+ mix.setRegistration("");
+ }
+ mMixCounter++;
+ }
+
+ @GuardedBy("mMixes")
+ protected void add(@NonNull ArrayList<AudioMix> mixes) {
+ for (AudioMix mix : mixes) {
+ setMixRegistration(mix);
+ mMixes.add(mix);
+ }
+ }
+
+ @GuardedBy("mMixes")
+ protected void remove(@NonNull ArrayList<AudioMix> mixes) {
+ for (AudioMix mix : mixes) {
+ mMixes.remove(mix);
}
}
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index ca22820..8eb8058 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -7039,26 +7039,75 @@
// TODO implement clearing mix attribute matching info in native audio policy
}
- public int setFocusPropertiesForPolicy(int duckingBehavior, IAudioPolicyCallback pcb) {
- if (DEBUG_AP) Log.d(TAG, "setFocusPropertiesForPolicy() duck behavior=" + duckingBehavior
- + " policy " + pcb.asBinder());
- // error handling
- boolean hasPermissionForPolicy =
+ /**
+ * Checks whether caller has MODIFY_AUDIO_ROUTING permission, and the policy is registered.
+ * @param errorMsg log warning if permission check failed.
+ * @return null if the operation on the audio mixes should be cancelled.
+ */
+ @GuardedBy("mAudioPolicies")
+ private AudioPolicyProxy checkUpdateForPolicy(IAudioPolicyCallback pcb, String errorMsg) {
+ // permission check
+ final boolean hasPermissionForPolicy =
(PackageManager.PERMISSION_GRANTED == mContext.checkCallingPermission(
android.Manifest.permission.MODIFY_AUDIO_ROUTING));
if (!hasPermissionForPolicy) {
- Slog.w(TAG, "Cannot change audio policy ducking handling for pid " +
+ Slog.w(TAG, errorMsg + " for pid " +
+ Binder.getCallingPid() + " / uid "
+ Binder.getCallingUid() + ", need MODIFY_AUDIO_ROUTING");
- return AudioManager.ERROR;
+ return null;
}
+ // policy registered?
+ final AudioPolicyProxy app = mAudioPolicies.get(pcb.asBinder());
+ if (app == null) {
+ Slog.w(TAG, errorMsg + " for pid " +
+ + Binder.getCallingPid() + " / uid "
+ + Binder.getCallingUid() + ", unregistered policy");
+ return null;
+ }
+ return app;
+ }
+ public int addMixForPolicy(AudioPolicyConfig policyConfig, IAudioPolicyCallback pcb) {
+ if (DEBUG_AP) { Log.d(TAG, "addMixForPolicy for " + pcb.asBinder()
+ + " with config:" + policyConfig); }
synchronized (mAudioPolicies) {
+ final AudioPolicyProxy app =
+ checkUpdateForPolicy(pcb, "Cannot add AudioMix in audio policy");
+ if (app == null){
+ return AudioManager.ERROR;
+ }
+ app.addMixes(policyConfig.getMixes());
+ }
+ return AudioManager.SUCCESS;
+ }
+
+ public int removeMixForPolicy(AudioPolicyConfig policyConfig, IAudioPolicyCallback pcb) {
+ if (DEBUG_AP) { Log.d(TAG, "removeMixForPolicy for " + pcb.asBinder()
+ + " with config:" + policyConfig); }
+ synchronized (mAudioPolicies) {
+ final AudioPolicyProxy app =
+ checkUpdateForPolicy(pcb, "Cannot add AudioMix in audio policy");
+ if (app == null) {
+ return AudioManager.ERROR;
+ }
+ app.removeMixes(policyConfig.getMixes());
+ }
+ return AudioManager.SUCCESS;
+ }
+
+ public int setFocusPropertiesForPolicy(int duckingBehavior, IAudioPolicyCallback pcb) {
+ if (DEBUG_AP) Log.d(TAG, "setFocusPropertiesForPolicy() duck behavior=" + duckingBehavior
+ + " policy " + pcb.asBinder());
+ synchronized (mAudioPolicies) {
+ final AudioPolicyProxy app =
+ checkUpdateForPolicy(pcb, "Cannot change audio policy focus properties");
+ if (app == null){
+ return AudioManager.ERROR;
+ }
if (!mAudioPolicies.containsKey(pcb.asBinder())) {
Slog.e(TAG, "Cannot change audio policy focus properties, unregistered policy");
return AudioManager.ERROR;
}
- final AudioPolicyProxy app = mAudioPolicies.get(pcb.asBinder());
if (duckingBehavior == AudioPolicy.FOCUS_POLICY_DUCKING_IN_POLICY) {
// is there already one policy managing ducking?
for (AudioPolicyProxy policy : mAudioPolicies.values()) {
@@ -7290,6 +7339,24 @@
Binder.restoreCallingIdentity(identity);
}
+ void addMixes(@NonNull ArrayList<AudioMix> mixes) {
+ // TODO optimize to not have to unregister the mixes already in place
+ synchronized (mMixes) {
+ AudioSystem.registerPolicyMixes(mMixes, false);
+ this.add(mixes);
+ AudioSystem.registerPolicyMixes(mMixes, true);
+ }
+ }
+
+ void removeMixes(@NonNull ArrayList<AudioMix> mixes) {
+ // TODO optimize to not have to unregister the mixes already in place
+ synchronized (mMixes) {
+ AudioSystem.registerPolicyMixes(mMixes, false);
+ this.remove(mixes);
+ AudioSystem.registerPolicyMixes(mMixes, true);
+ }
+ }
+
void connectMixes() {
final long identity = Binder.clearCallingIdentity();
AudioSystem.registerPolicyMixes(mMixes, true);
@@ -7407,7 +7474,8 @@
//======================
// misc
//======================
- private HashMap<IBinder, AudioPolicyProxy> mAudioPolicies =
+ private final HashMap<IBinder, AudioPolicyProxy> mAudioPolicies =
new HashMap<IBinder, AudioPolicyProxy>();
- private int mAudioPolicyCounter = 0; // always accessed synchronized on mAudioPolicies
+ @GuardedBy("mAudioPolicies")
+ private int mAudioPolicyCounter = 0;
}