Initial Project Import

This commit is contained in:
Moxie Marlinspike
2011-12-20 10:20:44 -08:00
commit bbea3fe1b1
397 changed files with 48065 additions and 0 deletions

View File

@@ -0,0 +1,127 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.database;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import android.content.Context;
import android.os.Environment;
import android.util.Log;
public class ApplicationExporter {
private static String getExportDirectoryPath() {
File sdDirectory = Environment.getExternalStorageDirectory();
return sdDirectory.getAbsolutePath() + File.separator + "TextSecureExport";
}
private static void verifyExternalStorageForExport() throws NoExternalStorageException {
if (!Environment.getExternalStorageDirectory().canWrite())
throw new NoExternalStorageException();
String exportDirectoryPath = getExportDirectoryPath();
File exportDirectory = new File(exportDirectoryPath);
if (!exportDirectory.exists())
exportDirectory.mkdir();
}
private static void verifyExternalStorageForImport() throws NoExternalStorageException {
if (!Environment.getExternalStorageDirectory().canRead() ||
!(new File(getExportDirectoryPath()).exists()))
throw new NoExternalStorageException();
}
private static void migrateFile(File from, File to) throws IOException {
if (from.exists()) {
FileChannel source = new FileInputStream(from).getChannel();
FileChannel destination = new FileOutputStream(to).getChannel();
destination.transferFrom(source, 0, source.size());
source.close();
destination.close();
}
}
private static void exportDirectory(Context context, String directoryName) throws IOException {
File directory = new File(context.getFilesDir().getParent() + File.separatorChar + directoryName);
File exportDirectory = new File(getExportDirectoryPath() + File.separatorChar + directoryName);
if (directory.exists()) {
exportDirectory.mkdirs();
File[] contents = directory.listFiles();
for (int i=0;i<contents.length;i++) {
File localFile = contents[i];
if (localFile.isFile()) {
File exportedFile = new File(exportDirectory.getAbsolutePath() + File.separator + localFile.getName());
migrateFile(localFile, exportedFile);
} else {
exportDirectory(context, directoryName + File.separator + localFile.getName());
}
}
} else {
Log.w("ApplicationExporter", "Could not find directory: " + directory.getAbsolutePath());
}
}
private static void importDirectory(Context context, String directoryName) throws IOException {
File directory = new File(getExportDirectoryPath() + File.separator + directoryName);
File importDirectory = new File(context.getFilesDir().getParent() + File.separator + directoryName);
if (directory.exists()) {
importDirectory.mkdirs();
File[] contents = directory.listFiles();
for (int i=0;i<contents.length;i++) {
File exportedFile = contents[i];
if (exportedFile.isFile()) {
File localFile = new File(importDirectory.getAbsolutePath() + File.separator + exportedFile.getName());
migrateFile(exportedFile, localFile);
} else {
importDirectory(context, directoryName + File.separator + exportedFile.getName());
}
}
}
}
public static void exoprtToSd(Context context) throws NoExternalStorageException, IOException {
verifyExternalStorageForExport();
exportDirectory(context, "");
// exportDirectory(context, "databases");
// exportDirectory(context, "sessions");
// exportDirectory(context, "shared_prefs");
}
public static void importFromSd(Context context) throws NoExternalStorageException, IOException {
verifyExternalStorageForImport();
importDirectory(context, "");
// importDirectory(context, "databases");
// importDirectory(context, "sessions");
// importDirectory(context, "shared_prefs");
}
}

View File

@@ -0,0 +1,166 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.database;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
public class CanonicalAddressDatabase {
private static final int DATABASE_VERSION = 1;
private static final String DATABASE_NAME = "canonical_address.db";
private static final String TABLE = "canonical_addresses";
private static final String ID_COLUMN = "_id";
private static final String ADDRESS_COLUMN = "address";
private static final String DATABASE_CREATE = "CREATE TABLE " + TABLE + " (" + ID_COLUMN + " integer PRIMARY KEY, " + ADDRESS_COLUMN + " TEXT NOT NULL);";
private static final String[] ID_PROJECTION = {ID_COLUMN};
private static final String SELECTION = "PHONE_NUMBERS_EQUAL(" + ADDRESS_COLUMN + ", ?)";
private static final Object lock = new Object();
private static CanonicalAddressDatabase instance;
private final DatabaseHelper databaseHelper;
private final HashMap<String,Long> addressCache = new HashMap<String,Long>();
private final Map<String,String> idCache = Collections.synchronizedMap(new HashMap<String,String>());
public static CanonicalAddressDatabase getInstance(Context context) {
synchronized (lock) {
if (instance == null)
instance = new CanonicalAddressDatabase(context);
return instance;
}
}
private CanonicalAddressDatabase(Context context) {
databaseHelper = new DatabaseHelper(context, DATABASE_NAME, null, DATABASE_VERSION);
}
public String getAddressFromId(String id) {
if (id == null || id.trim().equals("")) return "Anonymous";
String cachedAddress = idCache.get(id);
if (cachedAddress != null)
return cachedAddress;
Cursor cursor = null;
try {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
cursor = db.query(TABLE, null, ID_COLUMN + " = ?", new String[] {id+""}, null, null, null);
if (!cursor.moveToFirst())
return "Anonymous";
String address = cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS_COLUMN));
if (address == null || address.trim().equals("")) {
return "Anonymous";
} else {
idCache.put(id, address);
return address;
}
} finally {
if (cursor != null)
cursor.close();
}
}
public void close() {
databaseHelper.close();
instance = null;
}
public long getCanonicalAddress(String address) {
long canonicalAddress;
if ((canonicalAddress = getCanonicalAddressFromCache(address)) != -1)
return canonicalAddress;
canonicalAddress = getCanonicalAddressFromDatabase(address);
addressCache.put(address, canonicalAddress);
return canonicalAddress;
}
public List<Long> getCanonicalAddresses(List<String> addresses) {
List<Long> addressList = new LinkedList<Long>();
for (String address : addresses) {
addressList.add(getCanonicalAddress(address));
}
return addressList;
}
private long getCanonicalAddressFromCache(String address) {
if (addressCache.containsKey(address))
return new Long(addressCache.get(address));
return -1L;
}
private long getCanonicalAddressFromDatabase(String address) {
Cursor cursor = null;
try {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String[] selectionArguments = new String[] {address};
cursor = db.query(TABLE, ID_PROJECTION, SELECTION, selectionArguments, null, null, null);
if (cursor.getCount() == 0 || !cursor.moveToFirst()) {
ContentValues contentValues = new ContentValues(1);
contentValues.put(ADDRESS_COLUMN, address);
return db.insert(TABLE, ADDRESS_COLUMN, contentValues);
}
return cursor.getLong(cursor.getColumnIndexOrThrow(ID_COLUMN));
} finally {
if (cursor != null) {
cursor.close();
}
}
}
private static class DatabaseHelper extends SQLiteOpenHelper {
public DatabaseHelper(Context context, String name, CursorFactory factory, int version) {
super(context, name, factory, version);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(DATABASE_CREATE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
}

View File

@@ -0,0 +1,70 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.database;
import java.io.File;
import android.content.Context;
import android.util.Log;
public class CanonicalSessionMigrator {
private static void migrateSession(File sessionFile, File sessionsDirectory, long canonicalAddress) {
File canonicalSessionFile = new File(sessionsDirectory.getAbsolutePath() + File.separatorChar + canonicalAddress);
sessionFile.renameTo(canonicalSessionFile);
Log.w("CanonicalSessionMigrator", "Moving: " + sessionFile.toString() + " to " + canonicalSessionFile.toString());
File canonicalSessionFileLocal = new File(sessionsDirectory.getAbsolutePath() + File.separatorChar + canonicalAddress + "-local");
File localFile = new File(sessionFile.getAbsolutePath() + "-local");
if (localFile.exists())
localFile.renameTo(canonicalSessionFileLocal);
Log.w("CanonicalSessionMigrator", "Moving " + localFile + " to " + canonicalSessionFileLocal);
File canonicalSessionFileRemote = new File(sessionsDirectory.getAbsolutePath() + File.separatorChar + canonicalAddress + "-remote");
File remoteFile = new File(sessionFile.getAbsolutePath() + "-remote");
if (remoteFile.exists())
remoteFile.renameTo(canonicalSessionFileRemote);
Log.w("CanonicalSessionMigrator", "Moving " + remoteFile + " to " + canonicalSessionFileRemote);
}
public static void migrateSessions(Context context) {
if (context.getSharedPreferences("SecureSMS", Context.MODE_PRIVATE).getBoolean("canonicalized", false))
return;
CanonicalAddressDatabase canonicalDb = CanonicalAddressDatabase.getInstance(context);
File rootDirectory = context.getFilesDir();
File sessionsDirectory = new File(rootDirectory.getAbsolutePath() + File.separatorChar + "sessions");
sessionsDirectory.mkdir();
String[] files = rootDirectory.list();
for (int i=0;i<files.length;i++) {
File item = new File(rootDirectory.getAbsolutePath() + File.separatorChar + files[i]);
if (!item.isDirectory() && files[i].matches("[0-9]+")) {
long canonicalAddress = canonicalDb.getCanonicalAddress(files[i]);
migrateSession(item, sessionsDirectory, canonicalAddress);
}
}
context.getSharedPreferences("SecureSMS", Context.MODE_PRIVATE).edit().putBoolean("canonicalized", true).commit();
}
}

View File

@@ -0,0 +1,70 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.database;
import java.io.UnsupportedEncodingException;
import ws.com.google.android.mms.pdu.CharacterSets;
import ws.com.google.android.mms.pdu.EncodedStringValue;
import android.content.ContentValues;
import android.util.Log;
public class ContentValuesBuilder {
private final ContentValues contentValues;
public ContentValuesBuilder(ContentValues contentValues) {
this.contentValues = contentValues;
}
public void add(String key, String charsetKey, EncodedStringValue value) {
if (value != null) {
contentValues.put(key, toIsoString(value.getTextString()));
contentValues.put(charsetKey, value.getCharacterSet());
}
}
public void add(String contentKey, byte[] value) {
if (value != null) {
contentValues.put(contentKey, toIsoString(value));
}
}
public void add(String contentKey, int b) {
if (b != 0)
contentValues.put(contentKey, b);
}
public void add(String contentKey, long value) {
if (value != -1L)
contentValues.put(contentKey, value);
}
public ContentValues getContentValues() {
return contentValues;
}
private String toIsoString(byte[] bytes) {
try {
return new String(bytes, CharacterSets.MIMENAME_ISO_8859_1);
} catch (UnsupportedEncodingException e) {
Log.e("MmsDatabase", "ISO_8859_1 must be supported!", e);
return "";
}
}
}

View File

@@ -0,0 +1,61 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.database;
import java.util.Set;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
public abstract class Database {
protected static final String ID_WHERE = "_id = ?";
private static final String CONVERSATION_URI = "content://textsecure/thread/";
private static final String CONVERSATION_LIST_URI = "content://textsecure/conversation-list";
protected final SQLiteOpenHelper databaseHelper;
protected final Context context;
public Database(Context context, SQLiteOpenHelper databaseHelper) {
this.context = context;
this.databaseHelper = databaseHelper;
}
protected void notifyConversationListeners(Set<Long> threadIds) {
for (long threadId : threadIds)
notifyConversationListeners(threadId);
}
protected void notifyConversationListeners(long threadId) {
context.getContentResolver().notifyChange(Uri.parse(CONVERSATION_URI + threadId), null);
}
protected void notifyConversationListListeners() {
context.getContentResolver().notifyChange(Uri.parse(CONVERSATION_LIST_URI), null);
}
protected void setNotifyConverationListeners(Cursor cursor, long threadId) {
cursor.setNotificationUri(context.getContentResolver(), Uri.parse(CONVERSATION_URI + threadId));
}
protected void setNotifyConverationListListeners(Cursor cursor) {
cursor.setNotificationUri(context.getContentResolver(), Uri.parse(CONVERSATION_LIST_URI));
}
}

View File

@@ -0,0 +1,159 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.database;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
public class DatabaseFactory {
private static final int INTRODUCED_IDENTITIES_VERSION = 2;
private static final int DATABASE_VERSION = 2;
private static final String DATABASE_NAME = "messages.db";
private static final Object lock = new Object();
private static DatabaseFactory instance;
private static EncryptingMmsDatabase encryptingMmsInstance;
private static EncryptingPartDatabase encryptingPartInstance;
private final DatabaseHelper databaseHelper;
private final SmsDatabase sms;
private final EncryptingSmsDatabase encryptingSms;
private final MmsDatabase mms;
private final PartDatabase part;
private final ThreadDatabase thread;
private final CanonicalAddressDatabase address;
private final MmsAddressDatabase mmsAddress;
private final MmsSmsDatabase mmsSmsDatabase;
private final IdentityDatabase identityDatabase;
public static DatabaseFactory getInstance(Context context) {
synchronized (lock) {
if (instance == null)
instance = new DatabaseFactory(context);
return instance;
}
}
public static MmsSmsDatabase getMmsSmsDatabase(Context context) {
return getInstance(context).mmsSmsDatabase;
}
public static ThreadDatabase getThreadDatabase(Context context) {
return getInstance(context).thread;
}
public static SmsDatabase getSmsDatabase(Context context) {
return getInstance(context).sms;
}
public static MmsDatabase getMmsDatabase(Context context) {
return getInstance(context).mms;
}
public static CanonicalAddressDatabase getAddressDatabase(Context context) {
return getInstance(context).address;
}
public static EncryptingSmsDatabase getEncryptingSmsDatabase(Context context) {
return getInstance(context).encryptingSms;
}
public static EncryptingMmsDatabase getEncryptingMmsDatabase(Context context, MasterSecret masterSecret) {
synchronized (lock) {
if (encryptingMmsInstance == null) {
DatabaseFactory factory = getInstance(context);
encryptingMmsInstance = new EncryptingMmsDatabase(context, factory.databaseHelper, masterSecret);
}
return encryptingMmsInstance;
}
}
public static PartDatabase getPartDatabase(Context context) {
return getInstance(context).part;
}
public static EncryptingPartDatabase getEncryptingPartDatabase(Context context, MasterSecret masterSecret) {
synchronized (lock) {
if (encryptingPartInstance == null) {
DatabaseFactory factory = getInstance(context);
encryptingPartInstance = new EncryptingPartDatabase(context, factory.databaseHelper, masterSecret);
}
return encryptingPartInstance;
}
}
public static MmsAddressDatabase getMmsAddressDatabase(Context context) {
return getInstance(context).mmsAddress;
}
public static IdentityDatabase getIdentityDatabase(Context context) {
return getInstance(context).identityDatabase;
}
private DatabaseFactory(Context context) {
this.databaseHelper = new DatabaseHelper(context, DATABASE_NAME, null, DATABASE_VERSION);
this.sms = new SmsDatabase(context, databaseHelper);
this.encryptingSms = new EncryptingSmsDatabase(context, databaseHelper);
this.mms = new MmsDatabase(context, databaseHelper);
this.part = new PartDatabase(context, databaseHelper);
this.thread = new ThreadDatabase(context, databaseHelper);
this.address = CanonicalAddressDatabase.getInstance(context);
this.mmsAddress = new MmsAddressDatabase(context, databaseHelper);
this.mmsSmsDatabase = new MmsSmsDatabase(context, databaseHelper);
this.identityDatabase = new IdentityDatabase(context, databaseHelper);
}
public void close() {
databaseHelper.close();
address.close();
instance = null;
}
private static class DatabaseHelper extends SQLiteOpenHelper {
public DatabaseHelper(Context context, String name, CursorFactory factory, int version) {
super(context, name, factory, version);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(SmsDatabase.CREATE_TABLE);
db.execSQL(MmsDatabase.CREATE_TABLE);
db.execSQL(PartDatabase.CREATE_TABLE);
db.execSQL(ThreadDatabase.CREATE_TABLE);
db.execSQL(MmsAddressDatabase.CREATE_TABLE);
db.execSQL(IdentityDatabase.CREATE_TABLE);
// db.execSQL(CanonicalAddress.CREATE_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion < INTRODUCED_IDENTITIES_VERSION)
db.execSQL(IdentityDatabase.CREATE_TABLE);
}
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.database;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import android.content.Context;
import android.database.sqlite.SQLiteOpenHelper;
public class EncryptingMmsDatabase extends MmsDatabase {
private final MasterSecret masterSecret;
public EncryptingMmsDatabase(Context context, SQLiteOpenHelper databaseHelper, MasterSecret masterSecret) {
super(context, databaseHelper);
this.masterSecret = masterSecret;
}
@Override
protected PartDatabase getPartDatabase() {
return DatabaseFactory.getEncryptingPartDatabase(context, masterSecret);
}
}

View File

@@ -0,0 +1,57 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.database;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.EncryptingPartOutputStream;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import ws.com.google.android.mms.pdu.PduPart;
import android.content.Context;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
public class EncryptingPartDatabase extends PartDatabase {
private final MasterSecret masterSecret;
public EncryptingPartDatabase(Context context, SQLiteOpenHelper databaseHelper, MasterSecret masterSecret) {
super(context, databaseHelper);
this.masterSecret = masterSecret;
}
@Override
protected FileInputStream getPartInputStream(File path, PduPart part) throws FileNotFoundException {
Log.w("EncryptingPartDatabase", "Getting part at: " + path.getAbsolutePath());
if (!part.getEncrypted())
return super.getPartInputStream(path, part);
return new DecryptingPartInputStream(path, masterSecret);
}
@Override
protected FileOutputStream getPartOutputStream(File path, PduPart part) throws FileNotFoundException {
Log.w("EncryptingPartDatabase", "Writing part to: " + path.getAbsolutePath());
part.setEncrypted(true);
return new EncryptingPartOutputStream(path, masterSecret);
}
}

View File

@@ -0,0 +1,78 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.database;
import org.thoughtcrime.securesms.crypto.AsymmetricMasterCipher;
import org.thoughtcrime.securesms.crypto.AsymmetricMasterSecret;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.protocol.Prefix;
import android.content.Context;
import android.database.sqlite.SQLiteOpenHelper;
import android.telephony.SmsMessage;
public class EncryptingSmsDatabase extends SmsDatabase {
public EncryptingSmsDatabase(Context context, SQLiteOpenHelper databaseHelper) {
super(context, databaseHelper);
}
private String getAsymmetricEncryptedBody(AsymmetricMasterSecret masterSecret, String body) {
AsymmetricMasterCipher bodyCipher = new AsymmetricMasterCipher(masterSecret);
return Prefix.ASYMMETRIC_LOCAL_ENCRYPT + bodyCipher.encryptBody(body);
}
private String getEncryptedBody(MasterSecret masterSecret, String body) {
MasterCipher bodyCipher = new MasterCipher(masterSecret);
return Prefix.SYMMETRIC_ENCRYPT + bodyCipher.encryptBody(body);
}
private long insertMessageSent(MasterSecret masterSecret, String address, long threadId, String body, long date, int type) {
String encryptedBody = getEncryptedBody(masterSecret, body);
return insertMessageSent(address, threadId, encryptedBody, date, type);
}
public void updateSecureMessageBody(MasterSecret masterSecret, long messageId, String body) {
String encryptedBody = getEncryptedBody(masterSecret, body);
updateMessageBodyAndType(messageId, encryptedBody, Types.SECURE_RECEIVED_TYPE);
}
public void updateMessageBody(MasterSecret masterSecret, long messageId, String body) {
String encryptedBody = getEncryptedBody(masterSecret, body);
updateMessageBodyAndType(messageId, encryptedBody, Types.INBOX_TYPE);
}
public long insertMessageSent(MasterSecret masterSecret, String address, long threadId, String body, long date) {
return insertMessageSent(masterSecret, address, threadId, body, date, Types.ENCRYPTED_OUTBOX_TYPE);
}
public long insertSecureMessageSent(MasterSecret masterSecret, String address, long threadId, String body, long date) {
return insertMessageSent(masterSecret, address, threadId, body, date, Types.ENCRYPTING_TYPE);
}
public long insertMessageReceived(MasterSecret masterSecret, SmsMessage message, String body) {
String encryptedBody = getEncryptedBody(masterSecret, body);
return insertMessageReceived(message, encryptedBody);
}
public long insertMessageReceived(AsymmetricMasterSecret masterSecret, SmsMessage message, String body) {
String encryptedBody = getAsymmetricEncryptedBody(masterSecret, body);
return insertSecureMessageReceived(message, encryptedBody);
}
}

View File

@@ -0,0 +1,125 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.database;
import java.io.IOException;
import org.thoughtcrime.securesms.crypto.IdentityKey;
import org.thoughtcrime.securesms.crypto.InvalidKeyException;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.util.Base64;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.util.Log;
public class IdentityDatabase extends Database {
private static final Uri CHANGE_URI = Uri.parse("content://textsecure/identities");
private static final String TABLE_NAME = "identities";
private static final String ID = "_id";
public static final String IDENTITY_KEY = "key";
public static final String IDENTITY_NAME = "name";
public static final String MAC = "mac";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
IDENTITY_KEY + " TEXT UNIQUE, " + IDENTITY_NAME + " TEXT UNIQUE, " +
MAC + " TEXT);";
public IdentityDatabase(Context context, SQLiteOpenHelper databaseHelper) {
super(context, databaseHelper);
}
public Cursor getIdentities() {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
Cursor cursor = database.query(TABLE_NAME, null, null, null, null, null, null);
if (cursor != null)
cursor.setNotificationUri(context.getContentResolver(), CHANGE_URI);
return cursor;
}
public String getNameForIdentity(MasterSecret masterSecret, IdentityKey identityKey) {
MasterCipher masterCipher = new MasterCipher(masterSecret);
SQLiteDatabase database = databaseHelper.getReadableDatabase();
Cursor cursor = null;
Log.w("IdentityDatabase", "Querying for: " + Base64.encodeBytes(identityKey.serialize()));
try {
cursor = database.query(TABLE_NAME, null, IDENTITY_KEY + " = ?", new String[] {Base64.encodeBytes(identityKey.serialize())}, null, null, null);
if (cursor == null || !cursor.moveToFirst())
return null;
String identityName = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_NAME));
String identityKeyString = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY));
byte[] mac = Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(MAC)));
if (!masterCipher.verifyMacFor(identityName + identityKeyString, mac)) {
Log.w("IdentityDatabase", "Mac check failed!");
return null;
}
Log.w("IdentityDatabase", "Returning identity name: " + identityName);
return identityName;
} catch (IOException e) {
Log.w("IdentityDatabase", e);
return null;
} finally {
if (cursor != null)
cursor.close();
}
}
public void saveIdentity(MasterSecret masterSecret, IdentityKey identityKey, String tagName) throws InvalidKeyException {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
MasterCipher masterCipher = new MasterCipher(masterSecret);
String identityKeyString = Base64.encodeBytes(identityKey.serialize());
String macString = Base64.encodeBytes(masterCipher.getMacFor(tagName + identityKeyString));
ContentValues contentValues = new ContentValues();
contentValues.put(IDENTITY_KEY, identityKeyString);
contentValues.put(IDENTITY_NAME, tagName);
contentValues.put(MAC, macString);
long id = database.insert(TABLE_NAME, null, contentValues);
if (id == -1)
throw new InvalidKeyException("Error inserting key!");
context.getContentResolver().notifyChange(CHANGE_URI, null);
}
public void deleteIdentity(String name, String keyString) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
String where = IDENTITY_NAME + " = ? AND " + IDENTITY_KEY + " = ?";
String[] args = new String[] {name, keyString};
database.delete(TABLE_NAME, where, args);
context.getContentResolver().notifyChange(CHANGE_URI, null);
}
}

View File

@@ -0,0 +1,40 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.database;
public class InvalidKeyIdException extends Exception {
public InvalidKeyIdException() {
// TODO Auto-generated constructor stub
}
public InvalidKeyIdException(String detailMessage) {
super(detailMessage);
// TODO Auto-generated constructor stub
}
public InvalidKeyIdException(Throwable throwable) {
super(throwable);
// TODO Auto-generated constructor stub
}
public InvalidKeyIdException(String detailMessage, Throwable throwable) {
super(detailMessage, throwable);
// TODO Auto-generated constructor stub
}
}

View File

@@ -0,0 +1,146 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.database;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import org.thoughtcrime.securesms.crypto.InvalidKeyException;
import org.thoughtcrime.securesms.crypto.KeyPair;
import org.thoughtcrime.securesms.crypto.KeyUtil;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Hex;
import android.content.Context;
import android.util.Log;
public class LocalKeyRecord extends Record {
private static final Object FILE_LOCK = new Object();
private KeyPair localCurrentKeyPair;
private KeyPair localNextKeyPair;
private final MasterCipher masterCipher;
private final MasterSecret masterSecret;
public LocalKeyRecord(Context context, MasterSecret masterSecret, Recipient recipient) {
super(context, getFileNameForRecipient(context, recipient));
this.masterSecret = masterSecret;
this.masterCipher = new MasterCipher(masterSecret);
loadData();
}
public static boolean hasRecord(Context context, Recipient recipient) {
Log.w("LocalKeyRecord", "Checking: " + getFileNameForRecipient(context, recipient));
return Record.hasRecord(context, getFileNameForRecipient(context, recipient));
}
public static void delete(Context context, Recipient recipient) {
Record.delete(context, getFileNameForRecipient(context, recipient));
}
private static String getFileNameForRecipient(Context context, Recipient recipient) {
return CanonicalAddressDatabase.getInstance(context).getCanonicalAddress(recipient.getNumber()) + "-local";
}
public void advanceKeyIfNecessary(int keyId) {
Log.w("LocalKeyRecord", "Remote client acknowledges receiving key id: " + keyId);
if (keyId == localNextKeyPair.getId()) {
this.localCurrentKeyPair = this.localNextKeyPair;
this.localNextKeyPair = new KeyPair(this.localNextKeyPair.getId()+1, KeyUtil.generateKeyPair(), masterSecret);
}
}
public void setCurrentKeyPair(KeyPair localCurrentKeyPair) {
this.localCurrentKeyPair = localCurrentKeyPair;
}
public void setNextKeyPair(KeyPair localNextKeyPair) {
this.localNextKeyPair = localNextKeyPair;
}
public KeyPair getCurrentKeyPair() {
return this.localCurrentKeyPair;
}
public KeyPair getNextKeyPair() {
return this.localNextKeyPair;
}
public KeyPair getKeyPairForId(int id) throws InvalidKeyIdException {
if (this.localCurrentKeyPair.getId() == id) return this.localCurrentKeyPair;
else if (this.localNextKeyPair.getId() == id) return this.localNextKeyPair;
else throw new InvalidKeyIdException("No local key for ID: " + id);
}
public void save() {
synchronized (FILE_LOCK) {
try {
RandomAccessFile file = openRandomAccessFile();
FileChannel out = file.getChannel();
out.position(0);
writeKeyPair(localCurrentKeyPair, out);
writeKeyPair(localNextKeyPair, out);
out.force(true);
out.truncate(out.position());
out.close();
file.close();
} catch (IOException ioe) {
Log.w("keyrecord", ioe);
// XXX
}
}
}
private void loadData() {
Log.w("LocalKeyRecord", "Loading local key record...");
synchronized (FILE_LOCK) {
try {
FileInputStream in = this.openInputStream();
localCurrentKeyPair = readKeyPair(in);
localNextKeyPair = readKeyPair(in);
in.close();
} catch (FileNotFoundException e) {
Log.w("LocalKeyRecord", "No local keypair set found.");
return;
} catch (IOException ioe) {
Log.w("keyrecord", ioe);
// XXX
} catch (InvalidKeyException ike) {
Log.w("LocalKeyRecord", ike);
}
}
}
private void writeKeyPair(KeyPair keyPair, FileChannel out) throws IOException {
byte[] keyPairBytes = keyPair.toBytes();
writeBlob(keyPairBytes, out);
}
private KeyPair readKeyPair(FileInputStream in) throws IOException, InvalidKeyException {
byte[] keyPairBytes = readBlob(in);
return new KeyPair(keyPairBytes, masterCipher);
}
}

View File

@@ -0,0 +1,181 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.database;
import org.thoughtcrime.securesms.ConversationItem;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
public class MessageRecord {
private long id;
private long threadId;
private Recipient messageRecipient;
private Recipients recipients;
private String body;
private long date;
private long count;
private boolean read;
private long type;
private boolean emphasis;
private boolean keyExchange;
private boolean processedKeyExchange;
private boolean staleKeyExchange;
public MessageRecord(MessageRecord copy) {
this.id = copy.id;
this.threadId = copy.threadId;
this.messageRecipient = copy.messageRecipient;
this.recipients = copy.recipients;
this.body = copy.body;
this.date = copy.date;
this.count = copy.count;
this.read = copy.read;
this.type = copy.type;
this.emphasis = copy.emphasis;
this.keyExchange = copy.keyExchange;
this.processedKeyExchange = copy.processedKeyExchange;
}
public MessageRecord(long id, Recipients recipients, long date, long type, long threadId) {
this.id = id;
this.date = date;
this.type = type;
this.recipients = recipients;
this.threadId = threadId;
}
public MessageRecord(long id, Recipients recipients, long date, long count, boolean read, long threadId) {
this.id = id;
this.threadId = threadId;
this.recipients = recipients;
this.date = date;
this.count = count;
this.read = read;
}
public void setOnConversationItem(ConversationItem item) {
item.setMessageRecord(this);
}
public boolean isMms() {
return false;
}
public long getType() {
return type;
}
public void setMessageRecipient(Recipient recipient) {
this.messageRecipient = recipient;
}
public Recipient getMessageRecipient() {
return this.messageRecipient;
}
public void setEmphasis(boolean emphasis) {
this.emphasis = emphasis;
}
public boolean getEmphasis() {
return this.emphasis;
}
public void setId(long id) {
this.id = id;
}
public void setBody(String body) {
this.body = body;
}
public long getThreadId() {
return threadId;
}
public long getId() {
return id;
}
public Recipients getRecipients() {
return recipients;
}
public String getBody() {
return body;
}
public long getDate() {
return date;
}
public long getCount() {
return count;
}
public boolean getRead() {
return read;
}
public boolean isStaleKeyExchange() {
return this.staleKeyExchange;
}
public void setStaleKeyExchange(boolean staleKeyExchange) {
this.staleKeyExchange = staleKeyExchange;
}
public boolean isProcessedKeyExchange() {
return processedKeyExchange;
}
public void setProcessedKeyExchange(boolean processedKeyExchange) {
this.processedKeyExchange = processedKeyExchange;
}
public boolean isKeyExchange() {
return keyExchange || processedKeyExchange || staleKeyExchange;
}
public void setKeyExchange(boolean keyExchange) {
this.keyExchange = keyExchange;
}
public boolean isFailedDecryptType() {
return type == SmsDatabase.Types.FAILED_DECRYPT_TYPE;
}
public boolean isFailed() {
return SmsDatabase.Types.isFailedMessageType(type);
}
public boolean isOutgoing() {
return SmsDatabase.Types.isOutgoingMessageType(type);
}
public boolean isPending() {
return SmsDatabase.Types.isPendingMessageType(type);
}
public boolean isSecure() {
return SmsDatabase.Types.isSecureType(type);
}
}

View File

@@ -0,0 +1,135 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.database;
import java.io.UnsupportedEncodingException;
import ws.com.google.android.mms.pdu.CharacterSets;
import ws.com.google.android.mms.pdu.EncodedStringValue;
import ws.com.google.android.mms.pdu.PduHeaders;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
public class MmsAddressDatabase extends Database {
private static final String TABLE_NAME = "mms_addresses";
private static final String ID = "_id";
private static final String MMS_ID = "mms_id";
private static final String TYPE = "type";
private static final String ADDRESS = "address";
private static final String ADDRESS_CHARSET = "address_charset";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
MMS_ID + " INTEGER, " + TYPE + " INTEGER, " + ADDRESS + " TEXT, " +
ADDRESS_CHARSET + " INTEGER);";
public MmsAddressDatabase(Context context, SQLiteOpenHelper databaseHelper) {
super(context, databaseHelper);
}
private void insertAddress(long messageId, int type, EncodedStringValue address) {
if (address != null) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues();
contentValues.put(MMS_ID, messageId);
contentValues.put(TYPE, type);
contentValues.put(ADDRESS, toIsoString(address.getTextString()));
contentValues.put(ADDRESS_CHARSET, address.getCharacterSet());
database.insert(TABLE_NAME, null, contentValues);
}
}
private void insertAddress(long messageId, int type, EncodedStringValue[] addresses) {
if (addresses != null) {
for (int i=0;i<addresses.length;i++) {
insertAddress(messageId, type, addresses[i]);
}
}
}
private void addAddress(Cursor cursor, PduHeaders headers) {
long type = cursor.getLong(cursor.getColumnIndexOrThrow(TYPE));
String address = cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS));
long charset = cursor.getLong(cursor.getColumnIndexOrThrow(ADDRESS_CHARSET));
EncodedStringValue encodedAddress = new EncodedStringValue((int)charset, getBytes(address));
if (type == PduHeaders.FROM)
headers.setEncodedStringValue(encodedAddress, PduHeaders.FROM);
else
headers.appendEncodedStringValue(encodedAddress, (int)type);
}
public void insertAddressesForId(long messageId, PduHeaders headers) {
insertAddress(messageId, PduHeaders.FROM, headers.getEncodedStringValue(PduHeaders.FROM));
insertAddress(messageId, PduHeaders.TO, headers.getEncodedStringValues(PduHeaders.TO));
insertAddress(messageId, PduHeaders.CC, headers.getEncodedStringValues(PduHeaders.CC));
insertAddress(messageId, PduHeaders.BCC, headers.getEncodedStringValues(PduHeaders.BCC));
}
public void getAddressesForId(long messageId, PduHeaders headers) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
Cursor cursor = null;
try {
cursor = database.query(TABLE_NAME, null, MMS_ID + " = ?", new String[] {messageId+""}, null, null, null);
while (cursor != null && cursor.moveToNext()) {
addAddress(cursor, headers);
}
} finally {
if (cursor != null)
cursor.close();
}
}
public void deleteAddressesForId(long messageId) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
database.delete(TABLE_NAME, MMS_ID + " = ?", new String[] {messageId+""});
}
public void deleteAllAddresses() {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
database.delete(TABLE_NAME, null, null);
}
private byte[] getBytes(String data) {
try {
return data.getBytes(CharacterSets.MIMENAME_ISO_8859_1);
} catch (UnsupportedEncodingException e) {
Log.e("PduHeadersBuilder", "ISO_8859_1 must be supported!", e);
return new byte[0];
}
}
private String toIsoString(byte[] bytes) {
try {
return new String(bytes, CharacterSets.MIMENAME_ISO_8859_1);
} catch (UnsupportedEncodingException e) {
Log.e("MmsDatabase", "ISO_8859_1 must be supported!", e);
return "";
}
}
}

View File

@@ -0,0 +1,580 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.database;
import java.io.UnsupportedEncodingException;
import java.util.HashSet;
import java.util.Set;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
import ws.com.google.android.mms.InvalidHeaderValueException;
import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.CharacterSets;
import ws.com.google.android.mms.pdu.EncodedStringValue;
import ws.com.google.android.mms.pdu.MultimediaMessagePdu;
import ws.com.google.android.mms.pdu.NotificationInd;
import ws.com.google.android.mms.pdu.PduBody;
import ws.com.google.android.mms.pdu.PduHeaders;
import ws.com.google.android.mms.pdu.RetrieveConf;
import ws.com.google.android.mms.pdu.SendReq;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.util.Log;
public class MmsDatabase extends Database {
public static final String TABLE_NAME = "mms";
public static final String ID = "_id";
private static final String THREAD_ID = "thread_id";
private static final String DATE = "date";
public static final String MESSAGE_BOX = "msg_box";
private static final String READ = "read";
private static final String MESSAGE_ID = "m_id";
private static final String SUBJECT = "sub";
private static final String SUBJECT_CHARSET = "sub_cs";
private static final String CONTENT_TYPE = "ct_t";
private static final String CONTENT_LOCATION = "ct_l";
private static final String EXPIRY = "exp";
private static final String MESSAGE_CLASS = "m_cls";
public static final String MESSAGE_TYPE = "m_type";
private static final String MMS_VERSION = "v";
private static final String MESSAGE_SIZE = "m_size";
private static final String PRIORITY = "pri";
private static final String READ_REPORT = "rr";
private static final String REPORT_ALLOWED = "rpt_a";
private static final String RESPONSE_STATUS = "resp_st";
private static final String STATUS = "st";
private static final String TRANSACTION_ID = "tr_id";
private static final String RETRIEVE_STATUS = "retr_st";
private static final String RETRIEVE_TEXT = "retr_txt";
private static final String RETRIEVE_TEXT_CS = "retr_txt_cs";
private static final String READ_STATUS = "read_status";
private static final String CONTENT_CLASS = "ct_cls";
private static final String RESPONSE_TEXT = "resp_txt";
private static final String DELIVERY_TIME = "d_tm";
private static final String DELIVERY_REPORT = "d_rpt";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
THREAD_ID + " INTEGER, " + DATE + " INTEGER, " + MESSAGE_BOX + " INTEGER, " +
READ + " INTEGER DEFAULT 0, " + MESSAGE_ID + " TEXT, " + SUBJECT + " TEXT, " +
SUBJECT_CHARSET + " INTEGER, " + CONTENT_TYPE + " TEXT, " + CONTENT_LOCATION + " TEXT, " +
EXPIRY + " INTEGER, " + MESSAGE_CLASS + " TEXT, " + MESSAGE_TYPE + " INTEGER, " +
MMS_VERSION + " INTEGER, " + MESSAGE_SIZE + " INTEGER, " + PRIORITY + " INTEGER, " +
READ_REPORT + " INTEGER, " + REPORT_ALLOWED + " INTEGER, " + RESPONSE_STATUS + " INTEGER, " +
STATUS + " INTEGER, " + TRANSACTION_ID + " TEXT, " + RETRIEVE_STATUS + " INTEGER, " +
RETRIEVE_TEXT + " TEXT, " + RETRIEVE_TEXT_CS + " INTEGER, " + READ_STATUS + " INTEGER, " +
CONTENT_CLASS + " INTEGER, " + RESPONSE_TEXT + " TEXT, " + DELIVERY_TIME + " INTEGER, " +
DELIVERY_REPORT + " INTEGER);";
public MmsDatabase(Context context, SQLiteOpenHelper databaseHelper) {
super(context, databaseHelper);
}
public int getMessageCountForThread(long threadId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = null;
try {
cursor = db.query(TABLE_NAME, new String[] {"COUNT(*)"}, THREAD_ID + " = ?", new String[] {threadId+""}, null, null, null);
if (cursor != null && cursor.moveToFirst())
return cursor.getInt(0);
} finally {
if (cursor != null)
cursor.close();
}
return 0;
}
public long getThreadIdForMessage(long id) {
String sql = "SELECT " + THREAD_ID + " FROM " + TABLE_NAME + " WHERE " + ID + " = ?";
String[] sqlArgs = new String[] {id+""};
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = null;
try {
cursor = db.rawQuery(sql, sqlArgs);
if (cursor != null && cursor.moveToFirst())
return cursor.getLong(0);
else
return -1;
} finally {
if (cursor != null)
cursor.close();
}
}
private long getThreadIdForHeaders(PduHeaders headers) throws RecipientFormattingException {
try {
EncodedStringValue encodedString = headers.getEncodedStringValue(PduHeaders.FROM);
String fromString = new String(encodedString.getTextString(), CharacterSets.MIMENAME_ISO_8859_1);
Recipients recipients = RecipientFactory.getRecipientsFromString(context, fromString);
return DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
} catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
}
}
public String getMessageRecipient(long messageId) {
try {
PduHeaders headers = new PduHeaders();
MmsAddressDatabase database = DatabaseFactory.getMmsAddressDatabase(context);
database.getAddressesForId(messageId, headers);
EncodedStringValue encodedFrom = headers.getEncodedStringValue(PduHeaders.FROM);
if (encodedFrom != null)
return new String(encodedFrom.getTextString(), CharacterSets.MIMENAME_ISO_8859_1);
else
return "Anonymous";
} catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
}
}
public void updateResponseStatus(long messageId, int status) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues();
contentValues.put(RESPONSE_STATUS, status);
database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {messageId+""});
}
public void markAsSentFailed(long messageId) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues();
contentValues.put(MESSAGE_BOX, Types.MESSAGE_BOX_SENT_FAILED);
database.update(TABLE_NAME, contentValues, ID_WHERE, new String[]{messageId+""});
notifyConversationListeners(getThreadIdForMessage(messageId));
}
public void markAsSent(long messageId, byte[] mmsId, long status) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues();
contentValues.put(RESPONSE_STATUS, status);
contentValues.put(MESSAGE_ID, new String(mmsId));
contentValues.put(MESSAGE_BOX, Types.MESSAGE_BOX_SENT);
database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {messageId+""});
notifyConversationListeners(getThreadIdForMessage(messageId));
}
public void markAsSecureSent(long messageId, byte[] mmsId, long status) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues();
contentValues.put(RESPONSE_STATUS, status);
contentValues.put(MESSAGE_ID, new String(mmsId));
contentValues.put(MESSAGE_BOX, Types.MESSAGE_BOX_SECURE_SENT);
database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {messageId+""});
notifyConversationListeners(getThreadIdForMessage(messageId));
}
public void markDownloadState(long messageId, long state) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues();
contentValues.put(STATUS, state);
database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {messageId+""});
notifyConversationListeners(getThreadIdForMessage(messageId));
}
public void markAsNoSession(long messageId, long threadId) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues();
contentValues.put(MESSAGE_BOX, Types.MESSAGE_BOX_NO_SESSION_INBOX);
database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {messageId+""});
notifyConversationListeners(threadId);
}
public void markAsDecryptFailed(long messageId, long threadId) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues();
contentValues.put(MESSAGE_BOX, Types.MESSAGE_BOX_DECRYPT_FAILED_INBOX);
database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {messageId+""});
notifyConversationListeners(threadId);
}
public void setMessagesRead(long threadId) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues();
contentValues.put(READ, 1);
database.update(TABLE_NAME, contentValues, THREAD_ID + " = ?", new String[] {threadId+""});
}
public NotificationInd getNotificationMessage(long messageId) throws MmsException {
PduHeaders headers = getHeadersForId(messageId);
return new NotificationInd(headers);
}
public MultimediaMessagePdu getMediaMessage(long messageId) throws MmsException {
PduHeaders headers = getHeadersForId(messageId);
PartDatabase partDatabase = getPartDatabase();
PduBody body = partDatabase.getParts(messageId, false);
return new MultimediaMessagePdu(headers, body);
}
public SendReq getSendRequest(long messageId) throws MmsException {
PduHeaders headers = getHeadersForId(messageId);
PartDatabase partDatabase = getPartDatabase();
PduBody body = partDatabase.getParts(messageId, true);
return new SendReq(headers, body, messageId, headers.getMessageBox());
}
public SendReq[] getOutgoingMessages() throws MmsException {
MmsAddressDatabase addr = DatabaseFactory.getMmsAddressDatabase(context);
PartDatabase parts = getPartDatabase();
SQLiteDatabase database = databaseHelper.getReadableDatabase();
Cursor cursor = null;
try {
cursor = database.query(TABLE_NAME, null, MESSAGE_BOX + " = ? OR " + MESSAGE_BOX + " = ?", new String[] {Types.MESSAGE_BOX_OUTBOX+"", Types.MESSAGE_BOX_SECURE_OUTBOX+""}, null, null, null);
if (cursor == null || cursor.getCount() == 0)
return new SendReq[0];
SendReq[] requests = new SendReq[cursor.getCount()];
int i = 0;
while (cursor.moveToNext()) {
long messageId = cursor.getLong(cursor.getColumnIndexOrThrow(ID));
long outboxType = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX));
PduHeaders headers = getHeadersFromCursor(cursor);
addr.getAddressesForId(messageId, headers);
PduBody body = parts.getParts(messageId, true);
requests[i++] = new SendReq(headers, body, messageId, outboxType);
}
return requests;
} finally {
if (cursor != null)
cursor.close();
}
}
private long insertMessageReceived(MultimediaMessagePdu retrieved, String contentLocation, long threadId, long mailbox) throws MmsException {
PduHeaders headers = retrieved.getPduHeaders();
ContentValues contentValues = getContentValuesFromHeader(headers);
contentValues.put(MESSAGE_BOX, mailbox);
contentValues.put(THREAD_ID, threadId);
contentValues.put(CONTENT_LOCATION, contentLocation);
contentValues.put(STATUS, Types.DOWNLOAD_INITIALIZED);
long messageId = insertMediaMessage(retrieved, contentValues);
return messageId;
}
public long insertMessageReceived(RetrieveConf retrieved, String contentLocation, long threadId) throws MmsException {
return insertMessageReceived(retrieved, contentLocation, threadId, Types.MESSAGE_BOX_INBOX);
}
public long insertSecureMessageReceived(RetrieveConf retrieved, String contentLocation, long threadId) throws MmsException {
return insertMessageReceived(retrieved, contentLocation, threadId, Types.MESSAGE_BOX_DECRYPTING_INBOX);
}
public long insertSecureDecryptedMessageReceived(MultimediaMessagePdu retrieved, long threadId) throws MmsException {
return insertMessageReceived(retrieved, "", threadId, Types.MESSAGE_BOX_SECURE_INBOX);
}
public long insertMessageReceived(NotificationInd notification) {
try {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
PduHeaders headers = notification.getPduHeaders();
ContentValues contentValues = getContentValuesFromHeader(headers);
long threadId = getThreadIdForHeaders(headers);
MmsAddressDatabase addressDatabase = DatabaseFactory.getMmsAddressDatabase(context);
Log.w("MmsDatabse", "Message received type: " + headers.getOctet(PduHeaders.MESSAGE_TYPE));
contentValues.put(MESSAGE_BOX, Types.MESSAGE_BOX_INBOX);
contentValues.put(THREAD_ID, threadId);
contentValues.put(STATUS, Types.DOWNLOAD_INITIALIZED);
if (!contentValues.containsKey(DATE))
contentValues.put(DATE, System.currentTimeMillis() / 1000);
long messageId = db.insert(TABLE_NAME, null, contentValues);
addressDatabase.insertAddressesForId(messageId, headers);
notifyConversationListeners(threadId);
DatabaseFactory.getThreadDatabase(context).update(threadId);
DatabaseFactory.getThreadDatabase(context).setUnread(threadId);
return messageId;
} catch (RecipientFormattingException rfe) {
Log.w("MmsDatabase", rfe);
return -1;
}
}
public long insertMessageSent(SendReq sendRequest, long threadId, boolean isSecure) throws MmsException {
PduHeaders headers = sendRequest.getPduHeaders();
ContentValues contentValues = getContentValuesFromHeader(headers);
if (!isSecure) contentValues.put(MESSAGE_BOX, Types.MESSAGE_BOX_OUTBOX);
else contentValues.put(MESSAGE_BOX, Types.MESSAGE_BOX_SECURE_OUTBOX);
contentValues.put(THREAD_ID, threadId);
contentValues.put(READ, 1);
long messageId = insertMediaMessage(sendRequest, contentValues);
DatabaseFactory.getThreadDatabase(context).setRead(threadId);
return messageId;
}
private long insertMediaMessage(MultimediaMessagePdu message, ContentValues contentValues) throws MmsException {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
long messageId = db.insert(TABLE_NAME, null, contentValues);
PduBody body = message.getBody();
PartDatabase partsDatabase = getPartDatabase();
MmsAddressDatabase addressDatabase = DatabaseFactory.getMmsAddressDatabase(context);
addressDatabase.insertAddressesForId(messageId, message.getPduHeaders());
partsDatabase.insertParts(messageId, body);
notifyConversationListeners(contentValues.getAsLong(THREAD_ID));
DatabaseFactory.getThreadDatabase(context).update(contentValues.getAsLong(THREAD_ID));
return messageId;
}
public void delete(long messageId) {
long threadId = getThreadIdForMessage(messageId);
MmsAddressDatabase addrDatabase = DatabaseFactory.getMmsAddressDatabase(context);
PartDatabase partDatabase = DatabaseFactory.getPartDatabase(context);
partDatabase.deleteParts(messageId);
addrDatabase.deleteAddressesForId(messageId);
SQLiteDatabase database = databaseHelper.getWritableDatabase();
database.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""});
DatabaseFactory.getThreadDatabase(context).update(threadId);
notifyConversationListeners(threadId);
}
public void deleteThread(long threadId) {
Set<Long> singleThreadSet = new HashSet<Long>();
singleThreadSet.add(threadId);
deleteThreads(singleThreadSet);
}
/*package*/ void deleteThreads(Set<Long> threadIds) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
String where = "";
Cursor cursor = null;
for (long threadId : threadIds) {
where += THREAD_ID + " = '" + threadId + "' OR ";
}
where = where.substring(0, where.length() - 4);
try {
cursor = db.query(TABLE_NAME, new String[] {ID}, where, null, null, null, null);
while (cursor != null && cursor.moveToNext()) {
delete(cursor.getLong(0));
}
} finally {
if (cursor != null)
cursor.close();
}
}
public void deleteAllThreads() {
DatabaseFactory.getPartDatabase(context).deleteAllParts();
DatabaseFactory.getMmsAddressDatabase(context).deleteAllAddresses();
SQLiteDatabase database = databaseHelper.getWritableDatabase();
database.delete(TABLE_NAME, null, null);
}
public Cursor getCarrierMmsInformation() {
Uri uri = Uri.withAppendedPath(Uri.parse("content://telephony/carriers"), "current");
String selection = "type = 'mms'";
return context.getContentResolver().query(uri, null, selection, null, null);
}
private PduHeaders getHeadersForId(long messageId) throws MmsException {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
Cursor cursor = null;
try {
cursor = database.query(TABLE_NAME, null, ID_WHERE, new String[] {messageId+""}, null, null, null);
if (cursor == null || !cursor.moveToFirst())
throw new MmsException("No headers available at ID: " + messageId);
PduHeaders headers = getHeadersFromCursor(cursor);
long messageBox = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX));
MmsAddressDatabase addr = DatabaseFactory.getMmsAddressDatabase(context);
addr.getAddressesForId(messageId, headers);
headers.setMessageBox(messageBox);
return headers;
} finally {
if (cursor != null)
cursor.close();
}
}
private PduHeaders getHeadersFromCursor(Cursor cursor) throws InvalidHeaderValueException {
PduHeaders headers = new PduHeaders();
PduHeadersBuilder phb = new PduHeadersBuilder(headers, cursor);
phb.add(RETRIEVE_TEXT, RETRIEVE_TEXT_CS, PduHeaders.RETRIEVE_TEXT);
phb.add(SUBJECT, SUBJECT_CHARSET, PduHeaders.SUBJECT);
phb.addText(CONTENT_LOCATION, PduHeaders.CONTENT_LOCATION);
phb.addText(CONTENT_TYPE, PduHeaders.CONTENT_TYPE);
phb.addText(MESSAGE_CLASS, PduHeaders.MESSAGE_CLASS);
phb.addText(MESSAGE_ID, PduHeaders.MESSAGE_ID);
phb.addText(RESPONSE_TEXT, PduHeaders.RESPONSE_TEXT);
phb.addText(TRANSACTION_ID, PduHeaders.TRANSACTION_ID);
phb.addOctet(CONTENT_CLASS, PduHeaders.CONTENT_CLASS);
phb.addOctet(DELIVERY_REPORT, PduHeaders.DELIVERY_REPORT);
phb.addOctet(MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE);
phb.addOctet(MMS_VERSION, PduHeaders.MMS_VERSION);
phb.addOctet(PRIORITY, PduHeaders.PRIORITY);
phb.addOctet(READ_STATUS, PduHeaders.READ_STATUS);
phb.addOctet(REPORT_ALLOWED, PduHeaders.REPORT_ALLOWED);
phb.addOctet(RETRIEVE_STATUS, PduHeaders.RETRIEVE_STATUS);
phb.addOctet(STATUS, PduHeaders.STATUS);
phb.addLong(DATE, PduHeaders.DATE);
phb.addLong(DELIVERY_TIME, PduHeaders.DELIVERY_TIME);
phb.addLong(EXPIRY, PduHeaders.EXPIRY);
phb.addLong(MESSAGE_SIZE, PduHeaders.MESSAGE_SIZE);
return phb.getHeaders();
}
private ContentValues getContentValuesFromHeader(PduHeaders headers) {
ContentValues contentValues = new ContentValues();
ContentValuesBuilder cvb = new ContentValuesBuilder(contentValues);
cvb.add(RETRIEVE_TEXT, RETRIEVE_TEXT_CS, headers.getEncodedStringValue(PduHeaders.RETRIEVE_TEXT));
cvb.add(SUBJECT, SUBJECT_CHARSET, headers.getEncodedStringValue(PduHeaders.SUBJECT));
cvb.add(CONTENT_LOCATION, headers.getTextString(PduHeaders.CONTENT_LOCATION));
cvb.add(CONTENT_TYPE, headers.getTextString(PduHeaders.CONTENT_TYPE));
cvb.add(MESSAGE_CLASS, headers.getTextString(PduHeaders.MESSAGE_CLASS));
cvb.add(MESSAGE_ID, headers.getTextString(PduHeaders.MESSAGE_ID));
cvb.add(RESPONSE_TEXT, headers.getTextString(PduHeaders.RESPONSE_TEXT));
cvb.add(TRANSACTION_ID, headers.getTextString(PduHeaders.TRANSACTION_ID));
cvb.add(CONTENT_CLASS, headers.getOctet(PduHeaders.CONTENT_CLASS));
cvb.add(DELIVERY_REPORT, headers.getOctet(PduHeaders.DELIVERY_REPORT));
cvb.add(MESSAGE_TYPE, headers.getOctet(PduHeaders.MESSAGE_TYPE));
cvb.add(MMS_VERSION, headers.getOctet(PduHeaders.MMS_VERSION));
cvb.add(PRIORITY, headers.getOctet(PduHeaders.PRIORITY));
cvb.add(READ_REPORT, headers.getOctet(PduHeaders.READ_REPORT));
cvb.add(READ_STATUS, headers.getOctet(PduHeaders.READ_STATUS));
cvb.add(REPORT_ALLOWED, headers.getOctet(PduHeaders.REPORT_ALLOWED));
cvb.add(RETRIEVE_STATUS, headers.getOctet(PduHeaders.RETRIEVE_STATUS));
cvb.add(STATUS, headers.getOctet(PduHeaders.STATUS));
cvb.add(DATE, headers.getLongInteger(PduHeaders.DATE));
cvb.add(DELIVERY_TIME, headers.getLongInteger(PduHeaders.DELIVERY_TIME));
cvb.add(EXPIRY, headers.getLongInteger(PduHeaders.EXPIRY));
cvb.add(MESSAGE_SIZE, headers.getLongInteger(PduHeaders.MESSAGE_SIZE));
return cvb.getContentValues();
}
protected PartDatabase getPartDatabase() {
return DatabaseFactory.getPartDatabase(context);
}
public static class Types {
public static final String MMS_ERROR_TYPE = "err_type";
public static final int MESSAGE_BOX_INBOX = 1;
public static final int MESSAGE_BOX_SENT = 2;
public static final int MESSAGE_BOX_DRAFTS = 3;
public static final int MESSAGE_BOX_OUTBOX = 4;
public static final int MESSAGE_BOX_SECURE_OUTBOX = 5;
public static final int MESSAGE_BOX_SECURE_SENT = 6;
public static final int MESSAGE_BOX_DECRYPTING_INBOX = 7;
public static final int MESSAGE_BOX_SECURE_INBOX = 8;
public static final int MESSAGE_BOX_NO_SESSION_INBOX = 9;
public static final int MESSAGE_BOX_DECRYPT_FAILED_INBOX = 10;
public static final int MESSAGE_BOX_SENT_FAILED = 12;
public static final int DOWNLOAD_INITIALIZED = 1;
public static final int DOWNLOAD_NO_CONNECTIVITY = 2;
public static final int DOWNLOAD_CONNECTING = 3;
public static final int DOWNLOAD_SOFT_FAILURE = 4;
public static final int DOWNLOAD_HARD_FAILURE = 5;
public static boolean isSecureMmsBox(long mailbox) {
return mailbox == Types.MESSAGE_BOX_SECURE_OUTBOX || mailbox == Types.MESSAGE_BOX_SECURE_SENT || mailbox == Types.MESSAGE_BOX_SECURE_INBOX;
}
public static boolean isOutgoingMmsBox(long mailbox) {
return mailbox == Types.MESSAGE_BOX_OUTBOX || mailbox == Types.MESSAGE_BOX_SENT || mailbox == Types.MESSAGE_BOX_SECURE_OUTBOX || mailbox == Types.MESSAGE_BOX_SENT_FAILED || mailbox == Types.MESSAGE_BOX_SECURE_SENT;
}
public static boolean isPendingMmsBox(long mailbox) {
return mailbox == Types.MESSAGE_BOX_OUTBOX || mailbox == MESSAGE_BOX_SECURE_OUTBOX;
}
public static boolean isFailedMmsBox(long mailbox) {
return mailbox == Types.MESSAGE_BOX_SENT_FAILED;
}
public static boolean isDisplayDownloadButton(int status) {
return status == DOWNLOAD_INITIALIZED || status == DOWNLOAD_NO_CONNECTIVITY || status == DOWNLOAD_SOFT_FAILURE;
}
public static String getLabelForStatus(int status) {
Log.w("MmsDatabase", "Getting label for status: " + status);
switch (status) {
case DOWNLOAD_CONNECTING: return "Connecting to MMS server...";
case DOWNLOAD_INITIALIZED: return "Downloading MMS...";
case DOWNLOAD_HARD_FAILURE: return "MMS Download failed!";
}
return "Downloading...";
}
public static boolean isHardError(int status) {
return status == DOWNLOAD_HARD_FAILURE;
}
}
}

View File

@@ -0,0 +1,146 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.database;
import java.util.Iterator;
import java.util.List;
import org.thoughtcrime.securesms.ConversationItem;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
public class MmsMessageRecord extends MessageRecord {
private SlideDeck slideDeck;
private byte[] contentLocation;
private long messageSize;
private long expiry;
private boolean isNotification;
private long mailbox;
private int status;
private byte[] transactionId;
public MmsMessageRecord(MessageRecord record, SlideDeck slideDeck, long mailbox) {
super(record);
this.slideDeck = slideDeck;
this.isNotification = false;
this.mailbox = mailbox;
setBodyIfTextAvailable();
}
public MmsMessageRecord(MessageRecord record, byte[] contentLocation, long messageSize, long expiry, int status, byte[] transactionId) {
super(record);
this.contentLocation = contentLocation;
this.messageSize = messageSize;
this.expiry = expiry;
this.isNotification = true;
this.status = status;
this.transactionId = transactionId;
}
public byte[] getTransactionId() {
return transactionId;
}
public int getStatus() {
return this.status;
}
@Override
public boolean isOutgoing() {
return MmsDatabase.Types.isOutgoingMmsBox(mailbox);
}
@Override
public boolean isPending() {
return MmsDatabase.Types.isPendingMmsBox(mailbox);
}
@Override
public boolean isFailed() {
return MmsDatabase.Types.isFailedMmsBox(mailbox);
}
@Override
public boolean isSecure() {
return MmsDatabase.Types.isSecureMmsBox(mailbox);
}
// This is the double-dispatch pattern, don't refactor
// this into the base class.
public void setOnConversationItem(ConversationItem item) {
item.setMessageRecord(this);
}
public byte[] getContentLocation() {
return contentLocation;
}
public long getMessageSize() {
return (messageSize + 1023) / 1024;
}
public long getExpiration() {
return expiry * 1000;
}
public boolean isNotification() {
return isNotification;
}
public SlideDeck getSlideDeck() {
return slideDeck;
}
private void setBodyFromSlidesIfTextAvailable() {
List<Slide> slides = slideDeck.getSlides();
Iterator<Slide> i = slides.iterator();
while (i.hasNext()) {
Slide slide = i.next();
if (slide.hasText())
setBody(slide.getText());
}
}
private void setBodyIfTextAvailable() {
switch ((int)mailbox) {
case MmsDatabase.Types.MESSAGE_BOX_DECRYPTING_INBOX:
setBody("Decrypting MMS, please wait...");
setEmphasis(true);
return;
case MmsDatabase.Types.MESSAGE_BOX_DECRYPT_FAILED_INBOX:
setBody("Bad encrypted MMS message...");
setEmphasis(true);
return;
case MmsDatabase.Types.MESSAGE_BOX_NO_SESSION_INBOX:
setBody("MMS message encrypted for non-existing session...");
setEmphasis(true);
return;
}
setBodyFromSlidesIfTextAvailable();
}
@Override
public boolean isMms() {
return true;
}
}

View File

@@ -0,0 +1,122 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.database;
import java.util.HashSet;
import java.util.Set;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.util.Log;
public class MmsSmsDatabase extends Database {
public static final String TRANSPORT = "transport_type";
public MmsSmsDatabase(Context context, SQLiteOpenHelper databaseHelper) {
super(context, databaseHelper);
}
public Cursor getConversation(long threadId) {
String[] projection = {"_id", "body", "type", "address", "subject", "normalized_date AS date", "m_type", "msg_box", "transport_type"};
String order = "normalized_date ASC";
String selection = "thread_id = " + threadId;
Cursor cursor = queryTables(projection, selection, order, null);
setNotifyConverationListeners(cursor, threadId);
return cursor;
}
public Cursor getConversationSnippet(long threadId) {
String[] projection = {"_id", "body", "type", "address", "subject", "normalized_date AS date", "m_type", "msg_box", "transport_type"};
String order = "normalized_date DESC";
String selection = "thread_id = " + threadId;
Cursor cursor = queryTables(projection, selection, order, "1");
return cursor;
}
public Cursor getUnread() {
String[] projection = {"_id", "body", "read", "type", "address", "subject", "thread_id", "normalized_date AS date", "m_type", "msg_box", "transport_type"};
String order = "normalized_date ASC";
String selection = "read = 0";
Cursor cursor = queryTables(projection, selection, order, null);
return cursor;
}
public int getConversationCount(long threadId) {
int count = DatabaseFactory.getSmsDatabase(context).getMessageCountForThread(threadId);
count += DatabaseFactory.getMmsDatabase(context).getMessageCountForThread(threadId);
return count;
}
private Cursor queryTables(String[] projection, String selection, String order, String limit) {
String[] mmsProjection = {"date * 1000 AS normalized_date", "_id", "body", "read", "thread_id", "type", "address", "subject", "date", "m_type", "msg_box", "transport_type"};
String[] smsProjection = {"date * 1 AS normalized_date", "_id", "body", "read", "thread_id", "type", "address", "subject", "date", "m_type", "msg_box", "transport_type"};
SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder();
mmsQueryBuilder.setDistinct(true);
smsQueryBuilder.setDistinct(true);
mmsQueryBuilder.setTables(MmsDatabase.TABLE_NAME);
smsQueryBuilder.setTables(SmsDatabase.TABLE_NAME);
Set<String> mmsColumnsPresent = new HashSet<String>();
mmsColumnsPresent.add("_id");
mmsColumnsPresent.add("m_type");
mmsColumnsPresent.add("msg_box");
mmsColumnsPresent.add("date");
mmsColumnsPresent.add("read");
mmsColumnsPresent.add("thread_id");
Set<String> smsColumnsPresent = new HashSet<String>();
smsColumnsPresent.add("_id");
smsColumnsPresent.add("body");
smsColumnsPresent.add("type");
smsColumnsPresent.add("address");
smsColumnsPresent.add("subject");
smsColumnsPresent.add("date");
smsColumnsPresent.add("read");
smsColumnsPresent.add("thread_id");
String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery("transport_type", mmsProjection, mmsColumnsPresent, 0, "mms", selection, null, null, null);
String smsSubQuery = smsQueryBuilder.buildUnionSubQuery("transport_type", smsProjection, smsColumnsPresent, 0, "sms", selection, null, null, null);
SQLiteQueryBuilder unionQueryBuilder = new SQLiteQueryBuilder();
String unionQuery = unionQueryBuilder.buildUnionQuery(new String[] {smsSubQuery, mmsSubQuery}, order, null);
SQLiteQueryBuilder outerQueryBuilder = new SQLiteQueryBuilder();
outerQueryBuilder.setTables("(" + unionQuery + ")");
String query = outerQueryBuilder.buildQuery(projection, null, null, null, null, null, limit);
Log.w("MmsSmsDatabase", "Executing query: " + query);
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = db.rawQuery(query, null);
return cursor;
}
}

View File

@@ -0,0 +1,40 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.database;
public class NoExternalStorageException extends Exception {
public NoExternalStorageException() {
// TODO Auto-generated constructor stub
}
public NoExternalStorageException(String detailMessage) {
super(detailMessage);
// TODO Auto-generated constructor stub
}
public NoExternalStorageException(Throwable throwable) {
super(throwable);
// TODO Auto-generated constructor stub
}
public NoExternalStorageException(String detailMessage, Throwable throwable) {
super(detailMessage, throwable);
// TODO Auto-generated constructor stub
}
}

View File

@@ -0,0 +1,360 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.database;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import org.thoughtcrime.securesms.providers.PartProvider;
import ws.com.google.android.mms.ContentType;
import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.CharacterSets;
import ws.com.google.android.mms.pdu.PduBody;
import ws.com.google.android.mms.pdu.PduPart;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
public class PartDatabase extends Database {
private static final String TABLE_NAME = "part";
private static final String ID = "_id";
private static final String MMS_ID = "mid";
private static final String SEQUENCE = "seq";
private static final String CONTENT_TYPE = "ct";
private static final String NAME = "name";
private static final String CHARSET = "chset";
private static final String CONTENT_DISPOSITION = "cd";
private static final String FILENAME = "fn";
private static final String CONTENT_ID = "cid";
private static final String CONTENT_LOCATION = "cl";
private static final String CONTENT_TYPE_START = "ctt_s";
private static final String CONTENT_TYPE_TYPE = "ctt_t";
private static final String ENCRYPTED = "encrypted";
private static final String DATA = "_data";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
MMS_ID + " INTEGER, " + SEQUENCE + " INTEGER DEFAULT 0, " +
CONTENT_TYPE + " TEXT, " + NAME + " TEXT, " + CHARSET + " INTEGER, " +
CONTENT_DISPOSITION + " TEXT, " + FILENAME + " TEXT, " + CONTENT_ID + " TEXT, " +
CONTENT_LOCATION + " TEXT, " + CONTENT_TYPE_START + " INTEGER, " +
CONTENT_TYPE_TYPE + " TEXT, " + ENCRYPTED + " INTEGER, " + DATA + " TEXT);";
public PartDatabase(Context context, SQLiteOpenHelper databaseHelper) {
super(context, databaseHelper);
}
private void getPartValues(PduPart part, Cursor cursor) {
int charsetColumn = cursor.getColumnIndexOrThrow(CHARSET);
if (!cursor.isNull(charsetColumn))
part.setCharset(cursor.getInt(charsetColumn));
int contentTypeColumn = cursor.getColumnIndexOrThrow(CONTENT_TYPE);
if (!cursor.isNull(contentTypeColumn))
part.setContentType(getBytes(cursor.getString(contentTypeColumn)));
int nameColumn = cursor.getColumnIndexOrThrow(NAME);
if (!cursor.isNull(nameColumn))
part.setName(getBytes(cursor.getString(nameColumn)));
int fileNameColumn = cursor.getColumnIndexOrThrow(FILENAME);
if (!cursor.isNull(fileNameColumn))
part.setFilename(getBytes(cursor.getString(fileNameColumn)));
int contentDispositionColumn = cursor.getColumnIndexOrThrow(CONTENT_DISPOSITION);
if (!cursor.isNull(contentDispositionColumn))
part.setContentDisposition(getBytes(cursor.getString(contentDispositionColumn)));
int contentIdColumn = cursor.getColumnIndexOrThrow(CONTENT_ID);
if (!cursor.isNull(contentIdColumn))
part.setContentId(getBytes(cursor.getString(contentIdColumn)));
int contentLocationColumn = cursor.getColumnIndexOrThrow(CONTENT_LOCATION);
if (!cursor.isNull(contentLocationColumn))
part.setContentLocation(getBytes(cursor.getString(contentLocationColumn)));
int encryptedColumn = cursor.getColumnIndexOrThrow(ENCRYPTED);
if (!cursor.isNull(encryptedColumn))
part.setEncrypted(cursor.getInt(encryptedColumn) == 1);
}
private ContentValues getContentValuesForPart(PduPart part) throws MmsException {
ContentValues contentValues = new ContentValues();
if (part.getCharset() != 0 ) {
contentValues.put(CHARSET, part.getCharset());
}
if (part.getContentType() != null) {
contentValues.put(CONTENT_TYPE, toIsoString(part.getContentType()));
if (toIsoString(part.getContentType()).equals(ContentType.APP_SMIL))
contentValues.put(SEQUENCE, -1);
} else {
throw new MmsException("There is no content type for this part.");
}
if (part.getName() != null) {
contentValues.put(NAME, new String(part.getName()));
}
if (part.getFilename() != null) {
contentValues.put(FILENAME, new String(part.getFilename()));
}
if (part.getContentDisposition() != null) {
contentValues.put(CONTENT_DISPOSITION, toIsoString(part.getContentDisposition()));
}
if (part.getContentId() != null) {
contentValues.put(CONTENT_ID, toIsoString(part.getContentId()));
}
if (part.getContentLocation() != null) {
contentValues.put(CONTENT_LOCATION, toIsoString(part.getContentLocation()));
}
contentValues.put(ENCRYPTED, part.getEncrypted() ? 1 : 0);
return contentValues;
}
protected FileInputStream getPartInputStream(File file, PduPart part) throws FileNotFoundException {
Log.w("PartDatabase", "Reading non-encrypted part from: " + file.getAbsolutePath());
return new FileInputStream(file);
}
protected FileOutputStream getPartOutputStream(File file, PduPart part) throws FileNotFoundException {
Log.w("PartDatabase", "Writing non-encrypted part to: " + file.getAbsolutePath());
return new FileOutputStream(file);
}
private void readPartData(PduPart part, String filename) {
try {
File dataFile = new File(filename);
FileInputStream fin = getPartInputStream(dataFile, part);
ByteArrayOutputStream baos = new ByteArrayOutputStream((int)dataFile.length());
byte[] buffer = new byte[512];
int read;
while ((read = fin.read(buffer)) != -1)
baos.write(buffer, 0, read);
part.setData(baos.toByteArray());
fin.close();
} catch (IOException ioe) {
Log.w("PartDatabase", ioe);
part.setData(null);
}
}
private File writePartData(PduPart part) throws MmsException {
try {
File partsDirectory = context.getDir("parts", Context.MODE_PRIVATE);
File dataFile = File.createTempFile("part", ".mms", partsDirectory);
FileOutputStream fout = getPartOutputStream(dataFile, part);
if (part.getData() != null) {
Log.w("PartDatabase", "Writing part data from buffer");
fout.write(part.getData());
fout.close();
return dataFile;
} else if (part.getDataUri() != null) {
Log.w("PartDatabase", "Writing part dat from URI");
byte[] buf = new byte[512];
InputStream in = context.getContentResolver().openInputStream(part.getDataUri());
int read;
while ((read = in.read(buf)) != -1)
fout.write(buf, 0, read);
fout.close();
in.close();
return dataFile;
} else {
throw new MmsException("Part is empty!");
}
} catch (FileNotFoundException e) {
throw new AssertionError(e);
} catch (IOException e) {
throw new AssertionError(e);
}
}
private PduPart getPart(Cursor cursor, boolean includeData) {
PduPart part = new PduPart();
String dataLocation = cursor.getString(cursor.getColumnIndexOrThrow(DATA));
long partId = cursor.getLong(cursor.getColumnIndexOrThrow(ID));
getPartValues(part, cursor);
if (includeData)
readPartData(part, dataLocation);
part.setDataUri(ContentUris.withAppendedId(PartProvider.CONTENT_URI, partId));
return part;
}
private long insertPart(PduPart part, long mmsId) throws MmsException {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
File dataFile = writePartData(part);
Log.w("PartDatabase", "Wrote part to file: " + dataFile.getAbsolutePath());
ContentValues contentValues = getContentValuesForPart(part);
contentValues.put(MMS_ID, mmsId);
contentValues.put(DATA, dataFile.getAbsolutePath());
return database.insert(TABLE_NAME, null, contentValues);
}
public InputStream getPartStream(long partId) throws FileNotFoundException {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
Cursor cursor = null;
Log.w("PartDatabase", "Getting part at ID: " + partId);
try {
cursor = database.query(TABLE_NAME, new String[]{DATA, ENCRYPTED}, ID_WHERE, new String[] {partId+""}, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
PduPart part = new PduPart();
part.setEncrypted(cursor.getInt(1) == 1);
return getPartInputStream(new File(cursor.getString(0)), part);
} else {
throw new FileNotFoundException("No part for id: " + partId);
}
} finally {
if (cursor != null)
cursor.close();
}
}
public void insertParts(long mmsId, PduBody body) throws MmsException {
for (int i=0;i<body.getPartsNum();i++) {
long partId = insertPart(body.getPart(i), mmsId);
Log.w("PartDatabase", "Inserted part at ID: " + partId);
}
}
public PduPart getPart(long partId, boolean includeData) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
Cursor cursor = null;
try {
cursor = database.query(TABLE_NAME, null, ID_WHERE, new String[] {partId+""}, null, null, null);
if (cursor != null && cursor.moveToFirst())
return getPart(cursor, includeData);
else
return null;
} finally {
if (cursor != null)
cursor.close();
}
}
public PduBody getParts(long mmsId, boolean includeData) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
PduBody body = new PduBody();
Cursor cursor = null;
try {
cursor = database.query(TABLE_NAME, null, MMS_ID + " = ?", new String[] {mmsId+""}, null, null, null);
while (cursor != null && cursor.moveToNext()) {
PduPart part = getPart(cursor, includeData);
body.addPart(part);
}
return body;
} finally {
if (cursor != null)
cursor.close();
}
}
public void deleteParts(long mmsId) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
Cursor cursor = null;
try {
cursor = database.query(TABLE_NAME, new String[] {DATA}, MMS_ID + " = ?", new String[] {mmsId+""}, null, null, null);
while (cursor != null && cursor.moveToNext()) {
new File(cursor.getString(0)).delete();
}
} finally {
if (cursor != null)
cursor.close();
}
database.delete(TABLE_NAME, MMS_ID + " = ?", new String[] {mmsId+""});
}
public void deleteAllParts() {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
database.delete(TABLE_NAME, null, null);
File partsDirectory = context.getDir("parts", Context.MODE_PRIVATE);
File[] parts = partsDirectory.listFiles();
for (int i=0;i<parts.length;i++) {
parts[i].delete();
}
}
private byte[] getBytes(String data) {
try {
return data.getBytes(CharacterSets.MIMENAME_ISO_8859_1);
} catch (UnsupportedEncodingException e) {
Log.e("PduHeadersBuilder", "ISO_8859_1 must be supported!", e);
return new byte[0];
}
}
private String toIsoString(byte[] bytes) {
try {
return new String(bytes, CharacterSets.MIMENAME_ISO_8859_1);
} catch (UnsupportedEncodingException e) {
// Impossible to reach here!
Log.e("MmsDatabase", "ISO_8859_1 must be supported!", e);
return "";
}
}
}

View File

@@ -0,0 +1,79 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.database;
import java.io.UnsupportedEncodingException;
import ws.com.google.android.mms.InvalidHeaderValueException;
import ws.com.google.android.mms.pdu.CharacterSets;
import ws.com.google.android.mms.pdu.EncodedStringValue;
import ws.com.google.android.mms.pdu.PduHeaders;
import android.database.Cursor;
import android.util.Log;
public class PduHeadersBuilder {
private final PduHeaders headers;
private final Cursor cursor;
public PduHeadersBuilder(PduHeaders headers, Cursor cursor) {
this.headers = headers;
this.cursor = cursor;
}
public PduHeaders getHeaders() {
return headers;
}
public void addLong(String key, int headersKey) {
int columnIndex = cursor.getColumnIndexOrThrow(key);
if (!cursor.isNull(columnIndex))
headers.setLongInteger(cursor.getLong(columnIndex), headersKey);
}
public void addOctet(String key, int headersKey) throws InvalidHeaderValueException {
int columnIndex = cursor.getColumnIndexOrThrow(key);
if (!cursor.isNull(columnIndex))
headers.setOctet(cursor.getInt(columnIndex), headersKey);
}
public void addText(String key, int headersKey) {
String value = cursor.getString(cursor.getColumnIndexOrThrow(key));
if (value != null && value.trim().length() > 0)
headers.setTextString(getBytes(value), headersKey);
}
public void add(String key, String charsetKey, int headersKey) {
String value = cursor.getString(cursor.getColumnIndexOrThrow(key));
if (value != null && value.trim().length() > 0) {
int charsetValue = cursor.getInt(cursor.getColumnIndexOrThrow(charsetKey));
EncodedStringValue encodedValue = new EncodedStringValue(charsetValue, getBytes(value));
headers.setEncodedStringValue(encodedValue, headersKey);
}
}
private byte[] getBytes(String data) {
try {
return data.getBytes(CharacterSets.MIMENAME_ISO_8859_1);
} catch (UnsupportedEncodingException e) {
Log.e("PduHeadersBuilder", "ISO_8859_1 must be supported!", e);
return new byte[0];
}
}
}

View File

@@ -0,0 +1,95 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.database;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import org.thoughtcrime.securesms.util.Conversions;
import android.content.Context;
public abstract class Record {
protected final String address;
protected final Context context;
public Record(Context context, String address) {
this.context = context;
this.address = address;
}
public void delete() {
delete(this.context, this.address);
}
protected static void delete(Context context, String address) {
getAddressFile(context, address).delete();
}
protected static boolean hasRecord(Context context, String address) {
return getAddressFile(context, address).exists();
}
protected RandomAccessFile openRandomAccessFile() throws FileNotFoundException {
return new RandomAccessFile(getAddressFile(), "rw");
}
protected FileInputStream openInputStream() throws FileNotFoundException {
return new FileInputStream(getAddressFile().getAbsolutePath());
}
private File getAddressFile() {
return getAddressFile(context, address);
}
private static File getAddressFile(Context context, String address) {
return new File(context.getFilesDir().getAbsolutePath() + File.separatorChar + "sessions", address);
}
protected byte[] readBlob(FileInputStream in) throws IOException {
int length = readInteger(in);
byte[] blobBytes = new byte[length];
in.read(blobBytes, 0, blobBytes.length);
return blobBytes;
}
protected void writeBlob(byte[] blobBytes, FileChannel out) throws IOException {
writeInteger(blobBytes.length, out);
ByteBuffer buffer = ByteBuffer.wrap(blobBytes);
out.write(buffer);
}
protected int readInteger(FileInputStream in) throws IOException {
byte[] integer = new byte[4];
in.read(integer, 0, integer.length);
return Conversions.byteArrayToInt(integer);
}
protected void writeInteger(int value, FileChannel out) throws IOException {
byte[] valueBytes = Conversions.intToByteArray(value);
ByteBuffer buffer = ByteBuffer.wrap(valueBytes);
out.write(buffer);
}
}

View File

@@ -0,0 +1,149 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.database;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import org.thoughtcrime.securesms.crypto.InvalidKeyException;
import org.thoughtcrime.securesms.crypto.PublicKey;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Hex;
import android.content.Context;
import android.util.Log;
/**
* Represents the current and last public key belonging to the "remote"
* endpoint in an encrypted session. These are stored on disk.
*
* @author Moxie Marlinspike
*/
public class RemoteKeyRecord extends Record {
private static final Object FILE_LOCK = new Object();
private PublicKey remoteKeyCurrent;
private PublicKey remoteKeyLast;
public RemoteKeyRecord(Context context, Recipient recipient) {
super(context,getFileNameForRecipient(context, recipient));
loadData();
}
public static void delete(Context context, Recipient recipient) {
Record.delete(context, getFileNameForRecipient(context, recipient));
}
public static boolean hasRecord(Context context, Recipient recipient) {
Log.w("LocalKeyRecord", "Checking: " + getFileNameForRecipient(context, recipient));
return Record.hasRecord(context, getFileNameForRecipient(context, recipient));
}
private static String getFileNameForRecipient(Context context, Recipient recipient) {
return CanonicalAddressDatabase.getInstance(context).getCanonicalAddress(recipient.getNumber()) + "-remote";
}
public void updateCurrentRemoteKey(PublicKey remoteKey) {
Log.w("RemoteKeyRecord", "Updating current remote key: " + remoteKey.getId());
if (remoteKey.getId() > remoteKeyCurrent.getId()) {
this.remoteKeyLast = this.remoteKeyCurrent;
this.remoteKeyCurrent = remoteKey;
}
}
public void setCurrentRemoteKey(PublicKey remoteKeyCurrent) {
this.remoteKeyCurrent = remoteKeyCurrent;
}
public void setLastRemoteKey(PublicKey remoteKeyLast) {
this.remoteKeyLast = remoteKeyLast;
}
public PublicKey getCurrentRemoteKey() {
return this.remoteKeyCurrent;
}
public PublicKey getLastRemoteKey() {
return this.remoteKeyLast;
}
public PublicKey getKeyForId(int id) throws InvalidKeyIdException {
if (this.remoteKeyCurrent.getId() == id) return this.remoteKeyCurrent;
else if (this.remoteKeyLast.getId() == id) return this.remoteKeyLast;
else throw new InvalidKeyIdException("No remote key for ID: " + id);
}
public void save() {
Log.w("RemoteKeyRecord", "Saving remote key record for recipient: " + this.address);
synchronized (FILE_LOCK) {
try {
RandomAccessFile file = openRandomAccessFile();
FileChannel out = file.getChannel();
Log.w("RemoteKeyRecord", "Opened file of size: " + out.size());
out.position(0);
writeKey(remoteKeyCurrent, out);
writeKey(remoteKeyLast, out);
out.truncate(out.position());
out.close();
file.close();
} catch (IOException ioe) {
Log.w("keyrecord", ioe);
// XXX
}
}
}
private void loadData() {
Log.w("RemoteKeyRecord", "Loading remote key record for recipient: " + this.address);
synchronized (FILE_LOCK) {
try {
FileInputStream in = this.openInputStream();
remoteKeyCurrent = readKey(in);
remoteKeyLast = readKey(in);
in.close();
} catch (FileNotFoundException e) {
Log.w("RemoteKeyRecord", "No remote keys found.");
return;
} catch (IOException ioe) {
Log.w("keyrecord", ioe);
// XXX
}
}
}
private void writeKey(PublicKey key, FileChannel out) throws IOException {
byte[] keyBytes = key.serialize();
Log.w("RemoteKeyRecord", "Serializing remote key bytes: " + Hex.toString(keyBytes));
writeBlob(keyBytes, out);
}
private PublicKey readKey(FileInputStream in) throws IOException {
try {
byte[] keyBytes = readBlob(in);
return new PublicKey(keyBytes);
} catch (InvalidKeyException ike) {
throw new AssertionError(ike);
}
}
}

View File

@@ -0,0 +1,97 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.database;
import javax.crypto.spec.SecretKeySpec;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.SessionCipher;
import org.thoughtcrime.securesms.util.Conversions;
import org.thoughtcrime.securesms.util.Util;
/**
* Represents the currently negotiated session key for a given
* local key id and remote key id. This is stored encrypted on
* disk.
*
* @author Moxie Marlinspike
*/
public class SessionKey {
private int localKeyId;
private int remoteKeyId;
private SecretKeySpec cipherKey;
private SecretKeySpec macKey;
private MasterCipher masterCipher;
public SessionKey(int localKeyId, int remoteKeyId, SecretKeySpec cipherKey, SecretKeySpec macKey, MasterSecret masterSecret) {
this.localKeyId = localKeyId;
this.remoteKeyId = remoteKeyId;
this.cipherKey = cipherKey;
this.macKey = macKey;
this.masterCipher = new MasterCipher(masterSecret);
}
public SessionKey(byte[] bytes, MasterSecret masterSecret) {
this.masterCipher = new MasterCipher(masterSecret);
deserialize(bytes);
}
public byte[] serialize() {
byte[] localKeyIdBytes = Conversions.mediumToByteArray(localKeyId);
byte[] remoteKeyIdBytes = Conversions.mediumToByteArray(remoteKeyId);
byte[] cipherKeyBytes = cipherKey.getEncoded();
byte[] macKeyBytes = macKey.getEncoded();
byte[] combined = Util.combine(localKeyIdBytes, remoteKeyIdBytes, cipherKeyBytes, macKeyBytes);
return masterCipher.encryptBytes(combined);
}
private void deserialize(byte[] bytes) {
byte[] decrypted = masterCipher.encryptBytes(bytes);
this.localKeyId = Conversions.byteArrayToMedium(decrypted, 0);
this.remoteKeyId = Conversions.byteArrayToMedium(decrypted, 3);
byte[] keyBytes = new byte[SessionCipher.CIPHER_KEY_LENGTH];
System.arraycopy(decrypted, 6, keyBytes, 0, keyBytes.length);
byte[] macBytes = new byte[SessionCipher.MAC_KEY_LENGTH];
System.arraycopy(decrypted, 6 + keyBytes.length, macBytes, 0, macBytes.length);
this.cipherKey = new SecretKeySpec(keyBytes, "AES");
this.macKey = new SecretKeySpec(macBytes, "HmacSHA1");
}
public int getLocalKeyId() {
return this.localKeyId;
}
public int getRemoteKeyId() {
return this.remoteKeyId;
}
public SecretKeySpec getCipherKey() {
return this.cipherKey;
}
public SecretKeySpec getMacKey() {
return this.macKey;
}
}

View File

@@ -0,0 +1,227 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.database;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import org.thoughtcrime.securesms.crypto.IdentityKey;
import org.thoughtcrime.securesms.crypto.InvalidKeyException;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.recipients.Recipient;
import android.content.Context;
import android.util.Log;
/**
* A disk record representing a current session.
*
* @author Moxie Marlinspike
*/
public class SessionRecord extends Record {
private static final int CURRENT_VERSION_MARKER = 0X55555556;
private static final int[] VALID_VERSION_MARKERS = {CURRENT_VERSION_MARKER, 0X55555555};
private static final Object FILE_LOCK = new Object();
private int counter;
private byte[] localFingerprint;
private byte[] remoteFingerprint;
private int sessionVersion;
private IdentityKey identityKey;
private SessionKey sessionKeyRecord;
private boolean verifiedSessionKey;
private final MasterSecret masterSecret;
public SessionRecord(Context context, MasterSecret masterSecret, Recipient recipient) {
super(context, getFileNameForRecipient(context, recipient));
this.masterSecret = masterSecret;
this.sessionVersion = 31337;
loadData();
}
public static void delete(Context context, Recipient recipient) {
Record.delete(context, getFileNameForRecipient(context, recipient));
}
public static boolean hasSession(Context context, Recipient recipient) {
Log.w("LocalKeyRecord", "Checking: " + getFileNameForRecipient(context, recipient));
return Record.hasRecord(context, getFileNameForRecipient(context, recipient));
}
private static String getFileNameForRecipient(Context context, Recipient recipient) {
return CanonicalAddressDatabase.getInstance(context).getCanonicalAddress(recipient.getNumber()) + "";
}
public void setSessionKey(SessionKey sessionKeyRecord) {
this.sessionKeyRecord = sessionKeyRecord;
}
public void setSessionId(byte[] localFingerprint, byte[] remoteFingerprint) {
this.localFingerprint = localFingerprint;
this.remoteFingerprint = remoteFingerprint;
}
public void setIdentityKey(IdentityKey identityKey) {
this.identityKey = identityKey;
}
public int getSessionVersion() {
return (sessionVersion == 31337 ? 0 : sessionVersion);
}
public void setSessionVersion(int sessionVersion) {
this.sessionVersion = sessionVersion;
}
public int getCounter() {
return this.counter;
}
public void incrementCounter() {
this.counter++;
}
public byte[] getLocalFingerprint() {
return this.localFingerprint;
}
public byte[] getRemoteFingerprint() {
return this.remoteFingerprint;
}
public IdentityKey getIdentityKey() {
return this.identityKey;
}
public void setVerifiedSessionKey(boolean verifiedSessionKey) {
this.verifiedSessionKey = verifiedSessionKey;
}
public boolean isVerifiedSession() {
return this.verifiedSessionKey;
}
private void writeIdentityKey(FileChannel out) throws IOException {
if (identityKey == null) writeBlob(new byte[0], out);
else writeBlob(identityKey.serialize(), out);
}
private boolean isValidVersionMarker(int versionMarker) {
for (int i=0;i<VALID_VERSION_MARKERS.length;i++)
if (versionMarker == VALID_VERSION_MARKERS[i])
return true;
return false;
}
private void readIdentityKey(FileInputStream in) throws IOException {
try {
byte[] blob = readBlob(in);
if (blob.length == 0) this.identityKey = null;
else this.identityKey = new IdentityKey(blob, 0);
} catch (InvalidKeyException ike) {
throw new AssertionError(ike);
}
}
public void save() {
synchronized (FILE_LOCK) {
try {
RandomAccessFile file = openRandomAccessFile();
FileChannel out = file.getChannel();
out.position(0);
writeInteger(CURRENT_VERSION_MARKER, out);
writeInteger(counter, out);
writeBlob(localFingerprint, out);
writeBlob(remoteFingerprint, out);
writeInteger(sessionVersion, out);
writeIdentityKey(out);
writeInteger(verifiedSessionKey ? 1 : 0, out);
if (sessionKeyRecord != null)
writeBlob(sessionKeyRecord.serialize(), out);
out.truncate(out.position());
file.close();
} catch (IOException ioe) {
throw new IllegalArgumentException(ioe);
}
}
}
private void loadData() {
synchronized (FILE_LOCK) {
try {
FileInputStream in = this.openInputStream();
int versionMarker = readInteger(in);
// Sigh, always put a version number on everything.
if (!isValidVersionMarker(versionMarker)) {
this.counter = versionMarker;
this.localFingerprint = readBlob(in);
this.remoteFingerprint = readBlob(in);
this.sessionVersion = 31337;
if (in.available() != 0)
this.sessionKeyRecord = new SessionKey(readBlob(in), masterSecret);
in.close();
} else {
this.counter = readInteger(in);
this.localFingerprint = readBlob(in);
this.remoteFingerprint = readBlob(in);
this.sessionVersion = readInteger(in);
if (versionMarker >= 0X55555556) {
readIdentityKey(in);
this.verifiedSessionKey = (readInteger(in) == 1) ? true : false;
}
if (in.available() != 0)
this.sessionKeyRecord = new SessionKey(readBlob(in), masterSecret);
in.close();
}
} catch (FileNotFoundException e) {
Log.w("SessionRecord", "No session information found.");
return;
} catch (IOException ioe) {
Log.w("keyrecord", ioe);
// XXX
}
}
}
public SessionKey getSessionKey(int localKeyId, int remoteKeyId) {
if (this.sessionKeyRecord == null) return null;
if ((this.sessionKeyRecord.getLocalKeyId() == localKeyId) &&
(this.sessionKeyRecord.getRemoteKeyId() == remoteKeyId))
return this.sessionKeyRecord;
return null;
}
}

View File

@@ -0,0 +1,330 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.database;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteStatement;
import android.telephony.SmsMessage;
import android.util.Log;
/**
* Database for storage of SMS messages.
*
* @author Moxie Marlinspike
*/
public class SmsDatabase extends Database {
public static final String TRANSPORT = "transport_type";
public static final String TABLE_NAME = "sms";
public static final String ID = "_id";
public static final String THREAD_ID = "thread_id";
public static final String ADDRESS = "address";
public static final String PERSON = "person";
public static final String DATE = "date";
public static final String PROTOCOL = "protocol";
public static final String READ = "read";
public static final String STATUS = "status";
public static final String TYPE = "type";
public static final String REPLY_PATH_PRESENT = "reply_path_present";
public static final String SUBJECT = "subject";
public static final String BODY = "body";
public static final String SERVICE_CENTER = "service_center";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " integer PRIMARY KEY, " +
THREAD_ID + " INTEGER, " + ADDRESS + " TEXT, " + PERSON + " INTEGER, " + DATE + " INTEGER, " +
PROTOCOL + " INTEGER, " + READ + " INTEGER DEFAULT 0, " + STATUS + " INTEGER DEFAULT -1," +
TYPE + " INTEGER, " + REPLY_PATH_PRESENT + " INTEGER, " + SUBJECT + " TEXT, " + BODY + " TEXT, " +
SERVICE_CENTER + " TEXT);";
public SmsDatabase(Context context, SQLiteOpenHelper databaseHelper) {
super(context, databaseHelper);
}
private void updateType(long id, long type) {
Log.w("MessageDatabase", "Updating ID: " + id + " to type: " + type);
ContentValues contentValues = new ContentValues();
contentValues.put(TYPE, type);
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {id+""});
notifyConversationListeners(getThreadIdForMessage(id));
}
private long insertMessageReceived(SmsMessage message, String body, long type) {
List<Recipient> recipientList = new ArrayList<Recipient>(1);
recipientList.add(new Recipient(null, message.getDisplayOriginatingAddress(), null));
Recipients recipients = new Recipients(recipientList);
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
ContentValues values = new ContentValues(6);
values.put(ADDRESS, message.getDisplayOriginatingAddress());
values.put(DATE, new Long(System.currentTimeMillis()));
values.put(PROTOCOL, message.getProtocolIdentifier());
values.put(READ, Integer.valueOf(0));
if (message.getPseudoSubject().length() > 0)
values.put(SUBJECT, message.getPseudoSubject());
values.put(REPLY_PATH_PRESENT, message.isReplyPathPresent() ? 1 : 0);
values.put(SERVICE_CENTER, message.getServiceCenterAddress());
values.put(BODY, body);
values.put(TYPE, type);
values.put(THREAD_ID, threadId);
SQLiteDatabase db = databaseHelper.getWritableDatabase();
long messageId = db.insert(TABLE_NAME, null, values);
DatabaseFactory.getThreadDatabase(context).setUnread(threadId);
DatabaseFactory.getThreadDatabase(context).update(threadId);
notifyConversationListeners(threadId);
return messageId;
}
public long getThreadIdForMessage(long id) {
String sql = "SELECT " + THREAD_ID + " FROM " + TABLE_NAME + " WHERE " + ID + " = ?";
String[] sqlArgs = new String[] {id+""};
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = null;
try {
cursor = db.rawQuery(sql, sqlArgs);
if (cursor != null && cursor.moveToFirst())
return cursor.getLong(0);
else
return -1;
} finally {
if (cursor != null)
cursor.close();
}
}
public int getMessageCountForThread(long threadId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = null;
try {
cursor = db.query(TABLE_NAME, new String[] {"COUNT(*)"}, THREAD_ID + " = ?", new String[] {threadId+""}, null, null, null);
if (cursor != null && cursor.moveToFirst())
return cursor.getInt(0);
} finally {
if (cursor != null)
cursor.close();
}
return 0;
}
public void markAsDecryptFailed(long id) {
updateType(id, Types.FAILED_DECRYPT_TYPE);
}
public void markAsNoSession(long id) {
updateType(id, Types.NO_SESSION_TYPE);
}
public void markAsDecrypting(long id) {
updateType(id, Types.DECRYPT_IN_PROGRESS_TYPE);
}
public void markAsSent(long id, long type) {
if (type == Types.ENCRYPTING_TYPE)
updateType(id, Types.SECURE_SENT_TYPE);
else
updateType(id, Types.SENT_TYPE);
}
public void markAsSentFailed(long id) {
updateType(id, Types.FAILED_TYPE);
}
public void setMessagesRead(long threadId) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues();
contentValues.put(READ, 1);
long start = System.currentTimeMillis();
database.update(TABLE_NAME, contentValues, THREAD_ID + " = ? AND " + READ + " = 0", new String[] {threadId+""});
long end = System.currentTimeMillis();
Log.w("SmsDatabase", "setMessagesRead time: " + (end-start));
}
public void updateMessageBodyAndType(long messageId, String body, long type) {
ContentValues contentValues = new ContentValues();
contentValues.put(BODY, body);
contentValues.put(TYPE, type);
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {messageId+""});
DatabaseFactory.getThreadDatabase(context).update(getThreadIdForMessage(messageId));
notifyConversationListeners(getThreadIdForMessage(messageId));
notifyConversationListListeners();
}
public long insertSecureMessageReceived(SmsMessage message, String body) {
return insertMessageReceived(message, body, Types.DECRYPT_IN_PROGRESS_TYPE);
}
public long insertMessageReceived(SmsMessage message, String body) {
return insertMessageReceived(message, body, Types.INBOX_TYPE);
}
public long insertMessageSent(String address, long threadId, String body, long date, long type) {
ContentValues contentValues = new ContentValues(6);
// contentValues.put(ADDRESS, NumberUtil.filterNumber(address));
contentValues.put(ADDRESS, address);
contentValues.put(THREAD_ID, threadId);
contentValues.put(BODY, body);
contentValues.put(DATE, date);
contentValues.put(READ, 1);
contentValues.put(TYPE, type);
SQLiteDatabase db = databaseHelper.getWritableDatabase();
long messageId = db.insert(TABLE_NAME, ADDRESS, contentValues);
DatabaseFactory.getThreadDatabase(context).setRead(threadId);
DatabaseFactory.getThreadDatabase(context).update(threadId);
notifyConversationListeners(threadId);
return messageId;
}
public Cursor getOutgoingMessages() {
String outgoingSelection = "(" + TYPE + " = " + Types.ENCRYPTING_TYPE + " OR " + TYPE + " = " + Types.ENCRYPTED_OUTBOX_TYPE + ")";
SQLiteDatabase db = databaseHelper.getReadableDatabase();
return db.query(TABLE_NAME, null, outgoingSelection, null, null, null, null);
}
public Cursor getDecryptInProgressMessages() {
String where = TYPE + " = " + Types.DECRYPT_IN_PROGRESS_TYPE;
SQLiteDatabase db = databaseHelper.getReadableDatabase();
return db.query(TABLE_NAME, null, where, null, null, null, null);
}
public Cursor getEncryptedRogueMessages(Recipient recipient) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String selection = TYPE + " = " + Types.NO_SESSION_TYPE + " AND PHONE_NUMBERS_EQUAL(" + ADDRESS + ", ?)";
String[] args = {recipient.getNumber()};
return db.query(TABLE_NAME, null, selection, args, null, null, null);
}
public Cursor getMessage(long messageId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
return db.query(TABLE_NAME, null, ID_WHERE, new String[] {messageId+""}, null, null, null);
}
public void deleteMessage(long messageId) {
Log.w("MessageDatabase", "Deleting: " + messageId);
SQLiteDatabase db = databaseHelper.getWritableDatabase();
long threadId = getThreadIdForMessage(messageId);
db.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""});
DatabaseFactory.getThreadDatabase(context).update(threadId);
notifyConversationListeners(threadId);
}
/*package */void deleteThread(long threadId) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.delete(TABLE_NAME, THREAD_ID + " = ?", new String[] {threadId+""});
}
/*package*/ void deleteThreads(Set<Long> threadIds) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
String where = "";
for (long threadId : threadIds) {
where += THREAD_ID + " = '" + threadId + "' OR ";
}
where = where.substring(0, where.length() - 4);
db.delete(TABLE_NAME, where, null);
}
/*package */ void deleteAllThreads() {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.delete(TABLE_NAME, null, null);
}
/*package*/ SQLiteDatabase beginTransaction() {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
database.beginTransaction();
return database;
}
/*package*/ void endTransaction(SQLiteDatabase database) {
database.setTransactionSuccessful();
database.endTransaction();
}
/*package*/ void insertRaw(SQLiteDatabase database, ContentValues contentValues) {
database.insert(TABLE_NAME, null, contentValues);
}
/*package*/ SQLiteStatement createInsertStatement(SQLiteDatabase database) {
return database.compileStatement("INSERT INTO " + TABLE_NAME + " (" + ADDRESS + ", " + PERSON + ", " + DATE + ", " + PROTOCOL + ", " + READ + ", " + STATUS + ", " + TYPE + ", " + REPLY_PATH_PRESENT + ", " + SUBJECT + ", " + BODY + ", " + SERVICE_CENTER + ", THREAD_ID) " +
" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
}
public static class Types {
public static final int INBOX_TYPE = 1;
public static final int SENT_TYPE = 2;
public static final int SENT_PENDING = 4;
public static final int FAILED_TYPE = 5;
public static final int ENCRYPTING_TYPE = 42; // Messages are stored local encrypted and need async encryption and delivery.
public static final int ENCRYPTED_OUTBOX_TYPE = 43; // Messages are stored local encrypted and need delivery.
public static final int SECURE_SENT_TYPE = 44; // Messages were sent with async encryption.
public static final int SECURE_RECEIVED_TYPE = 45; // Messages were received with async decryption.
public static final int FAILED_DECRYPT_TYPE = 46; // Messages were received with async encryption and failed to decrypt.
public static final int DECRYPT_IN_PROGRESS_TYPE = 47; // Messages are in the process of being asymmetricaly decrypted.
public static final int NO_SESSION_TYPE = 48; // Messages were received with async encryption but there is no session yet.
public static boolean isFailedMessageType(long type) {
return type == FAILED_TYPE;
}
public static boolean isOutgoingMessageType(long type) {
return type == SENT_TYPE || type == SENT_PENDING || type == ENCRYPTING_TYPE || type == ENCRYPTED_OUTBOX_TYPE || type == SECURE_SENT_TYPE || type == FAILED_TYPE;
}
public static boolean isPendingMessageType(long type) {
return type == SENT_PENDING || type == ENCRYPTING_TYPE || type == ENCRYPTED_OUTBOX_TYPE;
}
public static boolean isSecureType(long type) {
return type == SECURE_SENT_TYPE || type == ENCRYPTING_TYPE || type == SECURE_RECEIVED_TYPE;
}
}
}

View File

@@ -0,0 +1,196 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.database;
import java.util.StringTokenizer;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.protocol.Prefix;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteStatement;
import android.net.Uri;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
public class SmsMigrator {
public static final int PROGRESS_UPDATE = 1;
public static final int SECONDARY_PROGRESS_UPDATE = 2;
public static final int COMPLETE = 3;
private static void addEncryptedStringToStatement(Context context, SQLiteStatement statement, Cursor cursor, MasterSecret masterSecret, int index, String key) {
int columnIndex = cursor.getColumnIndexOrThrow(key);
if (cursor.isNull(columnIndex))
statement.bindNull(index);
else
statement.bindString(index, encryptIfNecessary(context, masterSecret, cursor.getString(columnIndex)));
}
private static void addStringToStatement(SQLiteStatement statement, Cursor cursor, int index, String key) {
int columnIndex = cursor.getColumnIndexOrThrow(key);
if (cursor.isNull(columnIndex))
statement.bindNull(index);
else
statement.bindString(index, cursor.getString(columnIndex));
}
private static void addIntToStatement(SQLiteStatement statement, Cursor cursor, int index, String key) {
int columnIndex = cursor.getColumnIndexOrThrow(key);
if (cursor.isNull(columnIndex))
statement.bindNull(index);
else
statement.bindLong(index, cursor.getLong(columnIndex));
}
private static void getContentValuesForRow(Context context, MasterSecret masterSecret, Cursor cursor, long threadId, SQLiteStatement statement) {
addStringToStatement(statement, cursor, 1, SmsDatabase.ADDRESS);
addIntToStatement(statement, cursor, 2, SmsDatabase.PERSON);
addIntToStatement(statement, cursor, 3, SmsDatabase.DATE);
addIntToStatement(statement, cursor, 4, SmsDatabase.PROTOCOL);
addIntToStatement(statement, cursor, 5, SmsDatabase.READ);
addIntToStatement(statement, cursor, 6, SmsDatabase.STATUS);
addIntToStatement(statement, cursor, 7, SmsDatabase.TYPE);
addIntToStatement(statement, cursor, 8, SmsDatabase.REPLY_PATH_PRESENT);
addStringToStatement(statement, cursor, 9, SmsDatabase.SUBJECT);
addEncryptedStringToStatement(context, statement, cursor, masterSecret, 10, SmsDatabase.BODY);
addStringToStatement(statement, cursor, 11, SmsDatabase.SERVICE_CENTER);
statement.bindLong(12, threadId);
}
private static String getTheirCanonicalAddress(Context context, String theirRecipientId) {
Uri uri = Uri.parse("content://mms-sms/canonical-address/" + theirRecipientId);
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(uri, null, null, null, null);
if (cursor != null && cursor.moveToFirst())
return cursor.getString(0);
else
return null;
} finally {
if (cursor != null)
cursor.close();
}
}
private static Recipients getOurRecipients(Context context, String theirRecipients) {
StringTokenizer tokenizer = new StringTokenizer(theirRecipients.trim(), " ");
StringBuilder sb = new StringBuilder();
while (tokenizer.hasMoreTokens()) {
String theirRecipientId = tokenizer.nextToken();
String address = getTheirCanonicalAddress(context, theirRecipientId);
if (address == null)
continue;
if (sb.length() != 0)
sb.append(',');
sb.append(address);
}
try {
if (sb.length() == 0) return null;
else return RecipientFactory.getRecipientsFromString(context, sb.toString());
} catch (RecipientFormattingException rfe) {
Log.w("SmsMigrator", rfe);
return null;
}
}
private static String encryptIfNecessary(Context context, MasterSecret masterSecret, String body) {
if (!body.startsWith(Prefix.SYMMETRIC_ENCRYPT) && !body.startsWith(Prefix.ASYMMETRIC_ENCRYPT)) {
MasterCipher masterCipher = new MasterCipher(masterSecret);
return Prefix.SYMMETRIC_ENCRYPT + masterCipher.encryptBody(body);
}
return body;
}
private static void migrateConversation(Context context, MasterSecret masterSecret, Handler handler, long theirThreadId, long ourThreadId) {
SmsDatabase ourSmsDatabase = DatabaseFactory.getSmsDatabase(context);
Cursor cursor = null;
try {
Uri uri = Uri.parse("content://sms/conversations/" + theirThreadId);
cursor = context.getContentResolver().query(uri, null, null, null, null);
SQLiteDatabase transaction = ourSmsDatabase.beginTransaction();
SQLiteStatement statement = ourSmsDatabase.createInsertStatement(transaction);
while (cursor != null && cursor.moveToNext()) {
getContentValuesForRow(context, masterSecret, cursor, ourThreadId, statement);
statement.execute();
Message msg = handler.obtainMessage(SECONDARY_PROGRESS_UPDATE, 10000/cursor.getCount(), 0);
handler.sendMessage(msg);
}
ourSmsDatabase.endTransaction(transaction);
DatabaseFactory.getThreadDatabase(context).update(ourThreadId);
DatabaseFactory.getThreadDatabase(context).notifyConversationListeners(ourThreadId);
} finally {
if (cursor != null)
cursor.close();
}
}
public static void migrateDatabase(Context context, MasterSecret masterSecret, Handler handler) {
if (context.getSharedPreferences("SecureSMS", Context.MODE_PRIVATE).getBoolean("migrated", false))
return;
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
Cursor cursor = null;
try {
Uri threadListUri = Uri.parse("content://mms-sms/conversations?simple=true");
cursor = context.getContentResolver().query(threadListUri, null, null, null, "date ASC");
while (cursor != null && cursor.moveToNext()) {
long theirThreadId = cursor.getLong(cursor.getColumnIndexOrThrow("_id"));
String theirRecipients = cursor.getString(cursor.getColumnIndexOrThrow("recipient_ids"));
Recipients ourRecipients = getOurRecipients(context, theirRecipients);
if (ourRecipients != null) {
long ourThreadId = threadDatabase.getThreadIdFor(ourRecipients);
migrateConversation(context, masterSecret, handler, theirThreadId, ourThreadId);
}
Message msg = handler.obtainMessage(PROGRESS_UPDATE, 10000/cursor.getCount(), 0);
handler.sendMessage(msg);
}
} finally {
if (cursor != null)
cursor.close();
}
context.getSharedPreferences("SecureSMS", Context.MODE_PRIVATE).edit().putBoolean("migrated", true).commit();
handler.sendEmptyMessage(COMPLETE);
}
}

View File

@@ -0,0 +1,294 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.database;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
public class ThreadDatabase extends Database {
private static final String TABLE_NAME = "thread";
public static final String ID = "_id";
public static final String DATE = "date";
public static final String MESSAGE_COUNT = "message_count";
public static final String RECIPIENT_IDS = "recipient_ids";
public static final String SNIPPET = "snippet";
private static final String SNIPPET_CHARSET = "snippet_cs";
public static final String READ = "read";
private static final String TYPE = "type";
private static final String ERROR = "error";
private static final String HAS_ATTACHMENT = "has_attachment";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
DATE + " INTEGER DEFAULT 0, " + MESSAGE_COUNT + " INTEGER DEFAULT 0, " +
RECIPIENT_IDS + " TEXT, " + SNIPPET + " TEXT, " + SNIPPET_CHARSET + " INTEGER DEFAULT 0, " +
READ + " INTEGER DEFAULT 1, " + TYPE + " INTEGER DEFAULT 0, " + ERROR + " INTEGER DEFAULT 0, " +
HAS_ATTACHMENT + " INTEGER DEFAULT 0);";
public ThreadDatabase(Context context, SQLiteOpenHelper databaseHelper) {
super(context, databaseHelper);
}
private long[] getRecipientIds(Recipients recipients) {
Set<Long> recipientSet = new HashSet<Long>();
List<Recipient> recipientList = recipients.getRecipientsList();
for (Recipient recipient : recipientList) {
// String number = NumberUtil.filterNumber(recipient.getNumber());
String number = recipient.getNumber();
recipientSet.add(new Long(DatabaseFactory.getAddressDatabase(context).getCanonicalAddress(number)));
}
long[] recipientArray = new long[recipientSet.size()];
int i = 0;
for (Long recipientId : recipientSet) {
recipientArray[i++] = recipientId;
}
Arrays.sort(recipientArray);
return recipientArray;
}
private String getRecipientsAsString(long[] recipientIds) {
StringBuilder sb = new StringBuilder();
for (int i=0;i<recipientIds.length;i++) {
if (i != 0) sb.append(' ');
sb.append(recipientIds[i]);
}
return sb.toString();
}
private long createThreadForRecipients(String recipients, int recipientCount) {
ContentValues contentValues = new ContentValues(4);
long date = System.currentTimeMillis();
contentValues.put(DATE, date - date % 1000);
contentValues.put(RECIPIENT_IDS, recipients);
if (recipientCount > 1)
contentValues.put(TYPE, 1);
contentValues.put(MESSAGE_COUNT, 0);
SQLiteDatabase db = databaseHelper.getWritableDatabase();
return db.insert(TABLE_NAME, null, contentValues);
}
private void updateThread(long threadId, long count, String body, long date) {
ContentValues contentValues = new ContentValues(3);
contentValues.put(DATE, date - date % 1000);
contentValues.put(MESSAGE_COUNT, count);
contentValues.put(SNIPPET, body);
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.update(TABLE_NAME, contentValues, ID + " = ?", new String[] {threadId+""});
notifyConversationListListeners();
}
private void deleteThread(long threadId) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.delete(TABLE_NAME, ID_WHERE, new String[] {threadId+""});
notifyConversationListListeners();
}
private void deleteThreads(Set<Long> threadIds) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
String where = "";
for (long threadId : threadIds) {
where += ID + " = '" + threadId + "' OR ";
}
where = where.substring(0, where.length() - 4);
db.delete(TABLE_NAME, where, null);
notifyConversationListListeners();
}
private void deleteAllThreads() {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.delete(TABLE_NAME, null, null);
notifyConversationListListeners();
}
public void setRead(long threadId) {
ContentValues contentValues = new ContentValues(1);
contentValues.put(READ, 1);
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId+""});
DatabaseFactory.getSmsDatabase(context).setMessagesRead(threadId);
DatabaseFactory.getMmsDatabase(context).setMessagesRead(threadId);
notifyConversationListListeners();
}
public void setUnread(long threadId) {
ContentValues contentValues = new ContentValues(1);
contentValues.put("read", 0);
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId+""});
notifyConversationListListeners();
}
public Cursor getFilteredConversationList(List<String> filter) {
if (filter == null || filter.size() == 0)
return null;
List<Long> recipientIds = DatabaseFactory.getAddressDatabase(context).getCanonicalAddresses(filter);
if (recipientIds == null || recipientIds.size() == 0)
return null;
String selection = RECIPIENT_IDS + " = ?";
String[] selectionArgs = new String[recipientIds.size()];
for (int i=0;i<recipientIds.size()-1;i++)
selection += (" OR " + RECIPIENT_IDS + " = ?");
int i= 0;
for (long id : recipientIds) {
selectionArgs[i++] = id+"";
}
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, null, selection, selectionArgs, null, null, DATE + " DESC");
setNotifyConverationListListeners(cursor);
return cursor;
}
public Cursor getConversationList() {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, null, null, null, null, null, DATE + " DESC");
setNotifyConverationListListeners(cursor);
return cursor;
}
public void deleteConversation(long threadId) {
DatabaseFactory.getSmsDatabase(context).deleteThread(threadId);
DatabaseFactory.getMmsDatabase(context).deleteThread(threadId);
deleteThread(threadId);
notifyConversationListeners(threadId);
notifyConversationListListeners();
}
public void deleteConversations(Set<Long> selectedConversations) {
DatabaseFactory.getSmsDatabase(context).deleteThreads(selectedConversations);
DatabaseFactory.getMmsDatabase(context).deleteThreads(selectedConversations);
deleteThreads(selectedConversations);
notifyConversationListeners(selectedConversations);
notifyConversationListListeners();
}
public void deleteAllConversations() {
DatabaseFactory.getSmsDatabase(context).deleteAllThreads();
DatabaseFactory.getMmsDatabase(context).deleteAllThreads();
deleteAllThreads();
}
public long getThreadIdIfExistsFor(Recipients recipients) {
long[] recipientIds = getRecipientIds(recipients);
String recipientsList = getRecipientsAsString(recipientIds);
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String where = RECIPIENT_IDS + " = ?";
String[] recipientsArg = new String[] {recipientsList};
Cursor cursor = null;
try {
cursor = db.query(TABLE_NAME, new String[]{ID}, where, recipientsArg, null, null, null);
if (cursor != null && cursor.moveToFirst())
return cursor.getLong(cursor.getColumnIndexOrThrow(ID));
else
return -1L;
} finally {
if (cursor != null)
cursor.close();
}
}
public long getThreadIdFor(Recipients recipients) {
long[] recipientIds = getRecipientIds(recipients);
String recipientsList = getRecipientsAsString(recipientIds);
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String where = RECIPIENT_IDS + " = ?";
String[] recipientsArg = new String[] {recipientsList};
Cursor cursor = null;
try {
cursor = db.query(TABLE_NAME, new String[]{ID}, where, recipientsArg, null, null, null);
if (cursor != null && cursor.moveToFirst())
return cursor.getLong(cursor.getColumnIndexOrThrow(ID));
else
return createThreadForRecipients(recipientsList, recipientIds.length);
} finally {
if (cursor != null)
cursor.close();
}
}
public void update(long threadId) {
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context);
long count = mmsSmsDatabase.getConversationCount(threadId);
if (count == 0) {
deleteThread(threadId);
notifyConversationListListeners();
return;
}
Cursor cursor = null;
try {
cursor = mmsSmsDatabase.getConversationSnippet(threadId);
if (cursor != null && cursor.moveToFirst())
updateThread(threadId, count,
cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.BODY)),
cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.DATE)));
else
deleteThread(threadId);
} finally {
if (cursor != null)
cursor.close();
}
notifyConversationListListeners();
}
}