From a2dc781840c175913cb6a722699682b7e1830883 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Sat, 16 Jan 2021 14:17:32 -0500 Subject: [PATCH] Add an automatic session reset interval. --- .../securesms/database/RecipientDatabase.java | 30 ++++++++++- .../database/helpers/SQLCipherOpenHelper.java | 7 ++- .../jobs/AutomaticSessionResetJob.java | 53 +++++++++++++++++-- .../securesms/util/FeatureFlags.java | 10 +++- app/src/main/proto/Database.proto | 9 ++++ 5 files changed, 101 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java index 07878be888..4b1688db68 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -13,6 +13,7 @@ import com.annimon.stream.Stream; import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; +import net.sqlcipher.SQLException; import net.sqlcipher.database.SQLiteConstraintException; import org.signal.core.util.logging.Log; @@ -28,6 +29,7 @@ import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord; import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.model.ThreadRecord; +import org.thoughtcrime.securesms.database.model.databaseprotos.DeviceLastResetTime; import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileKeyCredentialColumnData; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupId; @@ -128,6 +130,7 @@ public class RecipientDatabase extends Database { private static final String MENTION_SETTING = "mention_setting"; private static final String STORAGE_PROTO = "storage_proto"; private static final String LAST_GV1_MIGRATE_REMINDER = "last_gv1_migrate_reminder"; + private static final String LAST_SESSION_RESET = "last_session_reset"; public static final String SEARCH_PROFILE_NAME = "search_signal_profile"; private static final String SORT_NAME = "sort_name"; @@ -346,7 +349,8 @@ public class RecipientDatabase extends Database { MENTION_SETTING + " INTEGER DEFAULT " + MentionSetting.ALWAYS_NOTIFY.getId() + ", " + STORAGE_PROTO + " TEXT DEFAULT NULL, " + CAPABILITIES + " INTEGER DEFAULT 0, " + - LAST_GV1_MIGRATE_REMINDER + " INTEGER DEFAULT 0);"; + LAST_GV1_MIGRATE_REMINDER + " INTEGER DEFAULT 0, " + + LAST_SESSION_RESET + " BLOB DEFAULT NULL);"; private static final String INSIGHTS_INVITEE_LIST = "SELECT " + TABLE_NAME + "." + ID + " FROM " + TABLE_NAME + @@ -1512,6 +1516,30 @@ public class RecipientDatabase extends Database { return 0; } + + public void setLastSessionResetTime(@NonNull RecipientId id, DeviceLastResetTime lastResetTime) { + ContentValues values = new ContentValues(1); + values.put(LAST_SESSION_RESET, lastResetTime.toByteArray()); + update(id, values); + } + + public @NonNull DeviceLastResetTime getLastSessionResetTimes(@NonNull RecipientId id) { + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + + try (Cursor cursor = db.query(TABLE_NAME, new String[] {LAST_SESSION_RESET}, ID_WHERE, SqlUtil.buildArgs(id), null, null, null)) { + if (cursor.moveToFirst()) { + try { + return DeviceLastResetTime.parseFrom(CursorUtil.requireBlob(cursor, LAST_SESSION_RESET)); + } catch (InvalidProtocolBufferException | SQLException e) { + Log.w(TAG, e); + return DeviceLastResetTime.newBuilder().build(); + } + } + } + + return DeviceLastResetTime.newBuilder().build(); + } + public void setCapabilities(@NonNull RecipientId id, @NonNull SignalServiceProfile.Capabilities capabilities) { long value = 0; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 367b716c16..eed3e27b79 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -168,8 +168,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab private static final int CLEAN_UP_GV1_IDS = 84; private static final int GV1_MIGRATION_REFACTOR = 85; private static final int CLEAR_PROFILE_KEY_CREDENTIALS = 86; + private static final int LAST_RESET_SESSION_TIME = 87; - private static final int DATABASE_VERSION = 86; + private static final int DATABASE_VERSION = 87; private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -1241,6 +1242,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab Log.i(TAG, "Cleared profile key credentials for " + count + " rows"); } + if (oldVersion < LAST_RESET_SESSION_TIME) { + db.execSQL("ALTER TABLE recipient ADD COLUMN last_session_reset BLOB DEFAULT NULL"); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AutomaticSessionResetJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AutomaticSessionResetJob.java index 651e87978b..1bb4eb94a5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AutomaticSessionResetJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AutomaticSessionResetJob.java @@ -7,6 +7,8 @@ import org.thoughtcrime.securesms.crypto.SessionUtil; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MessageDatabase; +import org.thoughtcrime.securesms.database.RecipientDatabase; +import org.thoughtcrime.securesms.database.model.databaseprotos.DeviceLastResetTime; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; @@ -23,6 +25,7 @@ import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.io.IOException; +import java.util.concurrent.TimeUnit; /** * - Archives the session associated with the specified device @@ -83,18 +86,35 @@ public class AutomaticSessionResetJob extends BaseJob { @Override protected void onRun() throws Exception { + SessionUtil.archiveSession(context, recipientId, deviceId); + insertLocalMessage(); + if (FeatureFlags.automaticSessionReset()) { - SessionUtil.archiveSession(context, recipientId, deviceId); - insertLocalMessage(); - sendNullMessage(); + long resetInterval = TimeUnit.SECONDS.toMillis(FeatureFlags.automaticSessionResetIntervalSeconds()); + DeviceLastResetTime resetTimes = DatabaseFactory.getRecipientDatabase(context).getLastSessionResetTimes(recipientId); + long timeSinceLastReset = System.currentTimeMillis() - getLastResetTime(resetTimes, deviceId); + + Log.i(TAG, "DeviceId: " + deviceId + ", Reset interval: " + resetInterval + ", Time since last reset: " + timeSinceLastReset); + + if (timeSinceLastReset > resetInterval) { + Log.i(TAG, "We're good! Sending a null message."); + + DatabaseFactory.getRecipientDatabase(context).setLastSessionResetTime(recipientId, setLastResetTime(resetTimes, deviceId, System.currentTimeMillis())); + Log.i(TAG, "Marked last reset time: " + System.currentTimeMillis()); + + sendNullMessage(); + Log.i(TAG, "Successfully sent!"); + } else { + Log.w(TAG, "Too soon! Time since last reset: " + timeSinceLastReset); + } } else { - insertLocalMessage(); + Log.w(TAG, "Automatic session reset send disabled!"); } } @Override protected boolean onShouldRetry(@NonNull Exception e) { - return e instanceof RetryLaterException; + return false; } @Override @@ -119,6 +139,29 @@ public class AutomaticSessionResetJob extends BaseJob { } } + private long getLastResetTime(@NonNull DeviceLastResetTime resetTimes, int deviceId) { + for (DeviceLastResetTime.Pair pair : resetTimes.getResetTimeList()) { + if (pair.getDeviceId() == deviceId) { + return pair.getLastResetTime(); + } + } + return 0; + } + + private @NonNull DeviceLastResetTime setLastResetTime(@NonNull DeviceLastResetTime resetTimes, int deviceId, long time) { + DeviceLastResetTime.Builder builder = DeviceLastResetTime.newBuilder(); + + for (DeviceLastResetTime.Pair pair : resetTimes.getResetTimeList()) { + if (pair.getDeviceId() != deviceId) { + builder.addResetTime(pair); + } + } + + builder.addResetTime(DeviceLastResetTime.Pair.newBuilder().setDeviceId(deviceId).setLastResetTime(time)); + + return builder.build(); + } + public static final class Factory implements Job.Factory { @Override public @NonNull AutomaticSessionResetJob create(@NonNull Parameters parameters, @NonNull Data data) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index cad69574fb..030171e721 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -67,7 +67,8 @@ public final class FeatureFlags { private static final String SEND_VIEWED_RECEIPTS = "android.sendViewedReceipts"; private static final String CUSTOM_VIDEO_MUXER = "android.customVideoMuxer"; private static final String CDS_REFRESH_INTERVAL = "cds.syncInterval.seconds"; - private static final String AUTOMATIC_SESSION_RESET = "android.automaticSessionReset"; + private static final String AUTOMATIC_SESSION_RESET = "android.automaticSessionReset.2"; + private static final String AUTOMATIC_SESSION_INTERVAL = "android.automaticSessionResetInterval"; private static final String DEFAULT_MAX_BACKOFF = "android.defaultMaxBackoff"; /** @@ -94,6 +95,7 @@ public final class FeatureFlags { CDS_REFRESH_INTERVAL, GROUP_NAME_MAX_LENGTH, AUTOMATIC_SESSION_RESET, + AUTOMATIC_SESSION_INTERVAL, DEFAULT_MAX_BACKOFF ); @@ -130,6 +132,7 @@ public final class FeatureFlags { CDS_REFRESH_INTERVAL, GROUP_NAME_MAX_LENGTH, AUTOMATIC_SESSION_RESET, + AUTOMATIC_SESSION_INTERVAL, DEFAULT_MAX_BACKOFF ); @@ -296,6 +299,11 @@ public final class FeatureFlags { return getBoolean(AUTOMATIC_SESSION_RESET, true); } + /** How often we allow an automatic session reset. */ + public static int automaticSessionResetIntervalSeconds() { + return getInteger(AUTOMATIC_SESSION_RESET, (int) TimeUnit.HOURS.toSeconds(1)); + } + public static int getDefaultMaxBackoffSeconds() { return getInteger(DEFAULT_MAX_BACKOFF, 60); } diff --git a/app/src/main/proto/Database.proto b/app/src/main/proto/Database.proto index 569c6b5ab7..691c4d40cf 100644 --- a/app/src/main/proto/Database.proto +++ b/app/src/main/proto/Database.proto @@ -82,3 +82,12 @@ message ProfileKeyCredentialColumnData { bytes profileKey = 1; bytes profileKeyCredential = 2; } + +message DeviceLastResetTime { + message Pair { + int32 deviceId = 1; + int64 lastResetTime = 2; + } + + repeated Pair resetTime = 1; +} \ No newline at end of file