Optimistically fetch profiles.

master
Greyson Parrelli 2020-06-12 10:22:46 -07:00 committed by Greyson Parrelli
parent f5626f678d
commit ce940235b0
11 changed files with 169 additions and 29 deletions

View File

@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.logging.AndroidLogger;
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
import org.thoughtcrime.securesms.logging.Log;
@ -133,6 +134,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
NotificationChannels.create(this);
RefreshPreKeysJob.scheduleIfNecessary();
StorageSyncHelper.scheduleRoutineSync();
RetrieveProfileJob.enqueueRoutineFetchIfNeccessary(this);
RegistrationUtil.markRegistrationPossiblyComplete();
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);

View File

@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.v2.ProfileKeySet;
@ -60,6 +61,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
@ -102,6 +104,7 @@ public class RecipientDatabase extends Database {
private static final String PROFILE_KEY_CREDENTIAL = "profile_key_credential";
private static final String SIGNAL_PROFILE_AVATAR = "signal_profile_avatar";
private static final String PROFILE_SHARING = "profile_sharing";
private static final String LAST_PROFILE_FETCH = "last_profile_fetch";
private static final String UNIDENTIFIED_ACCESS_MODE = "unidentified_access_mode";
private static final String FORCE_SMS_SELECTION = "force_sms_selection";
private static final String UUID_CAPABILITY = "uuid_supported";
@ -123,7 +126,8 @@ public class RecipientDatabase extends Database {
BLOCKED, MESSAGE_RINGTONE, CALL_RINGTONE, MESSAGE_VIBRATE, CALL_VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, MESSAGE_EXPIRATION_TIME, REGISTERED,
PROFILE_KEY, PROFILE_KEY_CREDENTIAL,
SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, SYSTEM_CONTACT_URI,
PROFILE_GIVEN_NAME, PROFILE_FAMILY_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, NOTIFICATION_CHANNEL,
PROFILE_GIVEN_NAME, PROFILE_FAMILY_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, LAST_PROFILE_FETCH,
NOTIFICATION_CHANNEL,
UNIDENTIFIED_ACCESS_MODE,
FORCE_SMS_SELECTION,
UUID_CAPABILITY, GROUPS_V2_CAPABILITY,
@ -295,6 +299,7 @@ public class RecipientDatabase extends Database {
PROFILE_JOINED_NAME + " TEXT DEFAULT NULL, " +
SIGNAL_PROFILE_AVATAR + " TEXT DEFAULT NULL, " +
PROFILE_SHARING + " INTEGER DEFAULT 0, " +
LAST_PROFILE_FETCH + " INTEGER DEFAULT 0, " +
UNIDENTIFIED_ACCESS_MODE + " INTEGER DEFAULT 0, " +
FORCE_SMS_SELECTION + " INTEGER DEFAULT 0, " +
UUID_CAPABILITY + " INTEGER DEFAULT " + Recipient.Capability.UNKNOWN.serialize() + ", " +
@ -842,6 +847,7 @@ public class RecipientDatabase extends Database {
String profileFamilyName = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_FAMILY_NAME));
String signalProfileAvatar = cursor.getString(cursor.getColumnIndexOrThrow(SIGNAL_PROFILE_AVATAR));
boolean profileSharing = cursor.getInt(cursor.getColumnIndexOrThrow(PROFILE_SHARING)) == 1;
long lastProfileFetch = cursor.getLong(cursor.getColumnIndexOrThrow(LAST_PROFILE_FETCH));
String notificationChannel = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION_CHANNEL));
int unidentifiedAccessMode = cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED_ACCESS_MODE));
boolean forceSmsSelection = cursor.getInt(cursor.getColumnIndexOrThrow(FORCE_SMS_SELECTION)) == 1;
@ -908,7 +914,7 @@ public class RecipientDatabase extends Database {
systemDisplayName, systemContactPhoto,
systemPhoneLabel, systemContactUri,
ProfileName.fromParts(profileGivenName, profileFamilyName), signalProfileAvatar,
AvatarHelper.hasAvatar(context, RecipientId.from(id)), profileSharing,
AvatarHelper.hasAvatar(context, RecipientId.from(id)), profileSharing, lastProfileFetch,
notificationChannel, UnidentifiedAccessMode.fromMode(unidentifiedAccessMode),
forceSmsSelection,
Recipient.Capability.deserialize(uuidCapabilityValue),
@ -1587,6 +1593,53 @@ public class RecipientDatabase extends Database {
return recipients;
}
/**
* @param lastInteractionThreshold Only include contacts that have been interacted with since this time.
* @param lastProfileFetchThreshold Only include contacts that haven't their profile fetched after this time.
* @param limit Only return at most this many contact.
*/
public List<RecipientId> getRecipientsForRoutineProfileFetch(long lastInteractionThreshold, long lastProfileFetchThreshold, int limit) {
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
Set<Recipient> recipientsWithinInteractionThreshold = new LinkedHashSet<>();
try (ThreadDatabase.Reader reader = threadDatabase.readerFor(threadDatabase.getRecentPushConversationList(-1, false))) {
ThreadRecord record;
while ((record = reader.getNext()) != null && record.getDate() > lastInteractionThreshold) {
Recipient recipient = Recipient.resolved(record.getRecipient().getId());
if (recipient.isGroup()) {
recipientsWithinInteractionThreshold.addAll(recipient.getParticipants());
} else {
recipientsWithinInteractionThreshold.add(recipient);
}
}
}
return Stream.of(recipientsWithinInteractionThreshold)
.filterNot(Recipient::isLocalNumber)
.filter(r -> r.getLastProfileFetchTime() < lastProfileFetchThreshold)
.limit(limit)
.map(Recipient::getId)
.toList();
}
public void markProfilesFetched(@NonNull Collection<RecipientId> ids, long time) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.beginTransaction();
try {
ContentValues values = new ContentValues(1);
values.put(LAST_PROFILE_FETCH, time);
for (RecipientId id : ids) {
db.update(TABLE_NAME, values, ID_WHERE, new String[] { id.serialize() });
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
public void applyBlockedUpdate(@NonNull List<SignalServiceAddress> blocked, List<byte[]> groupIds) {
List<String> blockedE164 = Stream.of(blocked)
.filter(b -> b.getNumber().isPresent())
@ -1885,6 +1938,7 @@ public class RecipientDatabase extends Database {
private final String signalProfileAvatar;
private final boolean hasProfileImage;
private final boolean profileSharing;
private final long lastProfileFetch;
private final String notificationChannel;
private final UnidentifiedAccessMode unidentifiedAccessMode;
private final boolean forceSmsSelection;
@ -1923,6 +1977,7 @@ public class RecipientDatabase extends Database {
@Nullable String signalProfileAvatar,
boolean hasProfileImage,
boolean profileSharing,
long lastProfileFetch,
@Nullable String notificationChannel,
@NonNull UnidentifiedAccessMode unidentifiedAccessMode,
boolean forceSmsSelection,
@ -1961,6 +2016,7 @@ public class RecipientDatabase extends Database {
this.signalProfileAvatar = signalProfileAvatar;
this.hasProfileImage = hasProfileImage;
this.profileSharing = profileSharing;
this.lastProfileFetch = lastProfileFetch;
this.notificationChannel = notificationChannel;
this.unidentifiedAccessMode = unidentifiedAccessMode;
this.forceSmsSelection = forceSmsSelection;
@ -2091,6 +2147,10 @@ public class RecipientDatabase extends Database {
return profileSharing;
}
public long getLastProfileFetch() {
return lastProfileFetch;
}
public @Nullable String getNotificationChannel() {
return notificationChannel;
}

View File

@ -135,8 +135,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int REMOTE_DELETE = 60;
private static final int COLOR_MIGRATION = 61;
private static final int LAST_SCROLLED = 62;
private static final int LAST_PROFILE_FETCH = 63;
private static final int DATABASE_VERSION = 62;
private static final int DATABASE_VERSION = 63;
private static final String DATABASE_NAME = "signal.db";
private final Context context;
@ -911,6 +912,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE thread ADD COLUMN last_scrolled INTEGER DEFAULT 0");
}
if (oldVersion < LAST_PROFILE_FETCH) {
db.execSQL("ALTER TABLE recipient ADD COLUMN last_profile_fetch INTEGER DEFAULT 0");
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();

View File

@ -45,7 +45,7 @@ public class RefreshPreKeysJob extends BaseJob {
}
public static void scheduleIfNecessary() {
long timeSinceLastRefresh = System.currentTimeMillis() - SignalStore.getLastPrekeyRefreshTime();
long timeSinceLastRefresh = System.currentTimeMillis() - SignalStore.misc().getLastPrekeyRefreshTime();
if (timeSinceLastRefresh > REFRESH_INTERVAL) {
Log.i(TAG, "Scheduling a prekey refresh. Time since last schedule: " + timeSinceLastRefresh + " ms");
@ -82,7 +82,7 @@ public class RefreshPreKeysJob extends BaseJob {
if (availableKeys >= PREKEY_MINIMUM && TextSecurePreferences.isSignedPreKeyRegistered(context)) {
Log.i(TAG, "Available keys sufficient.");
SignalStore.setLastPrekeyRefreshTime(System.currentTimeMillis());
SignalStore.misc().setLastPrekeyRefreshTime(System.currentTimeMillis());
return;
}
@ -98,7 +98,7 @@ public class RefreshPreKeysJob extends BaseJob {
TextSecurePreferences.setSignedPreKeyRegistered(context, true);
ApplicationDependencies.getJobManager().add(new CleanPreKeysJob());
SignalStore.setLastPrekeyRefreshTime(System.currentTimeMillis());
SignalStore.misc().setLastPrekeyRefreshTime(System.currentTimeMillis());
Log.i(TAG, "Successfully refreshed prekeys.");
}

View File

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.jobs;
import android.app.Application;
import android.content.Context;
import android.text.TextUtils;
@ -22,7 +23,7 @@ import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.linkpreview.Link;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.recipients.Recipient;
@ -32,6 +33,7 @@ import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.ProfileUtil;
import org.thoughtcrime.securesms.util.SetUtil;
import org.thoughtcrime.securesms.util.Stopwatch;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
@ -48,7 +50,6 @@ import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException
import org.whispersystems.signalservice.internal.util.concurrent.ListenableFuture;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
@ -128,6 +129,36 @@ public class RetrieveProfileJob extends BaseJob {
}
}
/**
* Will fetch some profiles to ensure we're decently up-to-date if we haven't done so within a
* certain time period.
*/
public static void enqueueRoutineFetchIfNeccessary(Application application) {
long timeSinceRefresh = System.currentTimeMillis() - SignalStore.misc().getLastProfileRefreshTime();
if (timeSinceRefresh < TimeUnit.HOURS.toMillis(12)) {
Log.i(TAG, "Too soon to refresh. Did the last refresh " + timeSinceRefresh + " ms ago.");
return;
}
SignalExecutors.BOUNDED.execute(() -> {
RecipientDatabase db = DatabaseFactory.getRecipientDatabase(application);
long current = System.currentTimeMillis();
List<RecipientId> ids = db.getRecipientsForRoutineProfileFetch(current - TimeUnit.DAYS.toMillis(30),
current - TimeUnit.DAYS.toMillis(1),
50);
if (ids.size() > 0) {
Log.i(TAG, "Optimistically refreshing " + ids.size() + " eligible recipient(s).");
enqueue(ids);
} else {
Log.i(TAG, "No recipients to refresh.");
}
SignalStore.misc().setLastProfileRefreshTime(System.currentTimeMillis());
});
}
private RetrieveProfileJob(@NonNull List<RecipientId> recipientIds) {
this(new Job.Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
@ -197,6 +228,9 @@ public class RetrieveProfileJob extends BaseJob {
process(profile.first(), profile.second());
}
Set<RecipientId> success = SetUtil.difference(recipientIds, retries);
DatabaseFactory.getRecipientDatabase(context).markProfilesFetched(success, System.currentTimeMillis());
stopwatch.split("process");
long keyCount = Stream.of(profiles).map(Pair::first).map(Recipient::getProfileKey).withoutNulls().count();

View File

@ -0,0 +1,42 @@
package org.thoughtcrime.securesms.keyvalue;
import androidx.annotation.NonNull;
public final class MiscellaneousValues extends SignalStoreValues {
private static final String LAST_PREKEY_REFRESH_TIME = "last_prekey_refresh_time";
private static final String MESSAGE_REQUEST_ENABLE_TIME = "message_request_enable_time";
private static final String LAST_PROFILE_REFRESH_TIME = "misc.last_profile_refresh_time";
MiscellaneousValues(@NonNull KeyValueStore store) {
super(store);
}
@Override
void onFirstEverAppLaunch() {
}
public long getLastPrekeyRefreshTime() {
return getLong(LAST_PREKEY_REFRESH_TIME, 0);
}
public void setLastPrekeyRefreshTime(long time) {
putLong(LAST_PREKEY_REFRESH_TIME, time);
}
public long getMessageRequestEnableTime() {
return getLong(MESSAGE_REQUEST_ENABLE_TIME, 0);
}
public void setMessageRequestEnableTime(long time) {
putLong(MESSAGE_REQUEST_ENABLE_TIME, time);
}
public long getLastProfileRefreshTime() {
return getLong(LAST_PROFILE_REFRESH_TIME, 0);
}
public void setLastProfileRefreshTime(long time) {
putLong(LAST_PROFILE_REFRESH_TIME, time);
}
}

View File

@ -12,9 +12,6 @@ import org.thoughtcrime.securesms.logging.SignalUncaughtExceptionHandler;
*/
public final class SignalStore {
private static final String LAST_PREKEY_REFRESH_TIME = "last_prekey_refresh_time";
private static final String MESSAGE_REQUEST_ENABLE_TIME = "message_request_enable_time";
private static final SignalStore INSTANCE = new SignalStore();
private final KeyValueStore store;
@ -25,6 +22,7 @@ public final class SignalStore {
private final StorageServiceValues storageServiceValues;
private final UiHints uiHints;
private final TooltipValues tooltipValues;
private final MiscellaneousValues misc;
private SignalStore() {
this.store = ApplicationDependencies.getKeyValueStore();
@ -35,6 +33,7 @@ public final class SignalStore {
this.storageServiceValues = new StorageServiceValues(store);
this.uiHints = new UiHints(store);
this.tooltipValues = new TooltipValues(store);
this.misc = new MiscellaneousValues(store);
}
public static void onFirstEverAppLaunch() {
@ -71,26 +70,14 @@ public final class SignalStore {
return INSTANCE.tooltipValues;
}
public static @NonNull MiscellaneousValues misc() {
return INSTANCE.misc;
}
public static @NonNull GroupsV2AuthorizationSignalStoreCache groupsV2AuthorizationCache() {
return new GroupsV2AuthorizationSignalStoreCache(getStore());
}
public static long getLastPrekeyRefreshTime() {
return getStore().getLong(LAST_PREKEY_REFRESH_TIME, 0);
}
public static void setLastPrekeyRefreshTime(long time) {
putLong(LAST_PREKEY_REFRESH_TIME, time);
}
public static long getMessageRequestEnableTime() {
return getStore().getLong(MESSAGE_REQUEST_ENABLE_TIME, 0);
}
public static void setMessageRequestEnableTime(long time) {
putLong(MESSAGE_REQUEST_ENABLE_TIME, time);
}
public static @NonNull PreferenceDataStore getPreferenceDataStore() {
return new SignalPreferenceDataStore(getStore());
}

View File

@ -89,6 +89,7 @@ public class Recipient {
private final String profileAvatar;
private final boolean hasProfileImage;
private final boolean profileSharing;
private final long lastProfileFetch;
private final String notificationChannel;
private final UnidentifiedAccessMode unidentifiedAccessMode;
private final boolean forceSmsSelection;
@ -332,6 +333,7 @@ public class Recipient {
this.profileAvatar = null;
this.hasProfileImage = false;
this.profileSharing = false;
this.lastProfileFetch = 0;
this.notificationChannel = null;
this.unidentifiedAccessMode = UnidentifiedAccessMode.DISABLED;
this.forceSmsSelection = false;
@ -374,6 +376,7 @@ public class Recipient {
this.profileAvatar = details.profileAvatar;
this.hasProfileImage = details.hasProfileImage;
this.profileSharing = details.profileSharing;
this.lastProfileFetch = details.lastProfileFetch;
this.notificationChannel = details.notificationChannel;
this.unidentifiedAccessMode = details.unidentifiedAccessMode;
this.forceSmsSelection = details.forceSmsSelection;
@ -610,6 +613,10 @@ public class Recipient {
return profileSharing;
}
public long getLastProfileFetchTime() {
return lastProfileFetch;
}
public boolean isGroup() {
return resolve().groupId != null;
}

View File

@ -53,6 +53,7 @@ public class RecipientDetails {
final String profileAvatar;
final boolean hasProfileImage;
final boolean profileSharing;
final long lastProfileFetch;
final boolean systemContact;
final boolean isLocalNumber;
final String notificationChannel;
@ -99,6 +100,7 @@ public class RecipientDetails {
this.profileAvatar = settings.getProfileAvatar();
this.hasProfileImage = settings.hasProfileImage();
this.profileSharing = settings.isProfileSharing();
this.lastProfileFetch = settings.getLastProfileFetch();
this.systemContact = systemContact;
this.isLocalNumber = isLocalNumber;
this.notificationChannel = settings.getNotificationChannel();
@ -146,6 +148,7 @@ public class RecipientDetails {
this.profileAvatar = null;
this.hasProfileImage = false;
this.profileSharing = false;
this.lastProfileFetch = 0;
this.systemContact = true;
this.isLocalNumber = false;
this.notificationChannel = null;

View File

@ -163,7 +163,7 @@ public class RecipientUtil {
return true;
}
long beforeTime = SignalStore.getMessageRequestEnableTime();
long beforeTime = SignalStore.misc().getMessageRequestEnableTime();
return DatabaseFactory.getMmsSmsDatabase(context).getConversationCount(threadId, beforeTime) > 0;
}

View File

@ -130,7 +130,7 @@ public final class FeatureFlags {
* desired test state.
*/
private static final Map<String, OnFlagChange> FLAG_CHANGE_LISTENERS = new HashMap<String, OnFlagChange>() {{
put(MESSAGE_REQUESTS, (change) -> SignalStore.setMessageRequestEnableTime(change == Change.ENABLED ? System.currentTimeMillis() : 0));
put(MESSAGE_REQUESTS, (change) -> SignalStore.misc().setMessageRequestEnableTime(change == Change.ENABLED ? System.currentTimeMillis() : 0));
put(VERSIONED_PROFILES, (change) -> {
if (change == Change.ENABLED) {
ApplicationDependencies.getJobManager().add(new ProfileUploadJob());