Add AudioRecordingConfiguration changed callback

Add a callback reporting changes in current audio recording
configuration for AudioRecord and MediaRecorder.
The callback is called when the capture path configuration
changes (pre preprocessing, format, sampling rate...) or capture
is silenced/unsilenced by the system.

Bug: 111438757
Test: CTS tests for AudioRecord and MediaRecorder
Change-Id: Ifd5c5b4f5ee8911822a3f05412c84edf2db7858a
This commit is contained in:
Eric Laurent
2018-12-14 17:22:22 -08:00
parent 69b9900a27
commit 333dfad3bf
8 changed files with 477 additions and 6 deletions

View File

@@ -23379,12 +23379,13 @@ package android.media {
method public android.media.AudioPresentation.Builder setProgramId(int);
}
public class AudioRecord implements android.media.AudioRouting {
public class AudioRecord implements android.media.AudioRecordingMonitor android.media.AudioRouting {
ctor public AudioRecord(int, int, int, int, int) throws java.lang.IllegalArgumentException;
method public void addOnRoutingChangedListener(android.media.AudioRouting.OnRoutingChangedListener, android.os.Handler);
method public deprecated void addOnRoutingChangedListener(android.media.AudioRecord.OnRoutingChangedListener, android.os.Handler);
method protected void finalize();
method public java.util.List<android.media.MicrophoneInfo> getActiveMicrophones() throws java.io.IOException;
method public android.media.AudioRecordingConfiguration getActiveRecordingConfiguration();
method public int getAudioFormat();
method public int getAudioSessionId();
method public int getAudioSource();
@@ -23409,6 +23410,7 @@ package android.media {
method public int read(float[], int, int, int);
method public int read(java.nio.ByteBuffer, int);
method public int read(java.nio.ByteBuffer, int, int);
method public void registerAudioRecordingCallback(java.util.concurrent.Executor, android.media.AudioManager.AudioRecordingCallback);
method public void release();
method public void removeOnRoutingChangedListener(android.media.AudioRouting.OnRoutingChangedListener);
method public deprecated void removeOnRoutingChangedListener(android.media.AudioRecord.OnRoutingChangedListener);
@@ -23420,6 +23422,7 @@ package android.media {
method public void startRecording() throws java.lang.IllegalStateException;
method public void startRecording(android.media.MediaSyncEvent) throws java.lang.IllegalStateException;
method public void stop() throws java.lang.IllegalStateException;
method public void unregisterAudioRecordingCallback(android.media.AudioManager.AudioRecordingCallback);
field public static final int ERROR = -1; // 0xffffffff
field public static final int ERROR_BAD_VALUE = -2; // 0xfffffffe
field public static final int ERROR_DEAD_OBJECT = -6; // 0xfffffffa
@@ -23474,6 +23477,12 @@ package android.media {
field public static final android.os.Parcelable.Creator<android.media.AudioRecordingConfiguration> CREATOR;
}
public abstract interface AudioRecordingMonitor {
method public abstract android.media.AudioRecordingConfiguration getActiveRecordingConfiguration();
method public abstract void registerAudioRecordingCallback(java.util.concurrent.Executor, android.media.AudioManager.AudioRecordingCallback);
method public abstract void unregisterAudioRecordingCallback(android.media.AudioManager.AudioRecordingCallback);
}
public abstract interface AudioRouting {
method public abstract void addOnRoutingChangedListener(android.media.AudioRouting.OnRoutingChangedListener, android.os.Handler);
method public abstract android.media.AudioDeviceInfo getPreferredDevice();
@@ -25475,11 +25484,12 @@ package android.media {
field public static final int MEDIA_TRACK_TYPE_VIDEO = 1; // 0x1
}
public class MediaRecorder implements android.media.AudioRouting {
public class MediaRecorder implements android.media.AudioRecordingMonitor android.media.AudioRouting {
ctor public MediaRecorder();
method public void addOnRoutingChangedListener(android.media.AudioRouting.OnRoutingChangedListener, android.os.Handler);
method protected void finalize();
method public java.util.List<android.media.MicrophoneInfo> getActiveMicrophones() throws java.io.IOException;
method public android.media.AudioRecordingConfiguration getActiveRecordingConfiguration();
method public static final int getAudioSourceMax();
method public int getMaxAmplitude() throws java.lang.IllegalStateException;
method public android.os.PersistableBundle getMetrics();
@@ -25488,6 +25498,7 @@ package android.media {
method public android.view.Surface getSurface();
method public void pause() throws java.lang.IllegalStateException;
method public void prepare() throws java.io.IOException, java.lang.IllegalStateException;
method public void registerAudioRecordingCallback(java.util.concurrent.Executor, android.media.AudioManager.AudioRecordingCallback);
method public void release();
method public void removeOnRoutingChangedListener(android.media.AudioRouting.OnRoutingChangedListener);
method public void reset();
@@ -25523,6 +25534,7 @@ package android.media {
method public void setVideoSource(int) throws java.lang.IllegalStateException;
method public void start() throws java.lang.IllegalStateException;
method public void stop() throws java.lang.IllegalStateException;
method public void unregisterAudioRecordingCallback(android.media.AudioManager.AudioRecordingCallback);
field public static final int MEDIA_ERROR_SERVER_DIED = 100; // 0x64
field public static final int MEDIA_RECORDER_ERROR_UNKNOWN = 1; // 0x1
field public static final int MEDIA_RECORDER_INFO_MAX_DURATION_REACHED = 800; // 0x320

View File

@@ -2953,7 +2953,7 @@ package android.media {
field public static final int PLAYER_TYPE_UNKNOWN = -1; // 0xffffffff
}
public class AudioRecord implements android.media.AudioRouting {
public class AudioRecord implements android.media.AudioRecordingMonitor android.media.AudioRouting {
ctor public AudioRecord(android.media.AudioAttributes, android.media.AudioFormat, int, int) throws java.lang.IllegalArgumentException;
}

View File

@@ -16,8 +16,10 @@
package android.media;
import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.annotation.UnsupportedAppUsage;
import android.app.ActivityThread;
@@ -43,6 +45,7 @@ import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Executor;
/**
* The AudioRecord class manages the audio resources for Java applications
@@ -58,7 +61,7 @@ import java.util.List;
* been read yet. Data should be read from the audio hardware in chunks of sizes inferior to
* the total recording buffer size.
*/
public class AudioRecord implements AudioRouting
public class AudioRecord implements AudioRouting, AudioRecordingMonitor, AudioRecordingMonitorClient
{
//---------------------------------------------------------
// Constants
@@ -1654,6 +1657,56 @@ public class AudioRecord implements AudioRouting
return activeMicrophones;
}
//--------------------------------------------------------------------------
// Implementation of AudioRecordingMonitor interface
//--------------------
AudioRecordingMonitorImpl mRecordingInfoImpl =
new AudioRecordingMonitorImpl((AudioRecordingMonitorClient) this);
/**
* 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) {
mRecordingInfoImpl.registerAudioRecordingCallback(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) {
mRecordingInfoImpl.unregisterAudioRecordingCallback(cb);
}
/**
* 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() {
return mRecordingInfoImpl.getActiveRecordingConfiguration();
}
//---------------------------------------------------------
// Implementation of AudioRecordingMonitorClient interface
//--------------------
/**
* @hide
*/
public int getPortId() {
return native_getPortId();
}
//---------------------------------------------------------
// Interface definitions
//--------------------

View File

@@ -0,0 +1,56 @@
/*
* 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 java.util.concurrent.Executor;
/**
* AudioRecordingMonitor defines an interface implemented by {@link AudioRecord} and
* {@link MediaRecorder} allowing applications to install a callback and be notified of changes
* in the capture path while recoding is active.
*/
public interface AudioRecordingMonitor {
/**
* 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
*/
void registerAudioRecordingCallback(@NonNull @CallbackExecutor Executor executor,
@NonNull AudioManager.AudioRecordingCallback cb);
/**
* Unregister an audio recording callback previously registered with
* {@link #registerAudioRecordingCallback(Executor, AudioManager.AudioRecordingCallback)}.
* @param cb non-null callback to unregister
*/
void unregisterAudioRecordingCallback(@NonNull AudioManager.AudioRecordingCallback cb);
/**
* 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
*/
@Nullable AudioRecordingConfiguration getActiveRecordingConfiguration();
}

View File

@@ -0,0 +1,28 @@
/*
* 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;
/**
* Interface implemented by classes using { @link AudioRecordingMonitor} interface.
* @hide
*/
public interface AudioRecordingMonitorClient {
/**
* @return the unique port ID allocated by audio framework to this recorder
*/
int getPortId();
}

View File

@@ -0,0 +1,250 @@
/*
* 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;
}
}

View File

@@ -16,7 +16,9 @@
package android.media;
import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.UnsupportedAppUsage;
@@ -40,6 +42,8 @@ import java.io.RandomAccessFile;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
/**
* Used to record audio and video. The recording control is based on a
@@ -83,7 +87,9 @@ import java.util.List;
* <a href="{@docRoot}guide/topics/media/audio-capture.html">Audio Capture</a> developer guide.</p>
* </div>
*/
public class MediaRecorder implements AudioRouting
public class MediaRecorder implements AudioRouting,
AudioRecordingMonitor,
AudioRecordingMonitorClient
{
static {
System.loadLibrary("media_jni");
@@ -304,7 +310,7 @@ public class MediaRecorder implements AudioRouting
/**
* Audio source for preemptible, low-priority software hotword detection
* It presents the same gain and pre processing tuning as {@link #VOICE_RECOGNITION}.
* It presents the same gain and pre-processing tuning as {@link #VOICE_RECOGNITION}.
* <p>
* An application should use this audio source when it wishes to do
* always-on software hotword detection, while gracefully giving in to any other application
@@ -1471,6 +1477,57 @@ public class MediaRecorder implements AudioRouting
private native final int native_getActiveMicrophones(
ArrayList<MicrophoneInfo> activeMicrophones);
//--------------------------------------------------------------------------
// Implementation of AudioRecordingMonitor interface
//--------------------
AudioRecordingMonitorImpl mRecordingInfoImpl =
new AudioRecordingMonitorImpl((AudioRecordingMonitorClient) this);
/**
* 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) {
mRecordingInfoImpl.registerAudioRecordingCallback(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) {
mRecordingInfoImpl.unregisterAudioRecordingCallback(cb);
}
/**
* 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() {
return mRecordingInfoImpl.getActiveRecordingConfiguration();
}
//---------------------------------------------------------
// Implementation of AudioRecordingMonitorClient interface
//--------------------
/**
* @hide
*/
public int getPortId() {
return native_getPortId();
}
private native int native_getPortId();
/**
* Called from native code when an interesting event happens. This method
* just uses the EventHandler system to post the event back to the main app thread.

View File

@@ -763,6 +763,20 @@ android_media_MediaRecord_getActiveMicrophones(JNIEnv *env,
}
return jStatus;
}
static jint android_media_MediaRecord_getPortId(JNIEnv *env, jobject thiz) {
sp<MediaRecorder> mr = getMediaRecorder(env, thiz);
if (mr == NULL) {
jniThrowException(env, "java/lang/IllegalStateException", NULL);
return (jint)AUDIO_PORT_HANDLE_NONE;
}
audio_port_handle_t portId;
process_media_recorder_call(env, mr->getPortId(&portId),
"java/lang/RuntimeException", "getPortId failed.");
return (jint)portId;
}
// ----------------------------------------------------------------------------
static const JNINativeMethod gMethods[] = {
@@ -801,6 +815,7 @@ static const JNINativeMethod gMethods[] = {
{"native_enableDeviceCallback", "(Z)V", (void *)android_media_MediaRecorder_enableDeviceCallback},
{"native_getActiveMicrophones", "(Ljava/util/ArrayList;)I", (void *)android_media_MediaRecord_getActiveMicrophones},
{"native_getPortId", "()I", (void *)android_media_MediaRecord_getPortId},
};
// This function only registers the native methods, and is called from