aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPei Huang <peihuang@google.com>2023-03-01 03:06:36 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2023-03-01 03:06:36 +0000
commit8094b291ba305f85dbd1e7441931fa281fd1e891 (patch)
tree997b0a7371be9d9ab8b648a64e5652f84188bfb2
parent36fd112bc08962ea2d5ae9a4131bd70cc6f1351b (diff)
parent33d46553a70089685d229386e4559ada86d37129 (diff)
downloadsl4a-8094b291ba305f85dbd1e7441931fa281fd1e891.tar.gz
Merge "Implement recording voice during a call." am: bb781ca970 am: 33d46553a7
Original change: https://android-review.googlesource.com/c/platform/external/sl4a/+/2458488 Change-Id: Iea9893c384c1a01db1362ff9fd4c113569eefa49 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
-rw-r--r--Common/src/com/googlecode/android_scripting/facade/telephony/AudioFileInfo.java (renamed from Common/src/com/googlecode/android_scripting/facade/telephony/AutoFileInfo.java)0
-rw-r--r--Common/src/com/googlecode/android_scripting/facade/telephony/InCallServiceImpl.java113
-rw-r--r--Common/src/com/googlecode/android_scripting/facade/telephony/PlayAudioInCall.java9
-rw-r--r--Common/src/com/googlecode/android_scripting/facade/telephony/RecordVoiceInCall.java281
-rw-r--r--Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyConstants.java3
-rw-r--r--Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyManagerFacade.java30
6 files changed, 415 insertions, 21 deletions
diff --git a/Common/src/com/googlecode/android_scripting/facade/telephony/AutoFileInfo.java b/Common/src/com/googlecode/android_scripting/facade/telephony/AudioFileInfo.java
index bd5fd5ff..bd5fd5ff 100644
--- a/Common/src/com/googlecode/android_scripting/facade/telephony/AutoFileInfo.java
+++ b/Common/src/com/googlecode/android_scripting/facade/telephony/AudioFileInfo.java
diff --git a/Common/src/com/googlecode/android_scripting/facade/telephony/InCallServiceImpl.java b/Common/src/com/googlecode/android_scripting/facade/telephony/InCallServiceImpl.java
index 35e2999e..1629217a 100644
--- a/Common/src/com/googlecode/android_scripting/facade/telephony/InCallServiceImpl.java
+++ b/Common/src/com/googlecode/android_scripting/facade/telephony/InCallServiceImpl.java
@@ -18,6 +18,12 @@ package com.googlecode.android_scripting.facade.telephony;
import static com.googlecode.android_scripting.facade.telephony.InCallServiceImpl.HandleVoiceThreadState.TERMINATE;
import static com.googlecode.android_scripting.facade.telephony.InCallServiceImpl.HandleVoiceThreadState.RUN;
+import static com.googlecode.android_scripting.facade.telephony.RecordVoiceInCall.MONO_CHANNEL;
+import static com.googlecode.android_scripting.facade.telephony.RecordVoiceInCall.SAMPLE_RATE_16K;
+import static com.googlecode.android_scripting.facade.telephony.RecordVoiceInCall.SAMPLE_RATE_48K;
+import static com.googlecode.android_scripting.facade.telephony.RecordVoiceInCall.STEREO_CHANNEL;
+
+import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -46,20 +52,25 @@ public class InCallServiceImpl extends InCallService {
private static InCallServiceImpl sService = null;
private static PlayAudioInCall playAudioInCall;
+ private static RecordVoiceInCall recordVoiceInCall;
- // The call is to play an audio file or record voice during a call
+ // The call is to play an audio file or record voice.
private static Call playRecordCall = null;
// A telephony device to route voice through mobile telphony network
private static AudioDeviceInfo audioTelephonyInfo = null;
- // Indicates if the call is playing or recording audio or not
- private static HandleVoiceThreadState playRecordAudioInCallState = TERMINATE;
+ // Indicates if the call is playing audio or not
+ private static HandleVoiceThreadState playAudioInCallState = TERMINATE;
+ // Indicates if the call is recording voice or not
+ private static HandleVoiceThreadState recordVoiceInCallState = TERMINATE;
private static AudioManager mAudioManager = null;
// The audio file is to play audio on the route of telephony network
- private static File audioFile;
+ private static File playAudioFile;
+ // The audio file is to store voice wav data on the route of telephony network
+ private static File recordVoiceFile;
public static InCallServiceImpl getService() {
return sService;
@@ -643,8 +654,10 @@ public class InCallServiceImpl extends InCallService {
if (getCallId(call).equals(getCallId(playRecordCall))) {
Log.d("Terminate the call for playing/recording.");
- playRecordAudioInCallState = TERMINATE;
+ playAudioInCallState = TERMINATE;
playRecordCall = null;
+ recordVoiceInCallState = TERMINATE;
+ recordVoiceInCall = null;
}
CallListener.onCallRemoved(id, call);
@@ -1503,8 +1516,8 @@ public class InCallServiceImpl extends InCallService {
*/
public static boolean playAudioFile(String audioFileName) {
Log.d(String.format("Playing audio file \"%s\"...", audioFileName));
- if (playRecordAudioInCallState.equals(RUN)) {
- Log.d("Playing/Recording is ongoing!");
+ if (playAudioInCallState.equals(RUN)) {
+ Log.d("Playing is ongoing!");
return false;
}
if (getService() == null) {
@@ -1516,14 +1529,13 @@ public class InCallServiceImpl extends InCallService {
Log.d("No Telephony AudioDeviceInfo!");
return false;
}
- audioFile = new File(getService().getFilesDir(), audioFileName);
- if (!audioFile.exists()) {
+ playAudioFile = new File(getService().getFilesDir(), audioFileName);
+ if (!playAudioFile.exists()) {
Log.d(String.format("%s not found in files folder!",audioFileName));
return false;
}
playAudioInCall = new PlayAudioInCall(mEventFacade,
- playRecordCall, audioFile, audioTelephonyInfo);
- playRecordAudioInCallState = RUN;
+ playRecordCall, playAudioFile, audioTelephonyInfo);
return playAudioInCall.playAudioFile();
}
@@ -1544,21 +1556,88 @@ public class InCallServiceImpl extends InCallService {
return null;
}
- public static HandleVoiceThreadState getPlayRecordAudioInCallState() {
- return playRecordAudioInCallState;
+ public static HandleVoiceThreadState getPlayAudioInCallState() {
+ return playAudioInCallState;
}
- public static void setPlayRecordAudioInCallState(HandleVoiceThreadState state) {
- playRecordAudioInCallState = state;
+ public static void setPlayAudioInCallState(HandleVoiceThreadState state) {
+ playAudioInCallState = state;
}
public static void stopPlayAudioFile() {
- if (playRecordAudioInCallState.equals(RUN) && playAudioInCall != null) {
+ if (playAudioInCallState.equals(RUN) && playAudioInCall != null) {
Log.d("Stop playing audio successfully!");
- playRecordAudioInCallState = TERMINATE;
+ playAudioInCallState = TERMINATE;
}
}
+ /**
+ * Records voice during a phone call.
+ *
+ * The method checks the following items before creating a thread to record voice.
+ * <ol>
+ * <li>Check recoding state. If there is already a voice recording, ignore the request.</li>
+ * <li>Check if call has been established.</li>
+ * <li>Check the input sample rate and channel count to meet constraints.</li>
+ * <li>Check if the record wav file is created successfully.</li>
+ * </ol>
+ * @param recordWavFile indicates the wav file name of the recording voice
+ * @param sampleRate indicates sampling rate of the recording voice
+ * @param channelCount indicates voice channel number to be recorded
+ * @return {@code true} if voice is successfully recorded. Otherwise, {@code false}
+ */
+ public static boolean recordVoice(
+ String recordWavFile, int sampleRate, int channelCount, boolean cancelNoiseEcho) {
+ Log.d(String.format("Recording voice to the \"%s\" file...", recordWavFile));
+ if (getRecordVoiceInCallState().equals(RUN)) {
+ Log.d("Recording is ongoing!");
+ return false;
+ }
+ if (getService() == null) {
+ Log.d("InCallService isn't activated yet");
+ return false;
+ }
+ if (sampleRate != SAMPLE_RATE_16K && sampleRate != SAMPLE_RATE_48K) {
+ Log.e(String.format("Don't support sample rate: %d", sampleRate));
+ return false;
+ }
+ if (channelCount != MONO_CHANNEL && channelCount != STEREO_CHANNEL) {
+ Log.e(String.format("Don't support channel count: %d", channelCount));
+ return false;
+ }
+ recordVoiceFile = new File(getService().getFilesDir(), recordWavFile);
+ if (!recordVoiceFile.exists()) {
+ try {
+ Log.d(String.format("Creates a empty %s wav file to store voice data!",
+ recordWavFile));
+ recordVoiceFile.createNewFile();
+ } catch (IOException e) {
+ Log.e(String.format("Failed to create %s wav file!", recordWavFile));
+ return false;
+ }
+ }
+ Log.d(String.format("The voice recording info: wav file: %s, Sampling rate: %d, channel count: %d",
+ recordWavFile, sampleRate, channelCount));
+ recordVoiceInCall = new RecordVoiceInCall(mEventFacade, playRecordCall, recordVoiceFile,
+ sampleRate, channelCount, cancelNoiseEcho);
+ return recordVoiceInCall.recordVoice();
+ }
+
+ public static void stopRecordVoice() {
+ if (getRecordVoiceInCallState().equals(RUN) && playAudioInCall != null) {
+ Log.d("Stop recording voice successfully!");
+ setRecordVoiceInCallState(TERMINATE);
+ }
+ }
+
+ public static HandleVoiceThreadState getRecordVoiceInCallState() {
+ return recordVoiceInCallState;
+ }
+
+ public static void setRecordVoiceInCallState(HandleVoiceThreadState state) {
+ recordVoiceInCallState = state;
+ }
+
public static String getCallPresentationInfoString(int presentation) {
switch (presentation) {
case TelecomManager.PRESENTATION_ALLOWED:
diff --git a/Common/src/com/googlecode/android_scripting/facade/telephony/PlayAudioInCall.java b/Common/src/com/googlecode/android_scripting/facade/telephony/PlayAudioInCall.java
index 7021c224..f708f467 100644
--- a/Common/src/com/googlecode/android_scripting/facade/telephony/PlayAudioInCall.java
+++ b/Common/src/com/googlecode/android_scripting/facade/telephony/PlayAudioInCall.java
@@ -90,6 +90,7 @@ public class PlayAudioInCall {
byte[] audioRaw = new byte[512];
int readBytes;
try {
+ InCallServiceImpl.setPlayAudioInCallState(RUN);
InCallServiceImpl.muteCall(true);
InputStream inputStream = new FileInputStream(audioFile);
inputStream.read(audioRaw, 0, 44);
@@ -104,8 +105,6 @@ public class PlayAudioInCall {
inputStream.close();
audioTrack.stop();
audioTrack.release();
- InCallServiceImpl.setPlayRecordAudioInCallState(TERMINATE);
- InCallServiceImpl.muteCall(false);
eventFacade.postEvent(TelephonyConstants.EventCallPlayAudioStateChanged,
new InCallServiceImpl.CallEvent<String>(InCallServiceImpl.getCallId(call),
TelephonyConstants.TELEPHONY_STATE_PLAY_AUDIO_END));
@@ -117,6 +116,10 @@ public class PlayAudioInCall {
new InCallServiceImpl.CallEvent<String>(InCallServiceImpl.getCallId(call),
TelephonyConstants.TELEPHONY_STATE_PLAY_AUDIO_FAIL));
}
+ finally {
+ InCallServiceImpl.muteCall(false);
+ InCallServiceImpl.setPlayAudioInCallState(TERMINATE);
+ }
});
playAudioThread.start();
return true;
@@ -174,7 +177,7 @@ public class PlayAudioInCall {
}
private boolean stopPlayAudio() {
- if (InCallServiceImpl.getPlayRecordAudioInCallState().equals(RUN)) {
+ if (InCallServiceImpl.getPlayAudioInCallState().equals(RUN)) {
return false;
}
Log.d("Stop playing audio!");
diff --git a/Common/src/com/googlecode/android_scripting/facade/telephony/RecordVoiceInCall.java b/Common/src/com/googlecode/android_scripting/facade/telephony/RecordVoiceInCall.java
new file mode 100644
index 00000000..3b4adb67
--- /dev/null
+++ b/Common/src/com/googlecode/android_scripting/facade/telephony/RecordVoiceInCall.java
@@ -0,0 +1,281 @@
+/*
+ * Copyright (C) 2023 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.googlecode.android_scripting.facade.telephony;
+
+import static com.googlecode.android_scripting.facade.telephony.InCallServiceImpl.HandleVoiceThreadState.RUN;
+import static com.googlecode.android_scripting.facade.telephony.InCallServiceImpl.HandleVoiceThreadState.TERMINATE;
+import static java.nio.ByteOrder.LITTLE_ENDIAN;
+
+import com.googlecode.android_scripting.Log;
+import com.googlecode.android_scripting.facade.EventFacade;
+import android.telecom.Call;
+import android.media.AudioFormat;
+import android.media.AudioRecord;
+import android.media.MediaRecorder.AudioSource;
+import android.media.audiofx.AcousticEchoCanceler;
+import android.media.audiofx.AudioEffect;
+import android.media.audiofx.NoiseSuppressor;
+import com.googlecode.android_scripting.facade.telephony.InCallServiceImpl.HandleVoiceThreadState;
+import java.io.File;
+import java.io.IOException;
+import java.io.FileOutputStream;
+import java.io.DataOutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.io.RandomAccessFile;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * The class handles recording voice on the route of the telephony network during a phone
+ * call.
+ */public class RecordVoiceInCall {
+ public static final int SAMPLE_RATE_16K = 16000;
+ public static final int SAMPLE_RATE_48K = 48000;
+ public static final int MONO_CHANNEL = 1;
+ public static final int STEREO_CHANNEL = 2;
+ private static final int PCM_16_BITS = 16;
+ private EventFacade eventFacade;
+ private Call call;
+ private int sampleRate;
+ private int channelCount;
+ private File recordFile;
+ private AudioFileInfo audioFileInfo;
+ private AudioFormat audioFormat;
+ private NoiseSuppressor noiseSuppressor;
+ private AcousticEchoCanceler acousticEchoCanceler;
+ private int minRecordAudioBufferSize;
+ private boolean cancelNoiseEcho;
+ private static Thread recordVoiceThread = null;
+
+ RecordVoiceInCall(EventFacade eventFacade,
+ Call call,
+ File recordFile,
+ int sampleRate,
+ int channelCount,
+ boolean cancelNoiseEcho) {
+ this.eventFacade = eventFacade;
+ this.call = call;
+ this.recordFile = recordFile;
+ this.sampleRate = sampleRate;
+ this.channelCount = channelCount;
+ this.cancelNoiseEcho = cancelNoiseEcho;
+ Log.d(String.format("eventFacade=%s, call=%s, recordFile=%s, sampleRate=%d, channelCount=%d",
+ this.eventFacade, this.call, this.recordFile, this.sampleRate, this.channelCount));
+ }
+
+ /**Handles functional flows of voice recording and exposes to be invoked by users.*/
+ boolean recordVoice() {
+ setRecordAudioInfo();
+ setAudioFormat();
+ setMinRecordAudioBufferSize();
+ AudioRecord audioRecord = new AudioRecord.Builder()
+ .setAudioSource(AudioSource.VOICE_DOWNLINK)
+ .setAudioFormat(audioFormat)
+ .setBufferSizeInBytes(minRecordAudioBufferSize)
+ .build();
+ if (cancelNoiseEcho) {
+ enableNoiseSuppressor(audioRecord);
+ enableAcousticEchoCanceler(audioRecord);
+ }
+ return createRecordVoiceThread(audioRecord);
+ }
+
+ private void setRecordAudioInfo() {
+ audioFileInfo = AudioFileInfo.create("WAVE", sampleRate, channelCount);
+ }
+
+ /**Configures an object of {@code AudioFormat} needed for voice recording in a call.
+ * The minimal info is to provide channel count and voice encoding scheme.
+ */
+ private void setAudioFormat() {
+ int channelMask = audioFileInfo.channelCount() == 1 ? AudioFormat.CHANNEL_IN_MONO
+ : AudioFormat.CHANNEL_IN_STEREO;
+ audioFormat = new AudioFormat.Builder().setChannelMask(channelMask)
+ .setSampleRate(audioFileInfo.sampleRate()).setEncoding(AudioFormat.ENCODING_PCM_16BIT)
+ .build();
+ Log.d(String.format(
+ "Audio format for recording, channel mask: %d, sample rate: %d, encoding: PCM_16bit",
+ channelMask, sampleRate));
+ }
+
+ /**Acquires minimal buffer size for sampling voice data.*/
+ private void setMinRecordAudioBufferSize() {
+ minRecordAudioBufferSize = AudioRecord.getMinBufferSize(audioFormat.getSampleRate(),
+ audioFormat.getChannelCount(),
+ audioFormat.getEncoding());
+ Log.d(String.format("Buffer size to record voice data: %d", minRecordAudioBufferSize));
+ }
+
+ private void enableNoiseSuppressor(AudioRecord audioRecord) {
+ if (NoiseSuppressor.isAvailable()) {
+ noiseSuppressor = NoiseSuppressor.create(audioRecord.getAudioSessionId());
+ if (noiseSuppressor.setEnabled(true) != AudioEffect.SUCCESS) {
+ Log.w("Failed to enable noiseSuppressor!");
+ return;
+ }
+ Log.d("Enable noiseSuppressor!");
+ return;
+ }
+ Log.d("Noise suppressor is not available!");
+ }
+
+ private void enableAcousticEchoCanceler(AudioRecord audioRecord) {
+ if (AcousticEchoCanceler.isAvailable()) {
+ acousticEchoCanceler = AcousticEchoCanceler.create(audioRecord.getAudioSessionId());
+ if (acousticEchoCanceler.setEnabled(true) != AudioEffect.SUCCESS) {
+ Log.w("Failed to enable AcousticEchoCanceler");
+ return;
+ }
+ Log.d("Enable AcousticEchoCanceler!");
+ return;
+ }
+ Log.d("AcousticEchoCanceler is not available!");
+ }
+
+ /**Creates a background thread to perform voice recording.*/
+ private boolean createRecordVoiceThread(AudioRecord audioRecord) {
+ recordVoiceThread = new Thread(()-> {
+ int totalVoiceBytes = 0;
+ try {
+ InCallServiceImpl.setRecordVoiceInCallState(RUN);
+ InCallServiceImpl.muteCall(true);
+ audioRecord.startRecording();
+ FileOutputStream outputStream = new FileOutputStream(recordFile);
+ DataOutputStream recordVoiceOutStream = new DataOutputStream(outputStream);
+
+ writeWaveHeader(recordVoiceOutStream, totalVoiceBytes);
+ short[] buffer = new short[minRecordAudioBufferSize];
+
+ while (InCallServiceImpl.getRecordVoiceInCallState().equals(RUN)) {
+ int shortsRead = audioRecord.read(buffer, 0, minRecordAudioBufferSize);
+ totalVoiceBytes += shortsRead * 2;
+ for (int count = 0; count < shortsRead; count++) {
+ recordVoiceOutStream.writeShort(getShortByOrder(buffer[count], LITTLE_ENDIAN));
+ }
+ }
+ recordVoiceOutStream.flush();
+ recordVoiceOutStream.close();
+ correctWaveHeaderChunkSize(totalVoiceBytes);
+ Log.d("End recording voice!");
+ eventFacade.postEvent(TelephonyConstants.EventCallRecordVoiceStateChanged,
+ new InCallServiceImpl.CallEvent<String>(InCallServiceImpl.getCallId(call),
+ TelephonyConstants.TELEPHONY_STATE_RECORD_VOICE_END));
+
+ } catch (IOException e) {
+ Log.d(String.format("Failed to record voice to \"%s\"!", recordFile.getName()));
+ eventFacade.postEvent(TelephonyConstants.EventCallPlayAudioStateChanged,
+ new InCallServiceImpl.CallEvent<String>(InCallServiceImpl.getCallId(call),
+ TelephonyConstants.TELEPHONY_STATE_PLAY_AUDIO_FAIL));
+
+ } finally {
+ InCallServiceImpl.muteCall(false);
+ releaseRecordSources(audioRecord);
+ }
+ });
+ recordVoiceThread.start();
+ return true;
+ }
+
+ private void releaseRecordSources(AudioRecord audioRecord) {
+ audioRecord.stop();
+ audioRecord.release();
+ if (noiseSuppressor != null) {
+ noiseSuppressor.release();
+ }
+ if (acousticEchoCanceler != null) {
+ acousticEchoCanceler.release();
+ }
+ InCallServiceImpl.setRecordVoiceInCallState(TERMINATE);
+ }
+ /**Creates and writes wave header to the beginning of the wave file.*/
+ private void writeWaveHeader(DataOutputStream dataOutputStream, int totalBytes)
+ throws IOException {
+ /* 1-4 */
+ dataOutputStream.write("RIFF".getBytes(StandardCharsets.UTF_8));
+ /* 5-8 Write Chunk Size ~= the byte size of voice data + 36 */
+ dataOutputStream.writeInt(getIntByOrder(totalBytes + 36, LITTLE_ENDIAN));
+ /* 9-12 */
+ dataOutputStream.write("WAVE".getBytes(StandardCharsets.UTF_8));
+ /* 13-16 */
+ dataOutputStream.write("fmt ".getBytes(StandardCharsets.UTF_8));
+ /* 17-20 Writes SubChunk1Size */
+ dataOutputStream.writeInt(getIntByOrder(16, LITTLE_ENDIAN));
+ /* 21-22 Writes audio format, PCM: 1 */
+ dataOutputStream.writeShort(getShortByOrder((short) 1, LITTLE_ENDIAN));
+ /* 23-24 Writes number of channels */
+ dataOutputStream.writeShort(
+ getShortByOrder((short) audioFileInfo.channelCount(), LITTLE_ENDIAN));
+ /* 25-28 Writes sampling rate */
+ dataOutputStream.writeInt(
+ getIntByOrder(audioFileInfo.sampleRate(), LITTLE_ENDIAN));
+ /* 29-32 Writes byte rate */
+ int byteRate = audioFileInfo.sampleRate() * audioFileInfo.channelCount() * PCM_16_BITS/2;
+ dataOutputStream.writeInt(
+ getIntByOrder(byteRate, LITTLE_ENDIAN));
+ /* 33-34 Writes block align */
+ int blockAlign = audioFileInfo.channelCount() * PCM_16_BITS / 2;
+ dataOutputStream.writeShort(
+ getShortByOrder((short) blockAlign, LITTLE_ENDIAN));
+ /* 35-36 Writes PCM bits */
+ dataOutputStream.writeShort(
+ getShortByOrder((short) PCM_16_BITS, LITTLE_ENDIAN));
+ /* 37-40 */
+ dataOutputStream.write("data".getBytes(StandardCharsets.UTF_8));
+ /* 41-44 SubChunk2Size ~= the byte size of voice data */
+ dataOutputStream.writeInt(
+ getIntByOrder(totalBytes, LITTLE_ENDIAN));
+ }
+
+ private void correctWaveHeaderChunkSize(int totalVoiceBytes) throws IOException {
+ RandomAccessFile randomVoiceAccessFile = new RandomAccessFile(recordFile, "rw");
+ writeWaveHeaderChunkSize(randomVoiceAccessFile, totalVoiceBytes);
+ writeWaveHeaderSubChunk2Size(randomVoiceAccessFile, totalVoiceBytes);
+ randomVoiceAccessFile.close();
+ }
+
+ /**A wav file consists of two chunks. The first chunk is the header data which describes
+ * the sample rate, channel count, the size of voice data and so on. See {@code #writeWaveHeader}.
+ */
+ private void writeWaveHeaderChunkSize(RandomAccessFile randomAccessFile, int totalVoiceBytes)
+ throws IOException {
+ randomAccessFile.seek(4);
+ randomAccessFile.write(intToBytes(totalVoiceBytes + 36, LITTLE_ENDIAN), 0, 4);
+ }
+
+ /**A wav file consists of two chunks. The second chunk is the voice data.*/
+ private void writeWaveHeaderSubChunk2Size(RandomAccessFile randomAccessFile, int totalVoiceBytes)
+ throws IOException {
+ randomAccessFile.seek(40);
+ randomAccessFile.write(intToBytes(totalVoiceBytes, LITTLE_ENDIAN), 0, 4);
+ }
+
+ /**A short type of integer can be represented with little endian or big endian.*/
+ private short getShortByOrder(short value, ByteOrder byteOrder) {
+ byte[] bytes = ByteBuffer.allocate(2).putShort(value).array();
+ return ByteBuffer.wrap(bytes).order(byteOrder).getShort();
+ }
+
+ /**Converts integer to byte array.*/
+ private byte[] intToBytes(int value, ByteOrder byteOrder) {
+ return ByteBuffer.allocate(4).order(byteOrder).putInt(value).array();
+ }
+
+ /**An integer type of integer can be represented with little endian or big endian.*/
+ private int getIntByOrder(int value, ByteOrder byteOrder) {
+ byte[] bytes = ByteBuffer.allocate(4).putInt(value).array();
+ return ByteBuffer.wrap(bytes).order(byteOrder).getInt();
+ }
+}
diff --git a/Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyConstants.java b/Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyConstants.java
index 46755dfe..591542f6 100644
--- a/Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyConstants.java
+++ b/Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyConstants.java
@@ -246,6 +246,8 @@ public class TelephonyConstants {
public static final String TELEPHONY_STATE_UNKNOWN = "UNKNOWN";
public static final String TELEPHONY_STATE_PLAY_AUDIO_END = "PLAYAUDIOEND";
public static final String TELEPHONY_STATE_PLAY_AUDIO_FAIL = "PLAYAUDIOFAIL";
+ public static final String TELEPHONY_STATE_RECORD_VOICE_END = "RECORDVOICEEND";
+ public static final String TELEPHONY_STATE_RECORD_VOICE_FAIL = "RECORDVOICEFAIL";
/**
* Constant for TTY Mode
@@ -404,6 +406,7 @@ public class TelephonyConstants {
public static final String EventMessageWaitingIndicatorChanged = "MessageWaitingIndicatorChanged";
public static final String EventPhysicalChannelConfigChanged = "PhysicalChannelConfigChanged";
public static final String EventCallPlayAudioStateChanged = "CallPlayAudioStateChanged";
+ public static final String EventCallRecordVoiceStateChanged = "CallRecordVoiceStateChanged";
/**
* Constants for OnStartTetheringCallback
diff --git a/Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyManagerFacade.java b/Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyManagerFacade.java
index 3cda123c..ac6f9e74 100644
--- a/Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyManagerFacade.java
+++ b/Common/src/com/googlecode/android_scripting/facade/telephony/TelephonyManagerFacade.java
@@ -1636,7 +1636,8 @@ public class TelephonyManagerFacade extends RpcReceiver {
*/
@Rpc(description = "Plays the specified audio file during a phone call")
public boolean telephonyPlayAudioFile(
- @RpcParameter(name = "audioFileName") String audioFileName) {
+ @RpcParameter(name = "audioFileName", description = "the audio file in the app's files folder")
+ String audioFileName) {
Log.d(String.format("Playing audio file \"%s\"...", audioFileName));
InCallServiceImpl.setEventFacade(mEventFacade);
return InCallServiceImpl.playAudioFile(audioFileName);
@@ -1649,6 +1650,33 @@ public class TelephonyManagerFacade extends RpcReceiver {
}
/**
+ * Records voice and writes to a wav file specified by {@code recordFileName}
+ * during a phone call.
+ *
+ * @return {@code true} if voice is successfully recorded
+ */
+ @Rpc(description = "Records voice and writes to a wav file during a phone call")
+ public boolean telephonyRecordVoice(
+ @RpcParameter(name = "recordFileName", description = "The recorded voice file name")
+ String recordFileName,
+ @RpcParameter(name = "sampleRate", description = "sampling rate of voice data")
+ @RpcDefault("16000") Integer sampleRate,
+ @RpcParameter(name = "channelCount", description = "channel number of voice to record")
+ @RpcDefault("1") Integer channelCount,
+ @RpcParameter(name = "cancelNoiseEcho", description = "enable echo canceler and noise suppressor")
+ @RpcDefault("false") Boolean cancelNoiseEcho) {
+ Log.d(String.format("Recording voice to the \"%s\" file...", recordFileName));
+ InCallServiceImpl.setEventFacade(mEventFacade);
+ return InCallServiceImpl.recordVoice(recordFileName, sampleRate, channelCount, cancelNoiseEcho);
+ }
+
+ /** Stops recording voice during a phone call.*/
+ @Rpc(description = "Stops recording voice during a call.")
+ public void telephonyStopRecordVoice() {
+ InCallServiceImpl.stopRecordVoice();
+ }
+
+ /**
* Get the list of Forbidden PLMNs stored on the USIM
* profile of the SIM for the default subscription.
*/