Join recipient preferences into thread query for faster lookup

// FREEBIE
This commit is contained in:
Moxie Marlinspike 2017-08-06 21:43:11 -07:00
parent 375207f073
commit a02f223a96
7 changed files with 126 additions and 65 deletions

View File

@ -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'

2
proguard-retrolambda.pro Normal file
View File

@ -0,0 +1,2 @@
-dontwarn java.lang.invoke.*
-dontwarn **$$Lambda$*

View File

@ -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);

View File

@ -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<String> 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());

View File

@ -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<String> TYPED_THREAD_PROJECTION = Stream.of(THREAD_PROJECTION)
.map(columnName -> TABLE_NAME + "." + columnName)
.toList();
private static final List<String> 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,

View File

@ -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) {

View File

@ -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<RecipientsPreferences> 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<RecipientDetails> getRecipientDetailsAsync(final Context context, final @NonNull Address address)
private @NonNull ListenableFutureTask<RecipientDetails> getRecipientDetailsAsync(final Context context, final @NonNull Address address, final @NonNull Optional<RecipientsPreferences> preferences)
{
Callable<RecipientDetails> task = new Callable<RecipientDetails>() {
@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<RecipientsPreferences> 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<RecipientsPreferences> preferences = DatabaseFactory.getRecipientPreferenceDatabase(context).getRecipientsPreferences(address);
private @NonNull RecipientDetails getIndividualRecipientDetails(Context context, @NonNull Address address, Optional<RecipientsPreferences> 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<Recipient> 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) {