From a02f223a96eb3dc947f6305b26b927ebc46f44ed Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Sun, 6 Aug 2017 21:43:11 -0700 Subject: [PATCH] Join recipient preferences into thread query for faster lookup // FREEBIE --- build.gradle | 10 ++- proguard-retrolambda.pro | 2 + .../securesms/DatabaseMigrationActivity.java | 3 +- .../database/RecipientPreferenceDatabase.java | 66 +++++++++------- .../securesms/database/ThreadDatabase.java | 78 +++++++++++++------ .../recipients/RecipientFactory.java | 10 ++- .../recipients/RecipientProvider.java | 22 +++--- 7 files changed, 126 insertions(+), 65 deletions(-) create mode 100644 proguard-retrolambda.pro diff --git a/build.gradle b/build.gradle index 5dd451b52..8057ac437 100644 --- a/build.gradle +++ b/build.gradle @@ -10,11 +10,13 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:2.3.0' classpath files('libs/gradle-witness.jar') + classpath 'me.tatarka:gradle-retrolambda:3.7.0' } } apply plugin: 'com.android.application' apply plugin: 'witness' +apply plugin: 'me.tatarka.retrolambda' repositories { maven { @@ -99,6 +101,8 @@ dependencies { exclude group: 'com.squareup.okhttp', module: 'okhttp' exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection' } + compile 'com.annimon:stream:1.1.8' + testCompile 'junit:junit:4.12' testCompile 'org.assertj:assertj-core:1.7.1' @@ -160,6 +164,7 @@ dependencyVerification { 'cn.carbswang.android:NumberPickerView:18b3c316d62c7c277978a8d4ed57a5b8f4e943762264960f579a8a549c756729', 'com.tomergoldst.android:tooltips:4c56697dd1ad64b8066535c61f961a6d901e7ae5d97ae27084ba40ad620349b6', 'com.klinkerapps:android-smsmms:e7c3328a0f3a8dd44daa8129de4e99996f3057a4546e47891b036b81e0ebf1d1', + 'com.annimon:stream:5da6e2e3e0551d61a3ea7014f04312276549e3dd739cf637996e4cf43c5535b9', 'com.android.support:support-v4:ed4cda7c752f51d33f9bbdfff3422b425b323d356cd1bdc9786aa413c912e594', 'com.android.support:support-vector-drawable:2697503d3e8e709023ae176ba5db7f98ca0aa0b4e6290aedcb3c371904806bf7', 'com.android.support:animated-vector-drawable:6d05cb63d1f68900220f85c56dfe1066a9bb19cb0ec1247cc68fc2ba32f6b4a7', @@ -222,8 +227,8 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_7 - targetCompatibility JavaVersion.VERSION_1_7 + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } packagingOptions { @@ -257,6 +262,7 @@ android { 'proguard-retrofit.pro', 'proguard-webrtc.pro', 'proguard-klinker.pro', + 'proguard-retrolambda.pro', 'proguard.cfg' testProguardFiles 'proguard-automation.pro', 'proguard.cfg' diff --git a/proguard-retrolambda.pro b/proguard-retrolambda.pro new file mode 100644 index 000000000..82e895098 --- /dev/null +++ b/proguard-retrolambda.pro @@ -0,0 +1,2 @@ +-dontwarn java.lang.invoke.* +-dontwarn **$$Lambda$* diff --git a/src/org/thoughtcrime/securesms/DatabaseMigrationActivity.java b/src/org/thoughtcrime/securesms/DatabaseMigrationActivity.java index f53f50064..09d8c3e64 100644 --- a/src/org/thoughtcrime/securesms/DatabaseMigrationActivity.java +++ b/src/org/thoughtcrime/securesms/DatabaseMigrationActivity.java @@ -10,6 +10,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; +import android.os.Parcelable; import android.support.annotation.NonNull; import android.view.View; import android.widget.Button; @@ -92,7 +93,7 @@ public class DatabaseMigrationActivity extends PassphraseRequiredActionBarActivi public void onClick(View v) { Intent intent = new Intent(DatabaseMigrationActivity.this, ApplicationMigrationService.class); intent.setAction(ApplicationMigrationService.MIGRATE_DATABASE); - intent.putExtra("master_secret", getIntent().getParcelableExtra("master_secret")); + intent.putExtra("master_secret", (Parcelable)getIntent().getParcelableExtra("master_secret")); startService(intent); promptLayout.setVisibility(View.GONE); diff --git a/src/org/thoughtcrime/securesms/database/RecipientPreferenceDatabase.java b/src/org/thoughtcrime/securesms/database/RecipientPreferenceDatabase.java index 558e5e8d1..77f33bf15 100644 --- a/src/org/thoughtcrime/securesms/database/RecipientPreferenceDatabase.java +++ b/src/org/thoughtcrime/securesms/database/RecipientPreferenceDatabase.java @@ -10,18 +10,22 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; +import com.annimon.stream.Stream; + import org.greenrobot.eventbus.EventBus; import org.thoughtcrime.securesms.color.MaterialColor; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.whispersystems.libsignal.util.guava.Optional; +import java.util.List; + public class RecipientPreferenceDatabase extends Database { private static final String TAG = RecipientPreferenceDatabase.class.getSimpleName(); private static final String RECIPIENT_PREFERENCES_URI = "content://textsecure/recipients/"; - private static final String TABLE_NAME = "recipient_preferences"; + static final String TABLE_NAME = "recipient_preferences"; private static final String ID = "_id"; private static final String ADDRESS = "recipient_ids"; private static final String BLOCK = "block"; @@ -33,6 +37,14 @@ public class RecipientPreferenceDatabase extends Database { private static final String DEFAULT_SUBSCRIPTION_ID = "default_subscription_id"; private static final String EXPIRE_MESSAGES = "expire_messages"; + private static final String[] RECIPIENT_PROJECTION = new String[] { + BLOCK, NOTIFICATION, VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, EXPIRE_MESSAGES + }; + + static final List TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION) + .map(columnName -> TABLE_NAME + "." + columnName) + .toList(); + public enum VibrateState { DEFAULT(0), ENABLED(1), DISABLED(2); @@ -91,31 +103,7 @@ public class RecipientPreferenceDatabase extends Database { cursor = database.query(TABLE_NAME, null, ADDRESS + " = ?", new String[] {address.serialize()}, null, null, null); if (cursor != null && cursor.moveToNext()) { - boolean blocked = cursor.getInt(cursor.getColumnIndexOrThrow(BLOCK)) == 1; - String notification = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION)); - int vibrateState = cursor.getInt(cursor.getColumnIndexOrThrow(VIBRATE)); - long muteUntil = cursor.getLong(cursor.getColumnIndexOrThrow(MUTE_UNTIL)); - String serializedColor = cursor.getString(cursor.getColumnIndexOrThrow(COLOR)); - Uri notificationUri = notification == null ? null : Uri.parse(notification); - boolean seenInviteReminder = cursor.getInt(cursor.getColumnIndexOrThrow(SEEN_INVITE_REMINDER)) == 1; - int defaultSubscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(DEFAULT_SUBSCRIPTION_ID)); - int expireMessages = cursor.getInt(cursor.getColumnIndexOrThrow(EXPIRE_MESSAGES)); - - MaterialColor color; - - try { - color = serializedColor == null ? null : MaterialColor.fromSerialized(serializedColor); - } catch (MaterialColor.UnknownColorException e) { - Log.w(TAG, e); - color = null; - } - - Log.w(TAG, "Muted until: " + muteUntil); - - return Optional.of(new RecipientsPreferences(blocked, muteUntil, - VibrateState.fromId(vibrateState), - notificationUri, color, seenInviteReminder, - defaultSubscriptionId, expireMessages)); + return Optional.of(getRecipientPreferences(cursor)); } return Optional.absent(); @@ -124,6 +112,32 @@ public class RecipientPreferenceDatabase extends Database { } } + RecipientsPreferences getRecipientPreferences(@NonNull Cursor cursor) { + boolean blocked = cursor.getInt(cursor.getColumnIndexOrThrow(BLOCK)) == 1; + String notification = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION)); + int vibrateState = cursor.getInt(cursor.getColumnIndexOrThrow(VIBRATE)); + long muteUntil = cursor.getLong(cursor.getColumnIndexOrThrow(MUTE_UNTIL)); + String serializedColor = cursor.getString(cursor.getColumnIndexOrThrow(COLOR)); + Uri notificationUri = notification == null ? null : Uri.parse(notification); + boolean seenInviteReminder = cursor.getInt(cursor.getColumnIndexOrThrow(SEEN_INVITE_REMINDER)) == 1; + int defaultSubscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(DEFAULT_SUBSCRIPTION_ID)); + int expireMessages = cursor.getInt(cursor.getColumnIndexOrThrow(EXPIRE_MESSAGES)); + + MaterialColor color; + + try { + color = serializedColor == null ? null : MaterialColor.fromSerialized(serializedColor); + } catch (MaterialColor.UnknownColorException e) { + Log.w(TAG, e); + color = null; + } + + return new RecipientsPreferences(blocked, muteUntil, + VibrateState.fromId(vibrateState), + notificationUri, color, seenInviteReminder, + defaultSubscriptionId, expireMessages); + } + public void setColor(Recipient recipient, MaterialColor color) { ContentValues values = new ContentValues(); values.put(COLOR, color.serialize()); diff --git a/src/org/thoughtcrime/securesms/database/ThreadDatabase.java b/src/org/thoughtcrime/securesms/database/ThreadDatabase.java index 5758e5004..6d3cca06f 100644 --- a/src/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/src/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -27,9 +27,12 @@ import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; +import com.annimon.stream.Stream; + import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.crypto.MasterCipher; import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo; +import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.RecipientsPreferences; import org.thoughtcrime.securesms.database.model.DisplayRecord; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord; @@ -83,6 +86,19 @@ public class ThreadDatabase extends Database { "CREATE INDEX IF NOT EXISTS archived_count_index ON " + TABLE_NAME + " (" + ARCHIVED + ", " + MESSAGE_COUNT + ");", }; + private static final String[] THREAD_PROJECTION = { + ID, DATE, MESSAGE_COUNT, ADDRESS, SNIPPET, SNIPPET_CHARSET, READ, TYPE, ERROR, SNIPPET_TYPE, + SNIPPET_URI, ARCHIVED, STATUS, RECEIPT_COUNT, EXPIRES_IN, LAST_SEEN + }; + + private static final List TYPED_THREAD_PROJECTION = Stream.of(THREAD_PROJECTION) + .map(columnName -> TABLE_NAME + "." + columnName) + .toList(); + + private static final List COMBINED_THREAD_RECIPIENT_PROJECTION = Stream.concat(Stream.of(TYPED_THREAD_PROJECTION), + Stream.of(RecipientPreferenceDatabase.TYPED_RECIPIENT_PROJECTION)) + .toList(); + public ThreadDatabase(Context context, SQLiteOpenHelper databaseHelper) { super(context, databaseHelper); } @@ -316,17 +332,22 @@ public class ThreadDatabase extends Database { } public Cursor getConversationList() { - SQLiteDatabase db = databaseHelper.getReadableDatabase(); - Cursor cursor = db.query(TABLE_NAME, null, ARCHIVED + " = ? AND " + MESSAGE_COUNT + " != 0", new String[] {"0"}, null, null, DATE + " DESC"); - - setNotifyConverationListListeners(cursor); - - return cursor; + return getConversationList("0"); } public Cursor getArchivedConversationList() { - SQLiteDatabase db = databaseHelper.getReadableDatabase(); - Cursor cursor = db.query(TABLE_NAME, null, ARCHIVED + " = ? AND " + MESSAGE_COUNT + " != 0", new String[] {"1"}, null, null, DATE + " DESC"); + return getConversationList("1"); + } + + private Cursor getConversationList(String archived) { + String projection = Util.join(COMBINED_THREAD_RECIPIENT_PROJECTION, ","); + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + Cursor cursor = db.rawQuery("SELECT " + projection + " FROM " + TABLE_NAME + + " LEFT OUTER JOIN " + RecipientPreferenceDatabase.TABLE_NAME + + " ON " + TABLE_NAME + "." + ADDRESS + " = " + RecipientPreferenceDatabase.TABLE_NAME + "." + ADDRESS + + " WHERE " + ARCHIVED + " = ? AND " + MESSAGE_COUNT + " != 0" + + " ORDER BY " + TABLE_NAME + "." + DATE + " DESC", + new String[] {archived}); setNotifyConverationListListeners(cursor); @@ -334,8 +355,14 @@ public class ThreadDatabase extends Database { } public Cursor getDirectShareList() { - SQLiteDatabase db = databaseHelper.getReadableDatabase(); - return db.query(TABLE_NAME, null, null, null, null, null, DATE + " DESC"); + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + String projection = Util.join(COMBINED_THREAD_RECIPIENT_PROJECTION, ","); + + return db.rawQuery("SELECT " + projection + " FROM " + TABLE_NAME + + " LEFT OUTER JOIN " + RecipientPreferenceDatabase.TABLE_NAME + + " ON " + TABLE_NAME + "." + ADDRESS + " = " + RecipientPreferenceDatabase.TABLE_NAME + "." + ADDRESS + + " ORDER BY " + TABLE_NAME + "." + DATE + " DESC", + null); } public int getArchivedConversationListCount() { @@ -574,22 +601,23 @@ public class ThreadDatabase extends Database { } public ThreadRecord getCurrent() { - long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.ID)); - Address address = Address.fromSerialized(cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.ADDRESS))); - Recipient recipient = RecipientFactory.getRecipientFor(context, address, true); + long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.ID)); + Address address = Address.fromSerialized(cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.ADDRESS))); + RecipientsPreferences preferences = DatabaseFactory.getRecipientPreferenceDatabase(context).getRecipientPreferences(cursor); + Recipient recipient = RecipientFactory.getRecipientFor(context, address, preferences, true); - DisplayRecord.Body body = getPlaintextBody(cursor); - long date = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.DATE)); - long count = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.MESSAGE_COUNT)); - long read = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.READ)); - long type = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET_TYPE)); - int distributionType = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.TYPE)); - boolean archived = cursor.getInt(cursor.getColumnIndex(ThreadDatabase.ARCHIVED)) != 0; - int status = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.STATUS)); - int receiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.RECEIPT_COUNT)); - long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.EXPIRES_IN)); - long lastSeen = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.LAST_SEEN)); - Uri snippetUri = getSnippetUri(cursor); + DisplayRecord.Body body = getPlaintextBody(cursor); + long date = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.DATE)); + long count = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.MESSAGE_COUNT)); + long read = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.READ)); + long type = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET_TYPE)); + int distributionType = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.TYPE)); + boolean archived = cursor.getInt(cursor.getColumnIndex(ThreadDatabase.ARCHIVED)) != 0; + int status = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.STATUS)); + int receiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.RECEIPT_COUNT)); + long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.EXPIRES_IN)); + long lastSeen = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.LAST_SEEN)); + Uri snippetUri = getSnippetUri(cursor); return new ThreadRecord(context, body, snippetUri, recipient, date, count, read == 1, threadId, receiptCount, status, type, distributionType, archived, diff --git a/src/org/thoughtcrime/securesms/recipients/RecipientFactory.java b/src/org/thoughtcrime/securesms/recipients/RecipientFactory.java index f7da7c171..f1eb2e0fd 100644 --- a/src/org/thoughtcrime/securesms/recipients/RecipientFactory.java +++ b/src/org/thoughtcrime/securesms/recipients/RecipientFactory.java @@ -21,6 +21,9 @@ import android.content.Intent; import android.support.annotation.NonNull; import org.thoughtcrime.securesms.database.Address; +import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase; +import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.RecipientsPreferences; +import org.whispersystems.libsignal.util.guava.Optional; public class RecipientFactory { @@ -30,7 +33,12 @@ public class RecipientFactory { public static @NonNull Recipient getRecipientFor(@NonNull Context context, @NonNull Address address, boolean asynchronous) { if (address == null) throw new AssertionError(address); - return provider.getRecipient(context, address, asynchronous); + return provider.getRecipient(context, address, Optional.absent(), asynchronous); + } + + public static @NonNull Recipient getRecipientFor(@NonNull Context context, @NonNull Address address, @NonNull RecipientsPreferences preferences, boolean asynchronous) { + if (address == null) throw new AssertionError(address); + return provider.getRecipient(context, address, Optional.of(preferences), asynchronous); } public static void clearCache(Context context) { diff --git a/src/org/thoughtcrime/securesms/recipients/RecipientProvider.java b/src/org/thoughtcrime/securesms/recipients/RecipientProvider.java index f8b3209f0..448b17f27 100644 --- a/src/org/thoughtcrime/securesms/recipients/RecipientProvider.java +++ b/src/org/thoughtcrime/securesms/recipients/RecipientProvider.java @@ -68,16 +68,16 @@ class RecipientProvider { null, null)); }}; - @NonNull Recipient getRecipient(Context context, Address address, boolean asynchronous) { + @NonNull Recipient getRecipient(Context context, Address address, Optional preferences, boolean asynchronous) { Recipient cachedRecipient = recipientCache.get(address); if (cachedRecipient != null && !cachedRecipient.isStale() && (asynchronous || !cachedRecipient.isResolving())) { return cachedRecipient; } if (asynchronous) { - cachedRecipient = new Recipient(address, cachedRecipient, getRecipientDetailsAsync(context, address)); + cachedRecipient = new Recipient(address, cachedRecipient, getRecipientDetailsAsync(context, address, preferences)); } else { - cachedRecipient = new Recipient(address, getRecipientDetailsSync(context, address, false)); + cachedRecipient = new Recipient(address, getRecipientDetailsSync(context, address, preferences, false)); } recipientCache.set(address, cachedRecipient); @@ -88,12 +88,12 @@ class RecipientProvider { recipientCache.reset(); } - private @NonNull ListenableFutureTask getRecipientDetailsAsync(final Context context, final @NonNull Address address) + private @NonNull ListenableFutureTask getRecipientDetailsAsync(final Context context, final @NonNull Address address, final @NonNull Optional preferences) { Callable task = new Callable() { @Override public RecipientDetails call() throws Exception { - return getRecipientDetailsSync(context, address, true); + return getRecipientDetailsSync(context, address, preferences, true); } }; @@ -102,13 +102,15 @@ class RecipientProvider { return future; } - private @NonNull RecipientDetails getRecipientDetailsSync(Context context, @NonNull Address address, boolean nestedAsynchronous) { + private @NonNull RecipientDetails getRecipientDetailsSync(Context context, @NonNull Address address, Optional preferences, boolean nestedAsynchronous) { if (address.isGroup()) return getGroupRecipientDetails(context, address, nestedAsynchronous); - else return getIndividualRecipientDetails(context, address); + else return getIndividualRecipientDetails(context, address, preferences); } - private @NonNull RecipientDetails getIndividualRecipientDetails(Context context, @NonNull Address address) { - Optional preferences = DatabaseFactory.getRecipientPreferenceDatabase(context).getRecipientsPreferences(address); + private @NonNull RecipientDetails getIndividualRecipientDetails(Context context, @NonNull Address address, Optional preferences) { + if (!preferences.isPresent()) { + preferences = DatabaseFactory.getRecipientPreferenceDatabase(context).getRecipientsPreferences(address); + } if (address.isPhone() && !TextUtils.isEmpty(address.toPhoneString())) { Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(address.toPhoneString())); @@ -149,7 +151,7 @@ class RecipientProvider { List members = new LinkedList<>(); for (Address memberAddress : memberAddresses) { - members.add(getRecipient(context, memberAddress, asynchronous)); + members.add(getRecipient(context, memberAddress, Optional.absent(), asynchronous)); } if (!groupId.isMmsGroup() && title == null) {