AudioService: Track client app recording activity

Update RecordActivityMonitor to use events sent from clients
about their recording activity and coalesce them with
configuration update events sent from audioserver (APM). This
allows providing a high-level view on the recording
configurations consistent with client expectations.

For recorders that are managed entrirely on the server side
(AAudio MMAP) all events do contain configs. These recorders
do not have an associated death handler and need to be cleared
on audioserver restart.

Clearing client-managed trackers on audioserver restart is not
needed anymore. Clients will either continue
recording (AudioRecord handles IAudioRecord re-instantation) or
stop recording and notify AudioService about that.

Bug: 123312504
Test: android.media.cts.AudioRecordingConfigurationTest
      AudioRecordTest#testAudioRecordInfoCallback
      MediaRecorderTest#testAudioRecordInfoCallback
      manual testing using Oboe and Solo test apps

Change-Id: I4718f983c2d90e9c3e7480719a24bd187df7f80a
diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp
index a296d64..3a25e67 100644
--- a/core/jni/android_media_AudioSystem.cpp
+++ b/core/jni/android_media_AudioSystem.cpp
@@ -26,6 +26,7 @@
 #include <nativehelper/JNIHelp.h>
 #include "core_jni_helpers.h"
 
+#include <audiomanager/AudioManager.h>
 #include <media/AudioSystem.h>
 #include <media/AudioPolicy.h>
 #include <media/MicrophoneInfo.h>
@@ -353,7 +354,15 @@
 static jint
 android_media_AudioSystem_newAudioPlayerId(JNIEnv *env, jobject thiz)
 {
-    return AudioSystem::newAudioUniqueId(AUDIO_UNIQUE_ID_USE_PLAYER);
+    int id = AudioSystem::newAudioUniqueId(AUDIO_UNIQUE_ID_USE_CLIENT);
+    return id != AUDIO_UNIQUE_ID_ALLOCATE ? id : PLAYER_PIID_INVALID;
+}
+
+static jint
+android_media_AudioSystem_newAudioRecorderId(JNIEnv *env, jobject thiz)
+{
+    int id = AudioSystem::newAudioUniqueId(AUDIO_UNIQUE_ID_USE_CLIENT);
+    return id != AUDIO_UNIQUE_ID_ALLOCATE ? id : RECORD_RIID_INVALID;
 }
 
 static jint
@@ -470,9 +479,10 @@
 
     env->CallStaticVoidMethod(clazz,
                               gAudioPolicyEventHandlerMethods.postRecordConfigEventFromNative,
-                              event, (jint) clientInfo->uid, clientInfo->session,
-                              clientInfo->source, clientInfo->port_id, clientInfo->silenced,
-                              recParamArray, jClientEffects, jEffects, source);
+                              event, (jint) clientInfo->riid, (jint) clientInfo->uid,
+                              clientInfo->session, clientInfo->source, clientInfo->port_id,
+                              clientInfo->silenced, recParamArray, jClientEffects, jEffects,
+                              source);
     env->DeleteLocalRef(clazz);
     env->DeleteLocalRef(recParamArray);
     env->DeleteLocalRef(jClientEffects);
@@ -2246,6 +2256,7 @@
     {"isSourceActive",      "(I)Z",     (void *)android_media_AudioSystem_isSourceActive},
     {"newAudioSessionId",   "()I",      (void *)android_media_AudioSystem_newAudioSessionId},
     {"newAudioPlayerId",    "()I",      (void *)android_media_AudioSystem_newAudioPlayerId},
+    {"newAudioRecorderId",  "()I",      (void *)android_media_AudioSystem_newAudioRecorderId},
     {"setDeviceConnectionState", "(IILjava/lang/String;Ljava/lang/String;I)I", (void *)android_media_AudioSystem_setDeviceConnectionState},
     {"getDeviceConnectionState", "(ILjava/lang/String;)I",  (void *)android_media_AudioSystem_getDeviceConnectionState},
     {"handleDeviceConfigChange", "(ILjava/lang/String;Ljava/lang/String;I)I", (void *)android_media_AudioSystem_handleDeviceConfigChange},
@@ -2440,7 +2451,7 @@
                     "dynamicPolicyCallbackFromNative", "(ILjava/lang/String;I)V");
     gAudioPolicyEventHandlerMethods.postRecordConfigEventFromNative =
             GetStaticMethodIDOrDie(env, env->FindClass(kClassPathName),
-                    "recordingCallbackFromNative", "(IIIIIZ[I[Landroid/media/audiofx/AudioEffect$Descriptor;[Landroid/media/audiofx/AudioEffect$Descriptor;I)V");
+                    "recordingCallbackFromNative", "(IIIIIIZ[I[Landroid/media/audiofx/AudioEffect$Descriptor;[Landroid/media/audiofx/AudioEffect$Descriptor;I)V");
 
     jclass audioMixClass = FindClassOrDie(env, "android/media/audiopolicy/AudioMix");
     gAudioMixClass = MakeGlobalRefOrDie(env, audioMixClass);
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index f8e43437..bcc57d2 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -3766,9 +3766,24 @@
      * with frameworks/av/include/media/AudioPolicy.h
      */
     /** @hide */
-    public final static int RECORD_CONFIG_EVENT_START = 1;
+    public static final int RECORD_CONFIG_EVENT_NONE = -1;
     /** @hide */
-    public final static int RECORD_CONFIG_EVENT_STOP = 0;
+    public static final int RECORD_CONFIG_EVENT_START = 0;
+    /** @hide */
+    public static final int RECORD_CONFIG_EVENT_STOP = 1;
+    /** @hide */
+    public static final int RECORD_CONFIG_EVENT_UPDATE = 2;
+    /** @hide */
+    public static final int RECORD_CONFIG_EVENT_DEATH = 3;
+    /**
+     * keep in sync with frameworks/native/include/audiomanager/AudioManager.h
+     */
+    /** @hide */
+    public static final int RECORD_RIID_INVALID = -1;
+    /** @hide */
+    public static final int RECORDER_STATE_STARTED = 0;
+    /** @hide */
+    public static final int RECORDER_STATE_STOPPED = 1;
 
     /**
      * All operations on this list are sync'd on mRecordCallbackLock.
diff --git a/media/java/android/media/AudioRecordingConfiguration.java b/media/java/android/media/AudioRecordingConfiguration.java
index 82bcbc1..74e6618 100644
--- a/media/java/android/media/AudioRecordingConfiguration.java
+++ b/media/java/android/media/AudioRecordingConfiguration.java
@@ -157,7 +157,7 @@
         return new AudioRecordingConfiguration( /*anonymized uid*/ -1,
                 in.mClientSessionId, in.mClientSource, in.mClientFormat,
                 in.mDeviceFormat, in.mPatchHandle, "" /*empty package name*/,
-                in.mClientPortId, in.mClientSilenced, in.mDeviceSource, in.mClientEffects,
+                /*anonymized portId*/ -1, in.mClientSilenced, in.mDeviceSource, in.mClientEffects,
                 in.mDeviceEffects);
     }
 
@@ -270,11 +270,12 @@
     }
 
     /**
+     * @hide
      * Returns the system unique ID assigned for the AudioRecord object corresponding to this
      * AudioRecordingConfiguration client.
      * @return the port ID.
      */
-    int getClientPortId() {
+    public int getClientPortId() {
         return mClientPortId;
     }
 
diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index d105fa3..987db8b 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -220,6 +220,11 @@
      */
     public static native int newAudioPlayerId();
 
+    /**
+     * Returns a new unused audio recorder ID
+     */
+    public static native int newAudioRecorderId();
+
 
     /*
      * Sets a group generic audio configuration parameters. The use of these parameters
@@ -347,6 +352,7 @@
         /**
          * Callback for recording activity notifications events
          * @param event
+         * @param riid recording identifier
          * @param uid uid of the client app performing the recording
          * @param session
          * @param source
@@ -361,7 +367,7 @@
          *          6: patch handle
          * @param packName package name of the client app performing the recording. NOT SUPPORTED
          */
-        void onRecordingConfigurationChanged(int event, int uid, int session, int source,
+        void onRecordingConfigurationChanged(int event, int riid, int uid, int session, int source,
                         int portId, boolean silenced, int[] recordingFormat,
                         AudioEffect.Descriptor[] clienteffects, AudioEffect.Descriptor[] effects,
                         int activeSource, String packName);
@@ -379,16 +385,23 @@
     /**
      * Callback from native for recording configuration updates.
      * @param event
+     * @param riid
+     * @param uid
      * @param session
      * @param source
+     * @param portId
+     * @param silenced
      * @param recordingFormat see
-     *     {@link AudioRecordingCallback#onRecordingConfigurationChanged(int, int, int, int, int,\
-     boolean, int[], AudioEffect.Descriptor[], AudioEffect.Descriptor[], int, String)}
+     *     {@link AudioRecordingCallback#onRecordingConfigurationChanged(int, int, int, int, int, \
+     int, boolean, int[], AudioEffect.Descriptor[], AudioEffect.Descriptor[], int, String)}
      *     for the description of the record format.
+     * @param cleintEffects
+     * @param effects
+     * @param activeSource
      */
     @UnsupportedAppUsage
-    private static void recordingCallbackFromNative(int event, int uid, int session, int source,
-                          int portId, boolean silenced, int[] recordingFormat,
+    private static void recordingCallbackFromNative(int event, int riid, int uid, int session,
+                          int source, int portId, boolean silenced, int[] recordingFormat,
                           AudioEffect.Descriptor[] clientEffects, AudioEffect.Descriptor[] effects,
                           int activeSource) {
         AudioRecordingCallback cb = null;
@@ -401,7 +414,7 @@
 
         if (cb != null) {
             // TODO receive package name from native
-            cb.onRecordingConfigurationChanged(event, uid, session, source, portId, silenced,
+            cb.onRecordingConfigurationChanged(event, riid, uid, session, source, portId, silenced,
                                         recordingFormat, clientEffects, effects, activeSource, "");
         }
     }
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 85de00a..81ddcdb4 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -60,6 +60,10 @@
 
     oneway void releasePlayer(in int piid);
 
+    int trackRecorder(in IBinder recorder);
+
+    oneway void recorderEvent(in int riid, in int event);
+
     // Java-only methods below.
 
     oneway void adjustSuggestedStreamVolume(int direction, int suggestedStreamType, int flags,
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index d3af8f07..e38defd 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -409,16 +409,16 @@
     private final AudioSystem.ErrorCallback mAudioSystemCallback = new AudioSystem.ErrorCallback() {
         public void onError(int error) {
             switch (error) {
-            case AudioSystem.AUDIO_STATUS_SERVER_DIED:
-                mRecordMonitor.clear();
+                case AudioSystem.AUDIO_STATUS_SERVER_DIED:
+                    mRecordMonitor.onAudioServerDied();
 
-                sendMsg(mAudioHandler, MSG_AUDIO_SERVER_DIED,
-                        SENDMSG_NOOP, 0, 0, null, 0);
-                sendMsg(mAudioHandler, MSG_DISPATCH_AUDIO_SERVER_STATE,
-                        SENDMSG_QUEUE, 0, 0, null, 0);
-                break;
-            default:
-                break;
+                    sendMsg(mAudioHandler, MSG_AUDIO_SERVER_DIED,
+                            SENDMSG_NOOP, 0, 0, null, 0);
+                    sendMsg(mAudioHandler, MSG_DISPATCH_AUDIO_SERVER_STATE,
+                            SENDMSG_QUEUE, 0, 0, null, 0);
+                    break;
+                default:
+                    break;
             }
         }
     };
@@ -6943,6 +6943,23 @@
         return mRecordMonitor.getActiveRecordingConfigurations(isPrivileged);
     }
 
+    //======================
+    // Audio recording state notification from clients
+    //======================
+    /**
+     * Track a recorder provided by the client
+     */
+    public int trackRecorder(IBinder recorder) {
+        return mRecordMonitor.trackRecorder(recorder);
+    }
+
+    /**
+     * Receive an event from the client about a tracked recorder
+     */
+    public void recorderEvent(int riid, int event) {
+        mRecordMonitor.recorderEvent(riid, event);
+    }
+
     public void disableRingtoneSync(final int userId) {
         final int callingUserId = UserHandle.getCallingUserId();
         if (callingUserId != userId) {
diff --git a/services/core/java/com/android/server/audio/RecordingActivityMonitor.java b/services/core/java/com/android/server/audio/RecordingActivityMonitor.java
index 87b272b..69d1ea7 100644
--- a/services/core/java/com/android/server/audio/RecordingActivityMonitor.java
+++ b/services/core/java/com/android/server/audio/RecordingActivityMonitor.java
@@ -33,7 +33,6 @@
 import java.text.DateFormat;
 import java.util.ArrayList;
 import java.util.Date;
-import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 
@@ -50,50 +49,148 @@
     // playback configurations that do not contain uid/package name information.
     private boolean mHasPublicClients = false;
 
-    private HashMap<Integer, AudioRecordingConfiguration> mRecordConfigs =
-            new HashMap<Integer, AudioRecordingConfiguration>();
+    static final class RecordingState {
+        private final int mRiid;
+        private final RecorderDeathHandler mDeathHandler;
+        private boolean mIsActive;
+        private AudioRecordingConfiguration mConfig;
+
+        RecordingState(int riid, RecorderDeathHandler handler) {
+            mRiid = riid;
+            mDeathHandler = handler;
+        }
+
+        RecordingState(AudioRecordingConfiguration config) {
+            mRiid = AudioManager.RECORD_RIID_INVALID;
+            mDeathHandler = null;
+            mConfig = config;
+        }
+
+        int getRiid() {
+            return mRiid;
+        }
+
+        int getPortId() {
+            return mConfig != null ? mConfig.getClientPortId() : -1;
+        }
+
+        AudioRecordingConfiguration getConfig() {
+            return mConfig;
+        }
+
+        boolean hasDeathHandler() {
+            return mDeathHandler != null;
+        }
+
+        boolean isActiveConfiguration() {
+            return mIsActive && mConfig != null;
+        }
+
+        // returns true if status of an active recording has changed
+        boolean setActive(boolean active) {
+            if (mIsActive == active) return false;
+            mIsActive = active;
+            return mConfig != null;
+        }
+
+        // returns true if an active recording has been updated
+        boolean setConfig(AudioRecordingConfiguration config) {
+            if (config.equals(mConfig)) return false;
+            mConfig = config;
+            return mIsActive;
+        }
+
+        void dump(PrintWriter pw) {
+            pw.println("riid " + mRiid + "; active? " + mIsActive);
+            if (mConfig != null) {
+                mConfig.dump(pw);
+            } else {
+                pw.println("  no config");
+            }
+        }
+    }
+    private List<RecordingState> mRecordStates = new ArrayList<RecordingState>();
 
     private final PackageManager mPackMan;
 
     RecordingActivityMonitor(Context ctxt) {
         RecMonitorClient.sMonitor = this;
+        RecorderDeathHandler.sMonitor = this;
         mPackMan = ctxt.getPackageManager();
     }
 
     /**
      * Implementation of android.media.AudioSystem.AudioRecordingCallback
      */
-    public void onRecordingConfigurationChanged(int event, int uid, int session, int source,
-                                                int portId, boolean silenced, int[] recordingInfo,
+    public void onRecordingConfigurationChanged(int event, int riid, int uid, int session,
+                                                int source, int portId, boolean silenced,
+                                                int[] recordingInfo,
                                                 AudioEffect.Descriptor[] clientEffects,
                                                 AudioEffect.Descriptor[] effects,
                                                 int activeSource, String packName) {
+        final AudioRecordingConfiguration config = createRecordingConfiguration(
+                uid, session, source, recordingInfo,
+                portId, silenced, activeSource, clientEffects, effects);
         if (MediaRecorder.isSystemOnlyAudioSource(source)) {
-            // still want to log event, it just won't appear in recording configurations
-            sEventLogger.log(new RecordingEvent(event, uid, session, source, packName)
-                    .printLog(TAG));
+            // still want to log event, it just won't appear in recording configurations;
+            sEventLogger.log(new RecordingEvent(event, riid, config).printLog(TAG));
             return;
         }
-        String clientEffectName =  clientEffects.length == 0 ? "None" : clientEffects[0].name;
-        String effectName =  effects.length == 0 ? "None" : effects[0].name;
-
-        final List<AudioRecordingConfiguration> configsSystem =
-                updateSnapshot(event, uid, session, source, recordingInfo,
-                portId, silenced, activeSource, clientEffects, effects);
-        if (configsSystem != null){
-            dispatchCallbacks(configsSystem);
-        }
+        dispatchCallbacks(updateSnapshot(event, riid, config));
     }
+
+    /**
+     * Track a recorder provided by the client
+     */
+    public int trackRecorder(IBinder recorder) {
+        if (recorder == null) {
+            Log.e(TAG, "trackRecorder called with null token");
+            return AudioManager.RECORD_RIID_INVALID;
+        }
+        final int newRiid = AudioSystem.newAudioRecorderId();
+        RecorderDeathHandler handler = new RecorderDeathHandler(newRiid, recorder);
+        if (!handler.init()) {
+            // probably means that the AudioRecord has already died
+            return AudioManager.RECORD_RIID_INVALID;
+        }
+        synchronized (mRecordStates) {
+            mRecordStates.add(new RecordingState(newRiid, handler));
+        }
+        // a newly added record is inactive, no change in active configs is possible.
+        return newRiid;
+    }
+
+    /**
+     * Receive an event from the client about a tracked recorder
+     */
+    public void recorderEvent(int riid, int event) {
+        int configEvent = event == AudioManager.RECORDER_STATE_STARTED
+                ? AudioManager.RECORD_CONFIG_EVENT_START :
+                event == AudioManager.RECORDER_STATE_STOPPED
+                ? AudioManager.RECORD_CONFIG_EVENT_STOP : AudioManager.RECORD_CONFIG_EVENT_NONE;
+        if (riid == AudioManager.RECORD_RIID_INVALID
+                || configEvent == AudioManager.RECORD_CONFIG_EVENT_NONE) {
+            sEventLogger.log(new RecordingEvent(event, riid, null).printLog(TAG));
+            return;
+        }
+        dispatchCallbacks(updateSnapshot(configEvent, riid, null));
+    }
+
+    void unregisterRecorder(int riid) {
+        dispatchCallbacks(updateSnapshot(AudioManager.RECORD_CONFIG_EVENT_DEATH, riid, null));
+    }
+
     private void dispatchCallbacks(List<AudioRecordingConfiguration> configs) {
+        if (configs == null) { // null means "no changes"
+            return;
+        }
         synchronized (mClients) {
             // list of recording configurations for "public consumption". It is only computed if
             // there are non-system recording activity listeners.
             final List<AudioRecordingConfiguration> configsPublic = mHasPublicClients
                     ? anonymizeForPublicConsumption(configs) :
                       new ArrayList<AudioRecordingConfiguration>();
-            final Iterator<RecMonitorClient> clientIterator = mClients.iterator();
-            while (clientIterator.hasNext()) {
-                final RecMonitorClient rmc = clientIterator.next();
+            for (RecMonitorClient rmc : mClients) {
                 try {
                     if (rmc.mIsPrivileged) {
                         rmc.mDispatcherCb.dispatchRecordingConfigChange(configs);
@@ -108,12 +205,12 @@
     }
 
     protected void dump(PrintWriter pw) {
-        // players
+        // recorders
         pw.println("\nRecordActivityMonitor dump time: "
                 + DateFormat.getTimeInstance().format(new Date()));
-        synchronized(mRecordConfigs) {
-            for (AudioRecordingConfiguration conf : mRecordConfigs.values()) {
-                conf.dump(pw);
+        synchronized (mRecordStates) {
+            for (RecordingState state : mRecordStates) {
+                state.dump(pw);
             }
         }
         pw.println("\n");
@@ -121,7 +218,7 @@
         sEventLogger.dump(pw);
     }
 
-    private ArrayList<AudioRecordingConfiguration> anonymizeForPublicConsumption(
+    private static ArrayList<AudioRecordingConfiguration> anonymizeForPublicConsumption(
             List<AudioRecordingConfiguration> sysConfigs) {
         ArrayList<AudioRecordingConfiguration> publicConfigs =
                 new ArrayList<AudioRecordingConfiguration>();
@@ -136,11 +233,30 @@
         AudioSystem.setRecordingCallback(this);
     }
 
-    void clear() {
-        synchronized (mRecordConfigs) {
-            mRecordConfigs.clear();
+    void onAudioServerDied() {
+        // Remove all RecordingState entries that do not have a death handler (that means
+        // they are tracked by the Audio Server). If there were active entries among removed,
+        // dispatch active configuration changes.
+        List<AudioRecordingConfiguration> configs = null;
+        synchronized (mRecordStates) {
+            boolean configChanged = false;
+            for (Iterator<RecordingState> it = mRecordStates.iterator(); it.hasNext(); ) {
+                RecordingState state = it.next();
+                if (!state.hasDeathHandler()) {
+                    if (state.isActiveConfiguration()) {
+                        configChanged = true;
+                        sEventLogger.log(new RecordingEvent(
+                                        AudioManager.RECORD_CONFIG_EVENT_DEATH,
+                                        state.getRiid(), state.getConfig()));
+                    }
+                    it.remove();
+                }
+            }
+            if (configChanged) {
+                configs = getActiveRecordingConfigurations(true /*isPrivileged*/);
+            }
         }
-        dispatchCallbacks(new ArrayList<AudioRecordingConfiguration>());
+        dispatchCallbacks(configs);
     }
 
     void registerRecordingCallback(IRecordingConfigDispatcher rcdb, boolean isPrivileged) {
@@ -181,21 +297,25 @@
     }
 
     List<AudioRecordingConfiguration> getActiveRecordingConfigurations(boolean isPrivileged) {
-        synchronized(mRecordConfigs) {
-            if (isPrivileged) {
-                return new ArrayList<AudioRecordingConfiguration>(mRecordConfigs.values());
-            } else {
-                final List<AudioRecordingConfiguration> configsPublic =
-                        anonymizeForPublicConsumption(
-                            new ArrayList<AudioRecordingConfiguration>(mRecordConfigs.values()));
-                return configsPublic;
+        List<AudioRecordingConfiguration> configs = new ArrayList<AudioRecordingConfiguration>();
+        synchronized (mRecordStates) {
+            for (RecordingState state : mRecordStates) {
+                if (state.isActiveConfiguration()) {
+                    configs.add(state.getConfig());
+                }
             }
         }
+        // AudioRecordingConfiguration objects never get updated. If config changes,
+        // the reference to the config is set in RecordingState.
+        if (!isPrivileged) {
+            configs = anonymizeForPublicConsumption(configs);
+        }
+        return configs;
     }
 
     /**
-     * Update the internal "view" of the active recording sessions
-     * @param event
+     * Create a recording configuration from the provided parameters
+     * @param uid
      * @param session
      * @param source
      * @param recordingFormat see
@@ -207,82 +327,133 @@
      * @param activeSource
      * @param clientEffects
      * @param effects
+     * @return null a configuration object.
+     */
+    private AudioRecordingConfiguration createRecordingConfiguration(int uid,
+            int session, int source, int[] recordingInfo, int portId, boolean silenced,
+            int activeSource, AudioEffect.Descriptor[] clientEffects,
+            AudioEffect.Descriptor[] effects) {
+        final AudioFormat clientFormat = new AudioFormat.Builder()
+                .setEncoding(recordingInfo[0])
+                // FIXME this doesn't support index-based masks
+                .setChannelMask(recordingInfo[1])
+                .setSampleRate(recordingInfo[2])
+                .build();
+        final AudioFormat deviceFormat = new AudioFormat.Builder()
+                .setEncoding(recordingInfo[3])
+                // FIXME this doesn't support index-based masks
+                .setChannelMask(recordingInfo[4])
+                .setSampleRate(recordingInfo[5])
+                .build();
+        final int patchHandle = recordingInfo[6];
+        final String[] packages = mPackMan.getPackagesForUid(uid);
+        final String packageName;
+        if (packages != null && packages.length > 0) {
+            packageName = packages[0];
+        } else {
+            packageName = "";
+        }
+        return new AudioRecordingConfiguration(uid, session, source,
+                clientFormat, deviceFormat, patchHandle, packageName,
+                portId, silenced, activeSource, clientEffects, effects);
+    }
+
+    /**
+     * Update the internal "view" of the active recording sessions
+     * @param event RECORD_CONFIG_EVENT_...
+     * @param riid
+     * @param config
      * @return null if the list of active recording sessions has not been modified, a list
      *     with the current active configurations otherwise.
      */
-    private List<AudioRecordingConfiguration> updateSnapshot(int event, int uid, int session,
-            int source, int[] recordingInfo, int portId, boolean silenced, int activeSource,
-            AudioEffect.Descriptor[] clientEffects, AudioEffect.Descriptor[] effects) {
-        final boolean configChanged;
-        final ArrayList<AudioRecordingConfiguration> configs;
-        synchronized(mRecordConfigs) {
-            switch (event) {
-            case AudioManager.RECORD_CONFIG_EVENT_STOP:
-                // return failure if an unknown recording session stopped
-                configChanged = (mRecordConfigs.remove(new Integer(portId)) != null);
-                if (configChanged) {
-                    sEventLogger.log(new RecordingEvent(event, uid, session, source, null));
-                }
-                break;
-            case AudioManager.RECORD_CONFIG_EVENT_START:
-                final AudioFormat clientFormat = new AudioFormat.Builder()
-                        .setEncoding(recordingInfo[0])
-                        // FIXME this doesn't support index-based masks
-                        .setChannelMask(recordingInfo[1])
-                        .setSampleRate(recordingInfo[2])
-                        .build();
-                final AudioFormat deviceFormat = new AudioFormat.Builder()
-                        .setEncoding(recordingInfo[3])
-                        // FIXME this doesn't support index-based masks
-                        .setChannelMask(recordingInfo[4])
-                        .setSampleRate(recordingInfo[5])
-                        .build();
-                final int patchHandle = recordingInfo[6];
-                final Integer portIdKey = new Integer(portId);
-
-                final String[] packages = mPackMan.getPackagesForUid(uid);
-                final String packageName;
-                if (packages != null && packages.length > 0) {
-                    packageName = packages[0];
+    private List<AudioRecordingConfiguration> updateSnapshot(
+            int event, int riid, AudioRecordingConfiguration config) {
+        List<AudioRecordingConfiguration> configs = null;
+        synchronized (mRecordStates) {
+            int stateIndex = -1;
+            if (riid != AudioManager.RECORD_RIID_INVALID) {
+                stateIndex = findStateByRiid(riid);
+            } else if (config != null) {
+                stateIndex = findStateByPortId(config.getClientPortId());
+            }
+            if (stateIndex == -1) {
+                if (event == AudioManager.RECORD_CONFIG_EVENT_START && config != null) {
+                    // First time registration for a recorder tracked by AudioServer.
+                    mRecordStates.add(new RecordingState(config));
+                    stateIndex = mRecordStates.size() - 1;
                 } else {
-                    packageName = "";
-                }
-                final AudioRecordingConfiguration updatedConfig =
-                        new AudioRecordingConfiguration(uid, session, source,
-                                clientFormat, deviceFormat, patchHandle, packageName,
-                                portId, silenced, activeSource, clientEffects, effects);
-
-                if (mRecordConfigs.containsKey(portIdKey)) {
-                    if (updatedConfig.equals(mRecordConfigs.get(portIdKey))) {
-                        configChanged = false;
-                    } else {
-                        // config exists but has been modified
-                        mRecordConfigs.remove(portIdKey);
-                        mRecordConfigs.put(portIdKey, updatedConfig);
-                        configChanged = true;
+                    if (config == null) {
+                        // Records tracked by clients must be registered first via trackRecorder.
+                        Log.e(TAG, String.format(
+                                        "Unexpected event %d for riid %d", event, riid));
                     }
-                } else {
-                    mRecordConfigs.put(portIdKey, updatedConfig);
-                    configChanged = true;
+                    return configs;
                 }
-                if (configChanged) {
-                    sEventLogger.log(new RecordingEvent(event, uid, session, source, packageName));
-                }
-                break;
-            default:
-                Log.e(TAG, String.format("Unknown event %d for session %d, source %d",
-                        event, session, source));
-                configChanged = false;
+            }
+            final RecordingState state = mRecordStates.get(stateIndex);
+
+            boolean configChanged;
+            switch (event) {
+                case AudioManager.RECORD_CONFIG_EVENT_START:
+                    configChanged = state.setActive(true);
+                    if (config != null) { // ??? Can remove ???
+                        configChanged = state.setConfig(config) || configChanged;
+                    }
+                    break;
+                case AudioManager.RECORD_CONFIG_EVENT_UPDATE:
+                    // For this event config != null
+                    configChanged = state.setConfig(config);
+                    break;
+                case AudioManager.RECORD_CONFIG_EVENT_STOP:
+                    configChanged = state.setActive(false);
+                    if (!state.hasDeathHandler()) {
+                        // A recorder tracked by AudioServer has to be removed now so it
+                        // does not leak. It will be re-registered if recording starts again.
+                        mRecordStates.remove(stateIndex);
+                    }
+                    break;
+                case AudioManager.RECORD_CONFIG_EVENT_DEATH:
+                    configChanged = state.isActiveConfiguration();
+                    mRecordStates.remove(stateIndex);
+                    break;
+                default:
+                    Log.e(TAG, String.format("Unknown event %d for riid %d / portid %d",
+                                    event, riid, state.getPortId()));
+                    configChanged = false;
             }
             if (configChanged) {
-                configs = new ArrayList<AudioRecordingConfiguration>(mRecordConfigs.values());
-            } else {
-                configs = null;
+                sEventLogger.log(new RecordingEvent(event, riid, state.getConfig()));
+                configs = getActiveRecordingConfigurations(true /*isPrivileged*/);
             }
         }
         return configs;
     }
 
+    // riid is assumed to be valid
+    private int findStateByRiid(int riid) {
+        synchronized (mRecordStates) {
+            for (int i = 0; i < mRecordStates.size(); i++) {
+                if (mRecordStates.get(i).getRiid() == riid) {
+                    return i;
+                }
+            }
+        }
+        return -1;
+    }
+
+    private int findStateByPortId(int portId) {
+        // Lookup by portId is unambiguous only for recordings managed by the Audio Server.
+        synchronized (mRecordStates) {
+            for (int i = 0; i < mRecordStates.size(); i++) {
+                if (!mRecordStates.get(i).hasDeathHandler()
+                        && mRecordStates.get(i).getPortId() == portId) {
+                    return i;
+                }
+            }
+        }
+        return -1;
+    }
+
     /**
      * Inner class to track clients that want to be notified of recording updates
      */
@@ -319,28 +490,80 @@
         }
     }
 
+    private static final class RecorderDeathHandler implements IBinder.DeathRecipient {
+
+        // can afford to be static because only one RecordingActivityMonitor ever instantiated
+        static RecordingActivityMonitor sMonitor;
+
+        final int mRiid;
+        private final IBinder mRecorderToken;
+
+        RecorderDeathHandler(int riid, IBinder recorderToken) {
+            mRiid = riid;
+            mRecorderToken = recorderToken;
+        }
+
+        public void binderDied() {
+            sMonitor.unregisterRecorder(mRiid);
+        }
+
+        boolean init() {
+            try {
+                mRecorderToken.linkToDeath(this, 0);
+                return true;
+            } catch (RemoteException e) {
+                Log.w(TAG, "Could not link to recorder death", e);
+                return false;
+            }
+        }
+    }
+
     /**
      * Inner class for recording event logging
      */
     private static final class RecordingEvent extends AudioEventLogger.Event {
         private final int mRecEvent;
+        private final int mRIId;
         private final int mClientUid;
         private final int mSession;
         private final int mSource;
         private final String mPackName;
 
-        RecordingEvent(int event, int uid, int session, int source, String packName) {
+        RecordingEvent(int event, int riid, AudioRecordingConfiguration config) {
             mRecEvent = event;
-            mClientUid = uid;
-            mSession = session;
-            mSource = source;
-            mPackName = packName;
+            mRIId = riid;
+            if (config != null) {
+                mClientUid = config.getClientUid();
+                mSession = config.getClientAudioSessionId();
+                mSource = config.getClientAudioSource();
+                mPackName = config.getClientPackageName();
+            } else {
+                mClientUid = -1;
+                mSession = -1;
+                mSource = -1;
+                mPackName = null;
+            }
+        }
+
+        private static String recordEventToString(int recEvent) {
+            switch (recEvent) {
+                case AudioManager.RECORD_CONFIG_EVENT_START:
+                    return "start";
+                case AudioManager.RECORD_CONFIG_EVENT_UPDATE:
+                    return "update";
+                case AudioManager.RECORD_CONFIG_EVENT_STOP:
+                    return "stop";
+                case AudioManager.RECORD_CONFIG_EVENT_DEATH:
+                    return "death";
+                default:
+                    return "unknown (" + recEvent + ")";
+            }
         }
 
         @Override
         public String eventToString() {
-            return new StringBuilder("rec ").append(
-                        mRecEvent == AudioManager.RECORD_CONFIG_EVENT_START ? "start" : "stop ")
+            return new StringBuilder("rec ").append(recordEventToString(mRecEvent))
+                    .append(" riid:").append(mRIId)
                     .append(" uid:").append(mClientUid)
                     .append(" session:").append(mSession)
                     .append(" src:").append(MediaRecorder.toLogFriendlyAudioSource(mSource))
@@ -349,5 +572,5 @@
     }
 
     private static final AudioEventLogger sEventLogger = new AudioEventLogger(50,
-            "recording activity as reported through AudioSystem.AudioRecordingCallback");
+            "recording activity received by AudioService");
 }