content: /*
   * Copyright (C) 2018 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 android.media;
  
  import android.annotation.CallbackExecutor;
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.content.Context;
  import android.os.Binder;
  import android.os.Handler;
  import android.os.HandlerThread;
  import android.os.IBinder;
  import android.os.Looper;
  import android.os.Message;
  import android.os.RemoteException;
  import android.os.ServiceManager;
  import android.util.Log;
  
  import com.android.internal.annotations.GuardedBy;
  
  import java.util.ArrayList;
  import java.util.LinkedList;
  import java.util.List;
  import java.util.concurrent.Executor;
  
  /**
   * Implementation of AudioRecordingMonitor interface.
   * @hide
   */
  public class AudioRecordingMonitorImpl implements AudioRecordingMonitor {
  
      private static final String TAG = "android.media.AudioRecordingMonitor";
  
      private static IAudioService sService; //lazy initialization, use getService()
  
      private final AudioRecordingMonitorClient mClient;
  
      AudioRecordingMonitorImpl(@NonNull AudioRecordingMonitorClient client) {
          mClient = client;
      }
  
      /**
       * Register a callback to be notified of audio capture changes via a
       * {@link AudioManager.AudioRecordingCallback}. A callback is received when the capture path
       * configuration changes (pre-processing, format, sampling rate...) or capture is
       * silenced/unsilenced by the system.
       * @param executor {@link Executor} to handle the callbacks.
       * @param cb non-null callback to register
       */
      public void registerAudioRecordingCallback(@NonNull @CallbackExecutor Executor executor,
              @NonNull AudioManager.AudioRecordingCallback cb) {
          if (cb == null) {
              throw new IllegalArgumentException("Illegal null AudioRecordingCallback");
          }
          if (executor == null) {
              throw new IllegalArgumentException("Illegal null Executor");
          }
          synchronized (mRecordCallbackLock) {
              // check if eventCallback already in list
              for (AudioRecordingCallbackInfo arci : mRecordCallbackList) {
                  if (arci.mCb == cb) {
                      throw new IllegalArgumentException(
                              "AudioRecordingCallback already registered");
                  }
              }
              beginRecordingCallbackHandling();
              mRecordCallbackList.add(new AudioRecordingCallbackInfo(executor, cb));
          }
      }
  
      /**
       * Unregister an audio recording callback previously registered with
       * {@link #registerAudioRecordingCallback(Executor, AudioManager.AudioRecordingCallback)}.
       * @param cb non-null callback to unregister
       */
      public void unregisterAudioRecordingCallback(@NonNull AudioManager.AudioRecordingCallback cb) {
          if (cb == null) {
              throw new IllegalArgumentException("Illegal null AudioRecordingCallback argument");
          }
  
          synchronized (mRecordCallbackLock) {
              for (AudioRecordingCallbackInfo arci : mRecordCallbackList) {
                  if (arci.mCb == cb) {
                      // ok to remove while iterating over list as we exit iteration
                      mRecordCallbackList.remove(arci);
                      if (mRecordCallbackList.size() == 0) {
                          endRecordingCallbackHandling();
                      }
                      return;
                  }
              }
              throw new IllegalArgumentException("AudioRecordingCallback was not registered");
          }
      }
  
      /**
       * Returns the current active audio recording for this audio recorder.
       * @return a valid {@link AudioRecordingConfiguration} if this recorder is active
       * or null otherwise.
       * @see AudioRecordingConfiguration
       */
      public @Nullable AudioRecordingConfiguration getActiveRecordingConfiguration() {
          final IAudioService service = getService();
          try {
              List<AudioRecordingConfiguration> configs = service.getActiveRecordingConfigurations();
              return getMyConfig(configs);
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      private static class AudioRecordingCallbackInfo {
          final AudioManager.AudioRecordingCallback mCb;
          final Executor mExecutor;
          AudioRecordingCallbackInfo(Executor e, AudioManager.AudioRecordingCallback cb) {
              mExecutor = e;
              mCb = cb;
          }
      }
  
      private static final int MSG_RECORDING_CONFIG_CHANGE = 1;
  
      private final Object mRecordCallbackLock = new Object();
      @GuardedBy("mRecordCallbackLock")
      @NonNull private LinkedList<AudioRecordingCallbackInfo> mRecordCallbackList =
              new LinkedList<AudioRecordingCallbackInfo>();
      @GuardedBy("mRecordCallbackLock")
      private @Nullable HandlerThread mRecordingCallbackHandlerThread;
      @GuardedBy("mRecordCallbackLock")
      private @Nullable volatile Handler mRecordingCallbackHandler;
  
      @GuardedBy("mRecordCallbackLock")
      private final IRecordingConfigDispatcher mRecordingCallback =
              new IRecordingConfigDispatcher.Stub() {
          @Override
          public void dispatchRecordingConfigChange(List<AudioRecordingConfiguration> configs) {
              AudioRecordingConfiguration config = getMyConfig(configs);
              if (config != null) {
                  synchronized (mRecordCallbackLock) {
                      if (mRecordingCallbackHandler != null) {
                          final Message m = mRecordingCallbackHandler.obtainMessage(
                                                MSG_RECORDING_CONFIG_CHANGE/*what*/, config /*obj*/);
                          mRecordingCallbackHandler.sendMessage(m);
                      }
                  }
              }
          }
      };
  
      @GuardedBy("mRecordCallbackLock")
      private void beginRecordingCallbackHandling() {
          if (mRecordingCallbackHandlerThread == null) {
              mRecordingCallbackHandlerThread = new HandlerThread(TAG + ".RecordingCallback");
              mRecordingCallbackHandlerThread.start();
              final Looper looper = mRecordingCallbackHandlerThread.getLooper();
              if (looper != null) {
                  mRecordingCallbackHandler = new Handler(looper) {
                      @Override
                      public void handleMessage(Message msg) {
                          switch (msg.what) {
                              case MSG_RECORDING_CONFIG_CHANGE: {
                                  if (msg.obj == null) {
                                      return;
                                  }
                                  ArrayList<AudioRecordingConfiguration> configs =
                                          new ArrayList<AudioRecordingConfiguration>();
                                  configs.add((AudioRecordingConfiguration) msg.obj);
  
                                  final LinkedList<AudioRecordingCallbackInfo> cbInfoList;
                                  synchronized (mRecordCallbackLock) {
                                      if (mRecordCallbackList.size() == 0) {
                                          return;
                                      }
                                      cbInfoList = new LinkedList<AudioRecordingCallbackInfo>(
                                          mRecordCallbackList);
                                  }
  
                                  final long identity = Binder.clearCallingIdentity();
                                  try {
                                      for (AudioRecordingCallbackInfo cbi : cbInfoList) {
                                          cbi.mExecutor.execute(() ->
                                                  cbi.mCb.onRecordingConfigChanged(configs));
                                      }
                                  } finally {
                                      Binder.restoreCallingIdentity(identity);
                                  }
                              } break;
                              default:
                                  Log.e(TAG, "Unknown event " + msg.what);
                                  break;
                          }
                      }
                  };
                  final IAudioService service = getService();
                  try {
                      service.registerRecordingCallback(mRecordingCallback);
                  } catch (RemoteException e) {
                      throw e.rethrowFromSystemServer();
                  }
              }
          }
      }
  
      @GuardedBy("mRecordCallbackLock")
      private void endRecordingCallbackHandling() {
          if (mRecordingCallbackHandlerThread != null) {
              final IAudioService service = getService();
              try {
                  service.unregisterRecordingCallback(mRecordingCallback);
              } catch (RemoteException e) {
                  throw e.rethrowFromSystemServer();
              }
              mRecordingCallbackHandlerThread.quit();
              mRecordingCallbackHandlerThread = null;
          }
      }
  
      AudioRecordingConfiguration getMyConfig(List<AudioRecordingConfiguration> configs) {
          int portId = mClient.getPortId();
          for (AudioRecordingConfiguration config : configs) {
              if (config.getClientPortId() == portId) {
                  return config;
              }
          }
          return null;
      }
  
      private static IAudioService getService() {
          if (sService != null) {
              return sService;
          }
          IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
          sService = IAudioService.Stub.asInterface(b);
          return sService;
      }
  }
  