From fe43ef65ab9ad2390dcc8575f8667379c566166f Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Wed, 9 Jan 2013 21:06:56 -0800 Subject: [PATCH] Support for auto-deleting old messages beyond a certain conversation thread length. --- res/values/strings.xml | 18 +++- res/xml/preferences.xml | 29 +++++-- .../ApplicationPreferencesActivity.java | 76 +++++++++++++++-- .../securesms/database/MmsDatabase.java | 46 +++++++++- .../securesms/database/SmsDatabase.java | 28 ++++++- .../securesms/database/ThreadDatabase.java | 55 ++++++++++++ .../thoughtcrime/securesms/util/Trimmer.java | 83 +++++++++++++++++++ 7 files changed, 319 insertions(+), 16 deletions(-) create mode 100644 src/org/thoughtcrime/securesms/util/Trimmer.java diff --git a/res/values/strings.xml b/res/values/strings.xml index 906f91924c..16a463b4e4 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -39,7 +39,12 @@ You need to have entered your passphrase before importing keys... You need to have entered your passphrase before managing keys... You haven\'t set a passphrase yet! - + Conversation length limit + messages per conversation + Delete all old messages now? + Are you sure you would like to immediately trim all conversation threads to the %s most recent messages? + Delete + Picture @@ -377,6 +382,17 @@ MMSC URL (Required) MMS Proxy Host (Optional) MMS Proxy Port (Optional) + Delivery Reports + Request a delivery report for each SMS message you send + Request a delivery report for each MMS message you send + MMS delivery reports + SMS delivery reports + Automatically delete older messages once a conversation thread exceeds a specified length + Delete old messages + Storage + Conversation length limit + Trim all threads now + Scan through all conversation threads and enforce conversation length limits diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml index 1b15a6546f..9f9ddb1f6a 100644 --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -14,17 +14,36 @@ - + + android:summary="@string/preferences__request_a_delivery_report_for_each_sms_message_you_send" + android:title="@string/preferences__sms_delivery_reports" /> + android:title="@string/preferences__mms_delivery_reports" /> + + + + + + + + + diff --git a/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java b/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java index b718adb693..67f679e655 100644 --- a/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java +++ b/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java @@ -17,6 +17,8 @@ package org.thoughtcrime.securesms; import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; @@ -25,6 +27,7 @@ import android.preference.EditTextPreference; import android.preference.Preference; import android.preference.PreferenceManager; import android.provider.ContactsContract; +import android.util.Log; import android.widget.Toast; import com.actionbarsherlock.app.SherlockPreferenceActivity; @@ -38,6 +41,7 @@ import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.util.Dialogs; import org.thoughtcrime.securesms.util.MemoryCleaner; +import org.thoughtcrime.securesms.util.Trimmer; import java.util.List; @@ -83,6 +87,10 @@ public class ApplicationPreferencesActivity extends SherlockPreferenceActivity { public static final String SMS_DELIVERY_REPORT_PREF = "pref_delivery_report_sms"; public static final String MMS_DELIVERY_REPORT_PREF = "pref_delivery_report_mms"; + public static final String THREAD_TRIM_ENABLED = "pref_trim_threads"; + public static final String THREAD_TRIM_LENGTH = "pref_trim_length"; + public static final String THREAD_TRIM_NOW = "pref_trim_now"; + @Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); @@ -104,6 +112,10 @@ public class ApplicationPreferencesActivity extends SherlockPreferenceActivity { .setOnPreferenceClickListener(new ManageIdentitiesClickListener()); this.findPreference(CHANGE_PASSPHRASE_PREF) .setOnPreferenceClickListener(new ChangePassphraseClickListener()); + this.findPreference(THREAD_TRIM_NOW) + .setOnPreferenceClickListener(new TrimNowClickListener()); + this.findPreference(THREAD_TRIM_LENGTH) + .setOnPreferenceChangeListener(new TrimLengthValidationListener()); } @Override @@ -159,20 +171,16 @@ public class ApplicationPreferencesActivity extends SherlockPreferenceActivity { preference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { @Override public boolean onPreferenceChange(Preference pref, Object newValue) { - preference.setSummary(newValue == null ? "Not set" : (String)newValue); + preference.setSummary(newValue == null ? "Not set" : ((String)newValue)); return true; } }); } private void initializeEditTextSummaries() { - final EditTextPreference mmscUrlPreference = (EditTextPreference)this.findPreference(MMSC_HOST_PREF); - final EditTextPreference mmsProxyHostPreference = (EditTextPreference)this.findPreference(MMSC_PROXY_HOST_PREF); - final EditTextPreference mmsProxyPortPreference = (EditTextPreference)this.findPreference(MMSC_PROXY_PORT_PREF); - - initializeEditTextSummary(mmscUrlPreference); - initializeEditTextSummary(mmsProxyHostPreference); - initializeEditTextSummary(mmsProxyPortPreference); + initializeEditTextSummary((EditTextPreference)this.findPreference(MMSC_HOST_PREF)); + initializeEditTextSummary((EditTextPreference)this.findPreference(MMSC_PROXY_HOST_PREF)); + initializeEditTextSummary((EditTextPreference)this.findPreference(MMSC_PROXY_PORT_PREF)); } private void initializeIdentitySelection() { @@ -330,4 +338,56 @@ public class ApplicationPreferencesActivity extends SherlockPreferenceActivity { } } + private class TrimNowClickListener implements Preference.OnPreferenceClickListener { + @Override + public boolean onPreferenceClick(Preference preference) { + final int threadLengthLimit = Integer.parseInt(PreferenceManager.getDefaultSharedPreferences(ApplicationPreferencesActivity.this) + .getString(THREAD_TRIM_LENGTH, "500")); + + AlertDialog.Builder builder = new AlertDialog.Builder(ApplicationPreferencesActivity.this); + builder.setTitle(R.string.ApplicationPreferencesActivity_delete_all_old_messages_now); + builder.setMessage(String.format(getString(R.string.ApplicationPreferencesActivity_are_you_sure_you_would_like_to_immediately_trim_all_conversation_threads_to_the_s_most_recent_messages), + threadLengthLimit)); + builder.setPositiveButton(R.string.ApplicationPreferencesActivity_delete, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Trimmer.trimAllThreads(ApplicationPreferencesActivity.this, threadLengthLimit); + } + }); + + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); + + return true; + } + } + + private class TrimLengthValidationListener implements Preference.OnPreferenceChangeListener { + + public TrimLengthValidationListener() { + EditTextPreference preference = (EditTextPreference)findPreference(THREAD_TRIM_LENGTH); + preference.setSummary(preference.getText() + " " + getString(R.string.ApplicationPreferencesActivity_messages_per_conversation)); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (newValue == null || ((String)newValue).trim().length() == 0) { + return false; + } + + try { + Integer.parseInt((String)newValue); + } catch (NumberFormatException nfe) { + Log.w("ApplicationPreferencesActivity", nfe); + return false; + } + + preference.setSummary((String)newValue + " " + + getString(R.string.ApplicationPreferencesActivity_messages_per_conversation)); + return true; + } + + } + } diff --git a/src/org/thoughtcrime/securesms/database/MmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsDatabase.java index a4f1ecf840..d93a019cfb 100644 --- a/src/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.RecipientFormattingException; import org.thoughtcrime.securesms.recipients.Recipients; +import org.thoughtcrime.securesms.util.Trimmer; import ws.com.google.android.mms.InvalidHeaderValueException; import ws.com.google.android.mms.MmsException; @@ -343,6 +344,7 @@ public class MmsDatabase extends Database { notifyConversationListeners(threadId); DatabaseFactory.getThreadDatabase(context).update(threadId); DatabaseFactory.getThreadDatabase(context).setUnread(threadId); + Trimmer.trimThread(context, threadId); return messageId; } catch (RecipientFormattingException rfe) { @@ -364,6 +366,8 @@ public class MmsDatabase extends Database { long messageId = insertMediaMessage(sendRequest, contentValues); DatabaseFactory.getThreadDatabase(context).setRead(threadId); + Trimmer.trimThread(context, threadId); + return messageId; } @@ -427,6 +431,35 @@ public class MmsDatabase extends Database { } } + /*package*/void deleteMessagesInThreadBeforeDate(long threadId, long date) { + date = date / 1000; + Cursor cursor = null; + + try { + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + String where = THREAD_ID + " = ? AND (CASE " + MESSAGE_BOX; + + for (int outgoingType : Types.OUTGOING_MAILBOX_TYPES) { + where += " WHEN " + outgoingType + " THEN " + DATE_SENT + " < " + date; + } + + where += (" ELSE " + DATE_RECEIVED + " < " + date + " END)"); + + Log.w("MmsDatabase", "Executing trim query: " + where); + cursor = db.query(TABLE_NAME, new String[] {ID}, where, new String[] {threadId+""}, null, null, null); + + while (cursor != null && cursor.moveToNext()) { + Log.w("MmsDatabase", "Trimming: " + cursor.getLong(0)); + delete(cursor.getLong(0)); + } + + } finally { + if (cursor != null) + cursor.close(); + } + } + + public void deleteAllThreads() { DatabaseFactory.getPartDatabase(context).deleteAllParts(); DatabaseFactory.getMmsAddressDatabase(context).deleteAllAddresses(); @@ -553,12 +586,23 @@ public class MmsDatabase extends Database { public static final int DOWNLOAD_SOFT_FAILURE = 4; public static final int DOWNLOAD_HARD_FAILURE = 5; + public static final int[] OUTGOING_MAILBOX_TYPES = {Types.MESSAGE_BOX_OUTBOX, + Types.MESSAGE_BOX_SENT, + Types.MESSAGE_BOX_SECURE_OUTBOX, + Types.MESSAGE_BOX_SENT_FAILED, + Types.MESSAGE_BOX_SECURE_SENT}; + 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; + for (int outgoingMailboxType : OUTGOING_MAILBOX_TYPES) { + if (mailbox == outgoingMailboxType) + return true; + } + + return false; } public static boolean isPendingMmsBox(long mailbox) { diff --git a/src/org/thoughtcrime/securesms/database/SmsDatabase.java b/src/org/thoughtcrime/securesms/database/SmsDatabase.java index 11c3dfe0e7..1ca27d9243 100644 --- a/src/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -27,6 +27,7 @@ import android.util.Log; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipients; +import org.thoughtcrime.securesms.util.Trimmer; import java.util.ArrayList; import java.util.List; @@ -112,6 +113,8 @@ public class SmsDatabase extends Database { DatabaseFactory.getThreadDatabase(context).setUnread(threadId); DatabaseFactory.getThreadDatabase(context).update(threadId); notifyConversationListeners(threadId); + Trimmer.trimThread(context, threadId); + return messageId; } @@ -235,6 +238,8 @@ public class SmsDatabase extends Database { DatabaseFactory.getThreadDatabase(context).setRead(threadId); DatabaseFactory.getThreadDatabase(context).update(threadId); notifyConversationListeners(threadId); + Trimmer.trimThread(context, threadId); + return messageId; } @@ -276,6 +281,18 @@ public class SmsDatabase extends Database { db.delete(TABLE_NAME, THREAD_ID + " = ?", new String[] {threadId+""}); } + /*package*/void deleteMessagesInThreadBeforeDate(long threadId, long date) { + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + String where = THREAD_ID + " = ? AND (CASE " + TYPE; + + for (int outgoingType : Types.OUTGOING_MESSAGE_TYPES) { + where += " WHEN " + outgoingType + " THEN " + DATE_SENT + " < " + date; + } + + where += (" ELSE " + DATE_RECEIVED + " < " + date + " END)"); + + db.delete(TABLE_NAME, where, new String[] {threadId+""}); + } /*package*/ void deleteThreads(Set threadIds) { SQLiteDatabase db = databaseHelper.getWritableDatabase(); @@ -348,12 +365,21 @@ public class SmsDatabase extends Database { 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 final int[] OUTGOING_MESSAGE_TYPES = {SENT_TYPE, SENT_PENDING, ENCRYPTING_TYPE, + ENCRYPTED_OUTBOX_TYPE, SECURE_SENT_TYPE, + FAILED_TYPE}; + 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; + for (int outgoingType : OUTGOING_MESSAGE_TYPES) { + if (type == outgoingType) + return true; + } + + return false; } public static boolean isPendingMessageType(long type) { diff --git a/src/org/thoughtcrime/securesms/database/ThreadDatabase.java b/src/org/thoughtcrime/securesms/database/ThreadDatabase.java index c4511e8fed..8f6def255c 100644 --- a/src/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/src/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -21,6 +21,7 @@ import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipients; @@ -143,6 +144,56 @@ public class ThreadDatabase extends Database { notifyConversationListListeners(); } + public void trimAllThreads(int length, ProgressListener listener) { + Cursor cursor = null; + int threadCount = 0; + int complete = 0; + + try { + cursor = this.getConversationList(); + + if (cursor != null) + threadCount = cursor.getCount(); + + while (cursor != null && cursor.moveToNext()) { + long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(ID)); + trimThread(threadId, length); + + listener.onProgress(++complete, threadCount); + } + } finally { + if (cursor != null) + cursor.close(); + } + } + + public void trimThread(long threadId, int length) { + Log.w("ThreadDatabase", "Trimming thread: " + threadId + " to: " + length); + Cursor cursor = null; + + try { + cursor = DatabaseFactory.getMmsSmsDatabase(context).getConversation(threadId); + + if (cursor != null && cursor.getCount() > length) { + Log.w("ThreadDatabase", "Cursor count is greater than length!"); + cursor.moveToPosition(cursor.getCount() - length); + + long lastTweetDate = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsDatabase.DATE_RECEIVED)); + + Log.w("ThreadDatabase", "Cut off tweet date: " + lastTweetDate); + + DatabaseFactory.getSmsDatabase(context).deleteMessagesInThreadBeforeDate(threadId, lastTweetDate); + DatabaseFactory.getMmsDatabase(context).deleteMessagesInThreadBeforeDate(threadId, lastTweetDate); + + update(threadId); + notifyConversationListeners(threadId); + } + } finally { + if (cursor != null) + cursor.close(); + } + } + public void setRead(long threadId) { ContentValues contentValues = new ContentValues(1); contentValues.put(READ, 1); @@ -290,4 +341,8 @@ public class ThreadDatabase extends Database { notifyConversationListListeners(); } + + public static interface ProgressListener { + public void onProgress(int complete, int total); + } } diff --git a/src/org/thoughtcrime/securesms/util/Trimmer.java b/src/org/thoughtcrime/securesms/util/Trimmer.java new file mode 100644 index 0000000000..021c7fb5eb --- /dev/null +++ b/src/org/thoughtcrime/securesms/util/Trimmer.java @@ -0,0 +1,83 @@ +package org.thoughtcrime.securesms.util; + +import android.app.ProgressDialog; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.AsyncTask; +import android.preference.PreferenceManager; +import android.widget.Toast; + +import org.thoughtcrime.securesms.ApplicationPreferencesActivity; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.ThreadDatabase; + +public class Trimmer { + + public static void trimAllThreads(Context context, int threadLengthLimit) { + new TrimmingProgressTask(context).execute(threadLengthLimit); + } + + public static void trimThread(final Context context, final long threadId) { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + boolean trimmingEnabled = preferences.getBoolean(ApplicationPreferencesActivity.THREAD_TRIM_ENABLED, false); + final int threadLengthLimit = Integer.parseInt(preferences.getString(ApplicationPreferencesActivity.THREAD_TRIM_LENGTH, "500")); + + if (!trimmingEnabled) + return; + + new Thread() { + @Override + public void run() { + DatabaseFactory.getThreadDatabase(context).trimThread(threadId, threadLengthLimit); + } + }.start(); + } + + private static class TrimmingProgressTask extends AsyncTask implements ThreadDatabase.ProgressListener { + private ProgressDialog progressDialog; + private Context context; + + public TrimmingProgressTask(Context context) { + this.context = context; + } + + @Override + protected void onPreExecute() { + progressDialog = new ProgressDialog(context); + progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); + progressDialog.setCancelable(false); + progressDialog.setIndeterminate(false); + progressDialog.setTitle("Deleting..."); + progressDialog.setMessage("Deleting old messages..."); + progressDialog.setMax(100); + progressDialog.show(); + } + + @Override + protected Void doInBackground(Integer... params) { + DatabaseFactory.getThreadDatabase(context).trimAllThreads(params[0], this); + return null; + } + + @Override + protected void onProgressUpdate(Integer... progress) { + double count = progress[1]; + double index = progress[0]; + + progressDialog.setProgress((int)Math.round((index / count) * 100.0)); + } + + @Override + protected void onPostExecute(Void result) { + progressDialog.dismiss(); + Toast.makeText(context, + "Old messages successfully deleted!", + Toast.LENGTH_LONG).show(); + } + + @Override + public void onProgress(int complete, int total) { + this.publishProgress(complete, total); + } + } +}