configurable local message retention period. (untested)

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/Config.java                         |  2 
src/main/java/eu/siacs/conversations/entities/Conversation.java          | 12 
src/main/java/eu/siacs/conversations/parser/MessageParser.java           | 10 
src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java    |  7 
src/main/java/eu/siacs/conversations/services/MessageArchiveService.java | 12 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java | 53 
src/main/java/eu/siacs/conversations/ui/SettingsActivity.java            |  1 
src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java      |  9 
src/main/res/values/arrays.xml                                           | 14 
src/main/res/values/strings.xml                                          |  7 
src/main/res/xml/preferences.xml                                         | 12 
11 files changed, 114 insertions(+), 25 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/Config.java 🔗

@@ -114,6 +114,8 @@ public final class Config {
 	public static final ChatState DEFAULT_CHATSTATE = ChatState.ACTIVE;
 	public static final int TYPING_TIMEOUT = 8;
 
+	public static final int EXPIRY_INTERVAL = 30 * 60 * 1000; // 30 minutes
+
 	public static final String ENABLED_CIPHERS[] = {
 		"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
 		"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA384",

src/main/java/eu/siacs/conversations/entities/Conversation.java 🔗

@@ -20,6 +20,7 @@ import java.util.Collections;
 import java.util.Comparator;
 import java.util.Iterator;
 import java.util.List;
+import java.util.ListIterator;
 import java.util.Locale;
 
 import eu.siacs.conversations.Config;
@@ -930,6 +931,17 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
 		account.getPgpDecryptionService().decrypt(messages);
 	}
 
+	public void expireOldMessages(long timestamp) {
+		synchronized (this.messages) {
+			for(ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext();) {
+				if (iterator.next().getTimeSent() < timestamp) {
+					iterator.remove();
+				}
+			}
+			untieMessages();
+		}
+	}
+
 	public void sort() {
 		synchronized (this.messages) {
 			Collections.sort(this.messages, new Comparator<Message>() {

src/main/java/eu/siacs/conversations/parser/MessageParser.java 🔗

@@ -521,6 +521,12 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
 				}
 			}
 
+			long deletionDate = mXmppConnectionService.getAutomaticMessageDeletionDate();
+			if (deletionDate != 0 && message.getTimeSent() < deletionDate) {
+				Log.d(Config.LOGTAG,account.getJid().toBareJid()+": skipping message from "+message.getCounterpart().toString()+" because it was sent prior to our deletion date");
+				return;
+			}
+
 			boolean checkForDuplicates = query != null
 					|| (isTypeGroupChat && packet.hasChild("delay","urn:xmpp:delay"))
 					|| message.getType() == Message.TYPE_PRIVATE;
@@ -570,9 +576,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
 				conversation.endOtrIfNeeded();
 			}
 
-			if (message.getEncryption() == Message.ENCRYPTION_NONE || mXmppConnectionService.saveEncryptedMessages()) {
-				mXmppConnectionService.databaseBackend.createMessage(message);
-			}
+			mXmppConnectionService.databaseBackend.createMessage(message);
 			final HttpConnectionManager manager = this.mXmppConnectionService.getHttpConnectionManager();
 			if (message.trusted() && message.treatAsDownloadable() != Message.Decision.NEVER && manager.getAutoAcceptFileSize() > 0) {
 				manager.createNewDownloadConnection(message);

src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java 🔗

@@ -769,6 +769,13 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 		db.delete(Message.TABLENAME, Message.CONVERSATION + "=?", args);
 	}
 
+	public boolean expireOldMessages(long timestamp) {
+		String where = Message.TIME_SENT+"<?";
+		String[] whereArgs = {String.valueOf(timestamp)};
+		SQLiteDatabase db = this.getReadableDatabase();
+		return db.delete(Message.TABLENAME,where,whereArgs) > 0;
+	}
+
 	public Pair<Long, String> getLastMessageReceived(Account account) {
 		Cursor cursor = null;
 		try {

src/main/java/eu/siacs/conversations/services/MessageArchiveService.java 🔗

@@ -56,6 +56,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
 			startCatchup = lastClearDate.first;
 			reference = null;
 		}
+		startCatchup = Math.max(startCatchup,mXmppConnectionService.getAutomaticMessageDeletionDate());
 		long endCatchup = account.getXmppConnection().getLastSessionEstablished();
 		final Query query;
 		if (startCatchup == 0) {
@@ -107,12 +108,15 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
 
 	public Query query(Conversation conversation, long start, long end) {
 		synchronized (this.queries) {
-			if (start > end) {
-				return null;
-			}
 			final Query query = new Query(conversation, start, end,PagingOrder.REVERSE);
 			if (start==0) {
 				query.reference = conversation.getFirstMamReference();
+				Log.d(Config.LOGTAG,"setting mam reference");
+			}
+			Log.d(Config.LOGTAG,"checking max of "+start+" end "+mXmppConnectionService.getAutomaticMessageDeletionDate());
+			query.start = Math.max(start,mXmppConnectionService.getAutomaticMessageDeletionDate());
+			if (start > end) {
+				return null;
 			}
 			this.queries.add(query);
 			this.execute(query);
@@ -222,7 +226,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
 			query.getConversation().setFirstMamReference(first == null ? null : first.getContent());
 		}
 		if (complete || relevant == null || abort) {
-			final boolean done = (complete || query.getMessageCount() == 0) && query.getStart() == 0;
+			final boolean done = (complete || query.getMessageCount() == 0) && query.getStart() <= mXmppConnectionService.getAutomaticMessageDeletionDate();
 			this.finalizeQuery(query, done);
 			Log.d(Config.LOGTAG,query.getAccount().getJid().toBareJid()+": finished mam after "+query.getTotalCount()+" messages. messages left="+Boolean.toString(!done));
 			if (query.getWith() == null && query.getMessageCount() > 0) {

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -59,6 +59,7 @@ import java.util.ListIterator;
 import java.util.Locale;
 import java.util.Map;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicLong;
 
 import de.duenndns.ssl.MemorizingTrustManager;
 import eu.siacs.conversations.Config;
@@ -265,6 +266,7 @@ public class XmppConnectionService extends Service {
 	private int mucRosterChangedListenerCount = 0;
 	private OnKeyStatusUpdated mOnKeyStatusUpdated = null;
 	private int keyStatusUpdatedListenerCount = 0;
+	private AtomicLong mLastExpiryRun = new AtomicLong(0);
 	private SecureRandom mRandom;
 	private LruCache<Pair<String,String>,ServiceDiscoveryResult> discoCache = new LruCache<>(20);
 	private final OnBindListener mOnBindListener = new OnBindListener() {
@@ -645,6 +647,9 @@ public class XmppConnectionService extends Service {
 				}
 			}
 		}
+		if (SystemClock.elapsedRealtime() - mLastExpiryRun.get() >= Config.EXPIRY_INTERVAL) {
+			expireOldMessages();
+		}
 		return START_STICKY;
 	}
 
@@ -854,6 +859,20 @@ public class XmppConnectionService extends Service {
 		}
 	}
 
+	private void expireOldMessages() {
+		mLastExpiryRun.set(SystemClock.elapsedRealtime());
+		synchronized (this.conversations) {
+			long timestamp = getAutomaticMessageDeletionDate();
+			if (timestamp > 0) {
+				databaseBackend.expireOldMessages(timestamp);
+				for (Conversation conversation : this.conversations) {
+					conversation.expireOldMessages(timestamp);
+				}
+				updateConversationUi();
+			}
+		}
+	}
+
 	public boolean hasInternetConnection() {
 		ConnectivityManager cm = (ConnectivityManager) getApplicationContext()
 				.getSystemService(Context.CONNECTIVITY_SERVICE);
@@ -1232,12 +1251,10 @@ public class XmppConnectionService extends Service {
 			if (addToConversation) {
 				conversation.add(message);
 			}
-			if (message.getEncryption() == Message.ENCRYPTION_NONE || saveEncryptedMessages()) {
-				if (saveInDb) {
-					databaseBackend.createMessage(message);
-				} else if (message.edited()) {
-					databaseBackend.updateMessage(message, message.getEditedId());
-				}
+			if (saveInDb) {
+				databaseBackend.createMessage(message);
+			} else if (message.edited()) {
+				databaseBackend.updateMessage(message, message.getEditedId());
 			}
 			updateConversationUi();
 		}
@@ -1347,6 +1364,12 @@ public class XmppConnectionService extends Service {
 			Runnable runnable = new Runnable() {
 				@Override
 				public void run() {
+					long deletionDate = getAutomaticMessageDeletionDate();
+					mLastExpiryRun.set(SystemClock.elapsedRealtime());
+					if (deletionDate > 0) {
+						Log.d(Config.LOGTAG, "deleting messages that are older than "+AbstractGenerator.getTimestamp(deletionDate));
+						databaseBackend.expireOldMessages(deletionDate);
+					}
 					Log.d(Config.LOGTAG, "restoring roster");
 					for (Account account : accounts) {
 						databaseBackend.readRoster(account.getRoster());
@@ -1518,8 +1541,11 @@ public class XmppConnectionService extends Service {
 						MessageArchiveService.Query query = getMessageArchiveService().query(conversation, 0, timestamp);
 						if (query != null) {
 							query.setCallback(callback);
+							callback.informUser(R.string.fetching_history_from_server);
+						} else {
+							callback.informUser(R.string.not_fetching_history_retention_period);
 						}
-						callback.informUser(R.string.fetching_history_from_server);
+
 					}
 				}
 			}
@@ -3046,6 +3072,15 @@ public class XmppConnectionService extends Service {
 				.getDefaultSharedPreferences(getApplicationContext());
 	}
 
+	public long getAutomaticMessageDeletionDate() {
+		try {
+			final long timeout = Long.parseLong(getPreferences().getString(SettingsActivity.AUTOMATIC_MESSAGE_DELETION, "0")) * 1000;
+			return timeout == 0 ? timeout : System.currentTimeMillis() - timeout;
+		} catch (NumberFormatException e) {
+			return 0;
+		}
+	}
+
 	public boolean confirmMessages() {
 		return getPreferences().getBoolean("confirm_messages", true);
 	}
@@ -3058,10 +3093,6 @@ public class XmppConnectionService extends Service {
 		return getPreferences().getBoolean("chat_states", false);
 	}
 
-	public boolean saveEncryptedMessages() {
-		return !getPreferences().getBoolean("dont_save_encrypted", false);
-	}
-
 	private boolean respectAutojoin() {
 		return getPreferences().getBoolean("autojoin", true);
 	}

src/main/java/eu/siacs/conversations/ui/SettingsActivity.java 🔗

@@ -44,6 +44,7 @@ public class SettingsActivity extends XmppActivity implements
 	public static final String TREAT_VIBRATE_AS_SILENT = "treat_vibrate_as_silent";
 	public static final String MANUALLY_CHANGE_PRESENCE = "manually_change_presence";
 	public static final String BLIND_TRUST_BEFORE_VERIFICATION = "btbv";
+	public static final String AUTOMATIC_MESSAGE_DELETION = "automatic_message_deletion";
 
 	public static final int REQUEST_WRITE_LOGS = 0xbf8701;
 	private SettingsFragment mSettingsFragment;

src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java 🔗

@@ -52,6 +52,7 @@ import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.entities.Message.FileParams;
 import eu.siacs.conversations.entities.Transferable;
 import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.services.MessageArchiveService;
 import eu.siacs.conversations.services.NotificationService;
 import eu.siacs.conversations.ui.ConversationActivity;
 import eu.siacs.conversations.ui.text.DividerSpan;
@@ -565,8 +566,12 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
 			timestamp = System.currentTimeMillis();
 		}
 		activity.setMessagesLoaded();
-		activity.xmppConnectionService.getMessageArchiveService().query(conversation, 0, timestamp);
-		Toast.makeText(activity, R.string.fetching_history_from_server,Toast.LENGTH_LONG).show();
+		MessageArchiveService.Query query = activity.xmppConnectionService.getMessageArchiveService().query(conversation, 0, timestamp);
+		if (query != null) {
+			Toast.makeText(activity, R.string.fetching_history_from_server, Toast.LENGTH_LONG).show();
+		} else {
+			Toast.makeText(activity,R.string.not_fetching_history_retention_period, Toast.LENGTH_SHORT).show();
+		}
 	}
 
 	@Override

src/main/res/values/arrays.xml 🔗

@@ -103,4 +103,18 @@
 		<item>610</item>
 		<item>2584</item>
 	</string-array>
+	<string-array name="automatic_message_deletion_values">
+		<item>0</item>
+		<item>86400</item>
+		<item>604800</item>
+		<item>2592000</item>
+		<item>15811200</item>
+	</string-array>
+	<string-array name="automatic_message_deletion">
+		<item>@string/never</item>
+		<item>@string/timeout_24_hours</item>
+		<item>@string/timeout_7_days</item>
+		<item>@string/timeout_30_days</item>
+		<item>@string/timeout_6_months</item>
+	</string-array>
 </resources>

src/main/res/values/strings.xml 🔗

@@ -724,5 +724,12 @@
 	<string name="hide_inactive_devices">Hide inactive devices</string>
 	<string name="distrust_omemo_key">Distrust device</string>
 	<string name="distrust_omemo_key_text">Are you sure you want to remove the verification for this device?\nThis device and messages coming from that device will be marked as untrusted.</string>
+	<string name="timeout_24_hours">24 hours</string>
+	<string name="timeout_7_days">7 days</string>
+	<string name="timeout_30_days">30 days</string>
+	<string name="timeout_6_months">6 months</string>
+	<string name="pref_automatically_delete_messages">Automatic message deletion</string>
+	<string name="pref_automatically_delete_messages_description">Automatically delete messages from this device that are older than the configured time frame.</string>
 	<string name="encrypting_message">Encrypting message</string>
+	<string name="not_fetching_history_retention_period">Overstepping local retention period.</string>
 </resources>

src/main/res/xml/preferences.xml 🔗

@@ -170,11 +170,13 @@
                     android:key="btbv"
                     android:title="@string/pref_blind_trust_before_verification"
                     android:summary="@string/pref_blind_trust_before_verification_summary"/>
-                <CheckBoxPreference
-                    android:defaultValue="false"
-                    android:key="dont_save_encrypted"
-                    android:summary="@string/pref_dont_save_encrypted_summary"
-                    android:title="@string/pref_dont_save_encrypted"/>
+                <ListPreference
+                    android:key="automatic_message_deletion"
+                    android:title="@string/pref_automatically_delete_messages"
+                    android:summary="@string/pref_automatically_delete_messages_description"
+                    android:defaultValue="0"
+                    android:entries="@array/automatic_message_deletion"
+                    android:entryValues="@array/automatic_message_deletion_values" />
                 <CheckBoxPreference
                     android:defaultValue="false"
                     android:key="dont_trust_system_cas"