blob: c2cd4bc0b7eb6407d17fc294d486041b3934ed5d [file] [log] [blame]
/*
* 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;
}
}