content: /*
   * Copyright (C) 2016 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.server.audio;
  
  import android.content.Context;
  import android.content.pm.PackageManager;
  import android.media.AudioFormat;
  import android.media.AudioManager;
  import android.media.AudioRecordingConfiguration;
  import android.media.AudioSystem;
  import android.media.IRecordingConfigDispatcher;
  import android.media.MediaRecorder;
  import android.media.audiofx.AudioEffect;
  import android.os.IBinder;
  import android.os.RemoteException;
  import android.util.Log;
  
  import java.io.PrintWriter;
  import java.text.DateFormat;
  import java.util.ArrayList;
  import java.util.Date;
  import java.util.Iterator;
  import java.util.List;
  
  /**
   * Class to receive and dispatch updates from AudioSystem about recording configurations.
   */
  public final class RecordingActivityMonitor implements AudioSystem.AudioRecordingCallback {
  
      public final static String TAG = "AudioService.RecordingActivityMonitor";
  
      private ArrayList<RecMonitorClient> mClients = new ArrayList<RecMonitorClient>();
      // a public client is one that needs an anonymized version of the playback configurations, we
      // keep track of whether there is at least one to know when we need to create the list of
      // playback configurations that do not contain uid/package name information.
      private boolean mHasPublicClients = false;
  
      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 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, riid, config).printLog(TAG));
              return;
          }
          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));
      }
  
      /**
       * Stop tracking the recorder
       */
      public void releaseRecorder(int riid) {
          dispatchCallbacks(updateSnapshot(AudioManager.RECORD_CONFIG_EVENT_RELEASE, 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>();
              for (RecMonitorClient rmc : mClients) {
                  try {
                      if (rmc.mIsPrivileged) {
                          rmc.mDispatcherCb.dispatchRecordingConfigChange(configs);
                      } else {
                          rmc.mDispatcherCb.dispatchRecordingConfigChange(configsPublic);
                      }
                  } catch (RemoteException e) {
                      Log.w(TAG, "Could not call dispatchRecordingConfigChange() on client", e);
                  }
              }
          }
      }
  
      protected void dump(PrintWriter pw) {
          // recorders
          pw.println("\nRecordActivityMonitor dump time: "
                  + DateFormat.getTimeInstance().format(new Date()));
          synchronized (mRecordStates) {
              for (RecordingState state : mRecordStates) {
                  state.dump(pw);
              }
          }
          pw.println("\n");
          // log
          sEventLogger.dump(pw);
      }
  
      private static ArrayList<AudioRecordingConfiguration> anonymizeForPublicConsumption(
              List<AudioRecordingConfiguration> sysConfigs) {
          ArrayList<AudioRecordingConfiguration> publicConfigs =
                  new ArrayList<AudioRecordingConfiguration>();
          // only add active anonymized configurations,
          for (AudioRecordingConfiguration config : sysConfigs) {
              publicConfigs.add(AudioRecordingConfiguration.anonymizedCopy(config));
          }
          return publicConfigs;
      }
  
      void initMonitor() {
          AudioSystem.setRecordingCallback(this);
      }
  
      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_RELEASE,
                                          state.getRiid(), state.getConfig()));
                      }
                      it.remove();
                  }
              }
              if (configChanged) {
                  configs = getActiveRecordingConfigurations(true /*isPrivileged*/);
              }
          }
          dispatchCallbacks(configs);
      }
  
      void registerRecordingCallback(IRecordingConfigDispatcher rcdb, boolean isPrivileged) {
          if (rcdb == null) {
              return;
          }
          synchronized (mClients) {
              final RecMonitorClient rmc = new RecMonitorClient(rcdb, isPrivileged);
              if (rmc.init()) {
                  if (!isPrivileged) {
                      mHasPublicClients = true;
                  }
                  mClients.add(rmc);
              }
          }
      }
  
      void unregisterRecordingCallback(IRecordingConfigDispatcher rcdb) {
          if (rcdb == null) {
              return;
          }
          synchronized (mClients) {
              final Iterator<RecMonitorClient> clientIterator = mClients.iterator();
              boolean hasPublicClients = false;
              while (clientIterator.hasNext()) {
                  RecMonitorClient rmc = clientIterator.next();
                  if (rcdb.equals(rmc.mDispatcherCb)) {
                      rmc.release();
                      clientIterator.remove();
                  } else {
                      if (!rmc.mIsPrivileged) {
                          hasPublicClients = true;
                      }
                  }
              }
              mHasPublicClients = hasPublicClients;
          }
      }
  
      List<AudioRecordingConfiguration> getActiveRecordingConfigurations(boolean isPrivileged) {
          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;
      }
  
      /**
       * Create a recording configuration from the provided parameters
       * @param uid
       * @param session
       * @param source
       * @param recordingFormat see
       *     {@link AudioSystem.AudioRecordingCallback#onRecordingConfigurationChanged(int, int, int,\
       int, int, boolean, int[], AudioEffect.Descriptor[], AudioEffect.Descriptor[], int, String)}
       *     for the definition of the contents of the array
       * @param portId
       * @param silenced
       * @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 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 {
                      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));
                      }
                      return configs;
                  }
              }
              final RecordingState state = mRecordStates.get(stateIndex);
  
              boolean configChanged;
              switch (event) {
                  case AudioManager.RECORD_CONFIG_EVENT_START:
                      configChanged = state.setActive(true);
                      if (config != null) {
                          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_RELEASE:
                      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) {
                  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
       */
      private final static class RecMonitorClient implements IBinder.DeathRecipient {
  
          // can afford to be static because only one RecordingActivityMonitor ever instantiated
          static RecordingActivityMonitor sMonitor;
  
          final IRecordingConfigDispatcher mDispatcherCb;
          final boolean mIsPrivileged;
  
          RecMonitorClient(IRecordingConfigDispatcher rcdb, boolean isPrivileged) {
              mDispatcherCb = rcdb;
              mIsPrivileged = isPrivileged;
          }
  
          public void binderDied() {
              Log.w(TAG, "client died");
              sMonitor.unregisterRecordingCallback(mDispatcherCb);
          }
  
          boolean init() {
              try {
                  mDispatcherCb.asBinder().linkToDeath(this, 0);
                  return true;
              } catch (RemoteException e) {
                  Log.w(TAG, "Could not link to client death", e);
                  return false;
              }
          }
  
          void release() {
              mDispatcherCb.asBinder().unlinkToDeath(this, 0);
          }
      }
  
      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.releaseRecorder(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 riid, AudioRecordingConfiguration config) {
              mRecEvent = event;
              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_RELEASE:
                      return "release";
                  default:
                      return "unknown (" + recEvent + ")";
              }
          }
  
          @Override
          public String eventToString() {
              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))
                      .append(mPackName == null ? "" : " pack:" + mPackName).toString();
          }
      }
  
      private static final AudioEventLogger sEventLogger = new AudioEventLogger(50,
              "recording activity received by AudioService");
  }
  