mirror of
https://github.com/oxen-io/session-android.git
synced 2025-10-25 12:08:36 +00:00
Initial Project Import
This commit is contained in:
127
src/org/thoughtcrime/securesms/database/ApplicationExporter.java
Normal file
127
src/org/thoughtcrime/securesms/database/ApplicationExporter.java
Normal 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");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 "";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
61
src/org/thoughtcrime/securesms/database/Database.java
Normal file
61
src/org/thoughtcrime/securesms/database/Database.java
Normal 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));
|
||||
}
|
||||
|
||||
}
|
||||
159
src/org/thoughtcrime/securesms/database/DatabaseFactory.java
Normal file
159
src/org/thoughtcrime/securesms/database/DatabaseFactory.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
125
src/org/thoughtcrime/securesms/database/IdentityDatabase.java
Normal file
125
src/org/thoughtcrime/securesms/database/IdentityDatabase.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
146
src/org/thoughtcrime/securesms/database/LocalKeyRecord.java
Normal file
146
src/org/thoughtcrime/securesms/database/LocalKeyRecord.java
Normal 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);
|
||||
}
|
||||
}
|
||||
181
src/org/thoughtcrime/securesms/database/MessageRecord.java
Normal file
181
src/org/thoughtcrime/securesms/database/MessageRecord.java
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
135
src/org/thoughtcrime/securesms/database/MmsAddressDatabase.java
Normal file
135
src/org/thoughtcrime/securesms/database/MmsAddressDatabase.java
Normal 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 "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
580
src/org/thoughtcrime/securesms/database/MmsDatabase.java
Normal file
580
src/org/thoughtcrime/securesms/database/MmsDatabase.java
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
146
src/org/thoughtcrime/securesms/database/MmsMessageRecord.java
Normal file
146
src/org/thoughtcrime/securesms/database/MmsMessageRecord.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
122
src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
Normal file
122
src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
Normal 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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
360
src/org/thoughtcrime/securesms/database/PartDatabase.java
Normal file
360
src/org/thoughtcrime/securesms/database/PartDatabase.java
Normal 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 "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
95
src/org/thoughtcrime/securesms/database/Record.java
Normal file
95
src/org/thoughtcrime/securesms/database/Record.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
149
src/org/thoughtcrime/securesms/database/RemoteKeyRecord.java
Normal file
149
src/org/thoughtcrime/securesms/database/RemoteKeyRecord.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
97
src/org/thoughtcrime/securesms/database/SessionKey.java
Normal file
97
src/org/thoughtcrime/securesms/database/SessionKey.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
227
src/org/thoughtcrime/securesms/database/SessionRecord.java
Normal file
227
src/org/thoughtcrime/securesms/database/SessionRecord.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
330
src/org/thoughtcrime/securesms/database/SmsDatabase.java
Normal file
330
src/org/thoughtcrime/securesms/database/SmsDatabase.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
196
src/org/thoughtcrime/securesms/database/SmsMigrator.java
Normal file
196
src/org/thoughtcrime/securesms/database/SmsMigrator.java
Normal 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);
|
||||
}
|
||||
}
|
||||
294
src/org/thoughtcrime/securesms/database/ThreadDatabase.java
Normal file
294
src/org/thoughtcrime/securesms/database/ThreadDatabase.java
Normal 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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user