diff --git a/media/java/android/media/AudioAttributes.java b/media/java/android/media/AudioAttributes.java index 17d3251b3a6f9..20c4978e779b7 100644 --- a/media/java/android/media/AudioAttributes.java +++ b/media/java/android/media/AudioAttributes.java @@ -161,6 +161,12 @@ public final class AudioAttributes implements Parcelable { * Usage value to use when the usage is for game audio. */ public final static int USAGE_GAME = 14; + /** + * @hide + * Usage value to use when feeding audio to the platform and replacing "traditional" audio + * source, such as audio capture devices. + */ + public final static int USAGE_VIRTUAL_SOURCE = 15; /** * Flag defining a behavior where the audibility of the sound will be ensured by the system. @@ -374,6 +380,7 @@ public final class AudioAttributes implements Parcelable { case USAGE_ASSISTANCE_NAVIGATION_GUIDANCE: case USAGE_ASSISTANCE_SONIFICATION: case USAGE_GAME: + case USAGE_VIRTUAL_SOURCE: mUsage = usage; break; default: diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 716ff992c008e..8fc0b8e4a86db 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -2663,9 +2663,13 @@ public class AudioManager { } IAudioService service = getService(); try { - if (!service.registerAudioPolicy(policy.getConfig(), policy.token())) { + String regId = service.registerAudioPolicy(policy.getConfig(), policy.token()); + if (regId == null) { return ERROR; + } else { + policy.setRegistration(regId); } + // successful registration } catch (RemoteException e) { Log.e(TAG, "Dead object in registerAudioPolicyAsync()", e); return ERROR; diff --git a/media/java/android/media/AudioService.java b/media/java/android/media/AudioService.java index 6a695170f3c0d..2f683828d3b13 100644 --- a/media/java/android/media/AudioService.java +++ b/media/java/android/media/AudioService.java @@ -48,6 +48,7 @@ import android.hardware.hdmi.HdmiTvClient; import android.hardware.usb.UsbManager; import android.media.MediaPlayer.OnCompletionListener; import android.media.MediaPlayer.OnErrorListener; +import android.media.audiopolicy.AudioMix; import android.media.audiopolicy.AudioPolicyConfig; import android.media.session.MediaSessionLegacyHelper; import android.os.Binder; @@ -118,6 +119,10 @@ public class AudioService extends IAudioService.Stub { /** Debug audio mode */ protected static final boolean DEBUG_MODE = Log.isLoggable(TAG + ".MOD", Log.DEBUG); + + /** Debug audio policy feature */ + protected static final boolean DEBUG_AP = Log.isLoggable(TAG + ".AP", Log.DEBUG); + /** Debug volumes */ protected static final boolean DEBUG_VOL = Log.isLoggable(TAG + ".VOL", Log.DEBUG); @@ -5634,31 +5639,33 @@ public class AudioService extends IAudioService.Stub { //========================================================================================== // Audio policy management //========================================================================================== - public boolean registerAudioPolicy(AudioPolicyConfig policyConfig, IBinder cb) { + public String registerAudioPolicy(AudioPolicyConfig policyConfig, IBinder cb) { //Log.v(TAG, "registerAudioPolicy for " + cb + " got policy:" + policyConfig); + String regId = null; boolean hasPermissionForPolicy = (PackageManager.PERMISSION_GRANTED == mContext.checkCallingOrSelfPermission( android.Manifest.permission.MODIFY_AUDIO_ROUTING)); if (!hasPermissionForPolicy) { Slog.w(TAG, "Can't register audio policy for pid " + Binder.getCallingPid() + " / uid " + Binder.getCallingUid() + ", need MODIFY_AUDIO_ROUTING"); - return false; + return null; } synchronized (mAudioPolicies) { - AudioPolicyProxy app = new AudioPolicyProxy(policyConfig, cb); try { + AudioPolicyProxy app = new AudioPolicyProxy(policyConfig, cb); cb.linkToDeath(app, 0/*flags*/); + regId = app.connectMixes(); mAudioPolicies.put(cb, app); } catch (RemoteException e) { // audio policy owner has already died! Slog.w(TAG, "Audio policy registration failed, could not link to " + cb + " binder death", e); - return false; + return null; } } - // TODO implement registration with native audio policy (including permission check) - return true; + return regId; } + public void unregisterAudioPolicyAsync(IBinder cb) { synchronized (mAudioPolicies) { AudioPolicyProxy app = mAudioPolicies.remove(cb); @@ -5668,27 +5675,59 @@ public class AudioService extends IAudioService.Stub { } else { cb.unlinkToDeath(app, 0/*flags*/); } + app.disconnectMixes(); } - // TODO implement registration with native audio policy + // TODO implement clearing mix attribute matching info in native audio policy } - public class AudioPolicyProxy implements IBinder.DeathRecipient { + /** + * This internal class inherits from AudioPolicyConfig which contains all the mixes and + * their configurations. + */ + public class AudioPolicyProxy extends AudioPolicyConfig implements IBinder.DeathRecipient { private static final String TAG = "AudioPolicyProxy"; AudioPolicyConfig mConfig; IBinder mToken; AudioPolicyProxy(AudioPolicyConfig config, IBinder token) { - mConfig = config; + super(config); + setRegistration(new String(config.toString() + ":ap:" + mAudioPolicyCounter++)); mToken = token; } public void binderDied() { synchronized (mAudioPolicies) { - Log.v(TAG, "audio policy " + mToken + " died"); + Log.i(TAG, "audio policy " + mToken + " died"); mAudioPolicies.remove(mToken); + disconnectMixes(); + } + } + + String connectMixes() { + updateMixes(AudioSystem.DEVICE_STATE_AVAILABLE); + return mRegistrationId; + } + + void disconnectMixes() { + updateMixes(AudioSystem.DEVICE_STATE_UNAVAILABLE); + } + + void updateMixes(int connectionState) { + 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()); } + AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_IN_REMOTE_SUBMIX, + connectionState, + mix.getRegistration()); + AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_OUT_REMOTE_SUBMIX, + connectionState, + mix.getRegistration()); } } }; private HashMap mAudioPolicies = new HashMap(); + private int mAudioPolicyCounter = 0; // always accessed synchronized on mAudioPolicies } diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 2d8042cb367e2..317cc212c4635 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -207,6 +207,6 @@ interface IAudioService { boolean isHdmiSystemAudioSupported(); - boolean registerAudioPolicy(in AudioPolicyConfig policyConfig, IBinder cb); + String registerAudioPolicy(in AudioPolicyConfig policyConfig, IBinder cb); oneway void unregisterAudioPolicyAsync(in IBinder cb); } diff --git a/media/java/android/media/audiopolicy/AudioMix.java b/media/java/android/media/audiopolicy/AudioMix.java index f7967f1f7d7b4..bb5268261d78c 100644 --- a/media/java/android/media/audiopolicy/AudioMix.java +++ b/media/java/android/media/audiopolicy/AudioMix.java @@ -24,13 +24,14 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** - * @hide CANDIDATE FOR PUBLIC API + * @hide */ public class AudioMix { private AudioMixingRule mRule; private AudioFormat mFormat; private int mRouteFlags; + private String mRegistrationId; /** * All parameters are guaranteed valid through the Builder. @@ -39,6 +40,7 @@ public class AudioMix { mRule = rule; mFormat = format; mRouteFlags = routeFlags; + mRegistrationId = null; } /** @@ -65,6 +67,15 @@ public class AudioMix { return mRule; } + void setRegistration(String regId) { + mRegistrationId = regId; + } + + /** @hide */ + public String getRegistration() { + return mRegistrationId; + } + /** @hide */ @IntDef(flag = true, value = { ROUTE_FLAG_RENDER, ROUTE_FLAG_LOOP_BACK } ) diff --git a/media/java/android/media/audiopolicy/AudioMixingRule.java b/media/java/android/media/audiopolicy/AudioMixingRule.java index ced78817a9929..2e06a807624f5 100644 --- a/media/java/android/media/audiopolicy/AudioMixingRule.java +++ b/media/java/android/media/audiopolicy/AudioMixingRule.java @@ -23,7 +23,7 @@ import java.util.Iterator; /** - * @hide CANDIDATE FOR PUBLIC API + * @hide * * Here's an example of creating a mixing rule for all media playback: *
diff --git a/media/java/android/media/audiopolicy/AudioPolicy.java b/media/java/android/media/audiopolicy/AudioPolicy.java
index 314eb887d1d11..255d828b4bfab 100644
--- a/media/java/android/media/audiopolicy/AudioPolicy.java
+++ b/media/java/android/media/audiopolicy/AudioPolicy.java
@@ -17,18 +17,26 @@
 package android.media.audiopolicy;
 
 import android.annotation.IntDef;
+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.IBinder;
 import android.util.Log;
+import android.util.Slog;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 
 /**
- * @hide CANDIDATE FOR PUBLIC API
+ * @hide
  * AudioPolicy provides access to the management of audio routing and audio focus.
  */
 public class AudioPolicy {
@@ -49,11 +57,13 @@ public class AudioPolicy {
     public static final int POLICY_STATUS_REGISTERED = 2;
 
     private int mStatus;
-    private AudioPolicyStatusListener mStatusListener = null;
+    private String mRegistrationId;
+    private AudioPolicyStatusListener mStatusListener;
 
     private final IBinder mToken = new Binder();
     /** @hide */
     public IBinder token() { return mToken; }
+    private Context mContext;
 
     private AudioPolicyConfig mConfig;
     /** @hide */
@@ -62,13 +72,14 @@ public class AudioPolicy {
     /**
      * The parameter is guaranteed non-null through the Builder
      */
-    private AudioPolicy(AudioPolicyConfig config) {
+    private AudioPolicy(AudioPolicyConfig config, Context context) {
         mConfig = config;
         if (mConfig.mMixes.isEmpty()) {
             mStatus = POLICY_STATUS_INVALID;
         } else {
             mStatus = POLICY_STATUS_UNREGISTERED;
         }
+        mContext = context;
     }
 
     /**
@@ -76,12 +87,15 @@ public class AudioPolicy {
      */
     public static class Builder {
         private ArrayList mMixes;
+        private Context mContext;
 
         /**
          * Constructs a new Builder with no audio mixes.
+         * @param context the context for the policy
          */
-        public Builder() {
+        public Builder(Context context) {
             mMixes = new ArrayList();
+            mContext = context;
         }
 
         /**
@@ -99,10 +113,115 @@ public class AudioPolicy {
         }
 
         public AudioPolicy build() {
-            return new AudioPolicy(new AudioPolicyConfig(mMixes));
+            return new AudioPolicy(new AudioPolicyConfig(mMixes), mContext);
         }
     }
 
+    /** @hide */
+    public void setRegistration(String regId) {
+        mRegistrationId = regId;
+        mConfig.setRegistration(regId);
+    }
+
+    private boolean policyReadyToUse() {
+        if (mContext == null) {
+            Log.e(TAG, "Cannot use AudioPolicy without context");
+            return false;
+        }
+        if (mRegistrationId == null) {
+            Log.e(TAG, "Cannot use unregistered AudioPolicy");
+            return false;
+        }
+        if (!(PackageManager.PERMISSION_GRANTED == mContext.checkCallingOrSelfPermission(
+                        android.Manifest.permission.MODIFY_AUDIO_ROUTING))) {
+            Slog.w(TAG, "Cannot use AudioPolicy for pid " + Binder.getCallingPid() + " / uid "
+                    + Binder.getCallingUid() + ", needs MODIFY_AUDIO_ROUTING");
+            return false;
+        }
+        return true;
+    }
+
+    private void checkMixReadyToUse(AudioMix mix, boolean forTrack)
+            throws IllegalArgumentException{
+        if (mix == null) {
+            String msg = forTrack ? "Invalid null AudioMix for AudioTrack creation"
+                    : "Invalid null AudioMix for AudioRecord creation";
+            throw new IllegalArgumentException(msg);
+        }
+        if (!mConfig.mMixes.contains(mix)) {
+            throw new IllegalArgumentException("Invalid mix: not part of this policy");
+        }
+        if ((mix.getRouteFlags() & AudioMix.ROUTE_FLAG_LOOP_BACK) != AudioMix.ROUTE_FLAG_LOOP_BACK)
+        {
+            throw new IllegalArgumentException("Invalid AudioMix: not defined for loop back");
+        }
+    }
+
+    /**
+     * @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.
+     * @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.
+     * @return a new {@link AudioRecord} 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
+     */
+    public AudioRecord createAudioRecordSink(AudioMix mix) throws IllegalArgumentException {
+        if (!policyReadyToUse()) {
+            Log.e(TAG, "Cannot create AudioRecord sink for AudioMix");
+            return null;
+        }
+        checkMixReadyToUse(mix, false/*not for an AudioTrack*/);
+        // create the AudioRecord, configured for loop back, using the same format as the mix
+        AudioRecord ar = new AudioRecord(
+                new AudioAttributes.Builder()
+                        .setInternalCapturePreset(MediaRecorder.AudioSource.REMOTE_SUBMIX)
+                        .addTag(mix.getRegistration())
+                        .build(),
+                mix.getFormat(),
+                AudioRecord.getMinBufferSize(mix.getFormat().getSampleRate(),
+                        // using stereo for buffer size to avoid the current poor support for masks
+                        AudioFormat.CHANNEL_IN_STEREO, mix.getFormat().getEncoding()),
+                AudioManager.AUDIO_SESSION_ID_GENERATE
+                );
+        return ar;
+    }
+
+    /**
+     * @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
+     *     {@link AudioMix}, or null if this policy was not successfully registered
+     *     with {@link AudioManager#registerAudioPolicy(AudioPolicy)}.
+     * @throws IllegalArgumentException
+     */
+    public AudioTrack createAudioTrackSource(AudioMix mix) throws IllegalArgumentException {
+        if (!policyReadyToUse()) {
+            Log.e(TAG, "Cannot create AudioTrack source for AudioMix");
+            return null;
+        }
+        checkMixReadyToUse(mix, true/*for an AudioTrack*/);
+        // create the AudioTrack, configured for loop back, using the same format as the mix
+        AudioTrack at = new AudioTrack(
+                new AudioAttributes.Builder()
+                        .setUsage(AudioAttributes.USAGE_VIRTUAL_SOURCE)
+                        .addTag(mix.getRegistration())
+                        .build(),
+                mix.getFormat(),
+                AudioTrack.getMinBufferSize(mix.getFormat().getSampleRate(),
+                        mix.getFormat().getChannelMask(), mix.getFormat().getEncoding()),
+                AudioTrack.MODE_STREAM,
+                AudioManager.AUDIO_SESSION_ID_GENERATE
+                );
+        return at;
+    }
 
     public int getStatus() {
         return mStatus;
@@ -118,10 +237,9 @@ public class AudioPolicy {
     }
 
     /** @hide */
-    @Override
-    public String toString () {
+    public String toLogFriendlyString() {
         String textDump = new String("android.media.audiopolicy.AudioPolicy:\n");
-        textDump += "config=" + mConfig.toString();
+        textDump += "config=" + mConfig.toLogFriendlyString();
         return (textDump);
     }
 
diff --git a/media/java/android/media/audiopolicy/AudioPolicyConfig.java b/media/java/android/media/audiopolicy/AudioPolicyConfig.java
index 2fc6d58b2e61f..a9a4175916cea 100644
--- a/media/java/android/media/audiopolicy/AudioPolicyConfig.java
+++ b/media/java/android/media/audiopolicy/AudioPolicyConfig.java
@@ -36,7 +36,13 @@ public class AudioPolicyConfig implements Parcelable {
 
     private static final String TAG = "AudioPolicyConfig";
 
-    ArrayList mMixes;
+    protected ArrayList mMixes;
+
+    protected String mRegistrationId = null;
+
+    protected AudioPolicyConfig(AudioPolicyConfig conf) {
+        mMixes = conf.mMixes;
+    }
 
     AudioPolicyConfig(ArrayList mixes) {
         mMixes = mixes;
@@ -117,7 +123,6 @@ public class AudioPolicyConfig implements Parcelable {
         }
     }
 
-    /** @hide */
     public static final Parcelable.Creator CREATOR
             = new Parcelable.Creator() {
         /**
@@ -133,9 +138,7 @@ public class AudioPolicyConfig implements Parcelable {
         }
     };
 
-    /** @hide */
-    @Override
-    public String toString () {
+    public String toLogFriendlyString () {
         String textDump = new String("android.media.audiopolicy.AudioPolicyConfig:\n");
         textDump += mMixes.size() + " AudioMix:\n";
         for(AudioMix mix : mMixes) {
@@ -166,4 +169,13 @@ public class AudioPolicyConfig implements Parcelable {
         }
         return textDump;
     }
+
+    public void setRegistration(String regId) {
+        mRegistrationId = regId;
+        int mixIndex = 0;
+        for (AudioMix mix : mMixes) {
+            mix.setRegistration(mRegistrationId + "mix:" + mixIndex++);
+        }
+    }
+
 }