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;
 }