mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-09-20 16:54:28 +01:00
1) Move contact URI, contact photo URI, and custom label into recipient database, so there are no longer any contact DB queries during Recipient object loading. 2) Use a SoftHashMap so that any referenced Recipient objects can't get kicked out of the cache. 3) Don't load Recipient objects through the provider during sync. This was a super expensive thing to do, and blew up the cache. 4) Only apply changes to Recipient objects during sync if they are in the cache. Otherwise, there should be no outstanding references, and the changes are fine going exclusively to the DB.
265 lines
12 KiB
Java
265 lines
12 KiB
Java
package org.thoughtcrime.securesms.util;
|
|
|
|
import android.Manifest;
|
|
import android.accounts.Account;
|
|
import android.accounts.AccountManager;
|
|
import android.content.ContentResolver;
|
|
import android.content.Context;
|
|
import android.content.OperationApplicationException;
|
|
import android.database.Cursor;
|
|
import android.net.Uri;
|
|
import android.os.RemoteException;
|
|
import android.provider.ContactsContract;
|
|
import android.support.annotation.NonNull;
|
|
import android.support.annotation.Nullable;
|
|
import android.text.TextUtils;
|
|
import android.util.Log;
|
|
|
|
import com.annimon.stream.Collectors;
|
|
import com.annimon.stream.Stream;
|
|
|
|
import org.thoughtcrime.securesms.ApplicationContext;
|
|
import org.thoughtcrime.securesms.R;
|
|
import org.thoughtcrime.securesms.contacts.ContactAccessor;
|
|
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
|
import org.thoughtcrime.securesms.crypto.SessionUtil;
|
|
import org.thoughtcrime.securesms.database.Address;
|
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
|
import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult;
|
|
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
|
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
|
|
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
|
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
|
import org.thoughtcrime.securesms.push.AccountManagerFactory;
|
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
|
import org.thoughtcrime.securesms.sms.IncomingJoinedMessage;
|
|
import org.whispersystems.libsignal.util.guava.Optional;
|
|
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
|
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
|
|
|
|
import java.io.IOException;
|
|
import java.util.Calendar;
|
|
import java.util.Collections;
|
|
import java.util.HashSet;
|
|
import java.util.LinkedList;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
|
|
public class DirectoryHelper {
|
|
|
|
private static final String TAG = DirectoryHelper.class.getSimpleName();
|
|
|
|
public static void refreshDirectory(@NonNull Context context, @Nullable MasterSecret masterSecret, boolean notifyOfNewUsers)
|
|
throws IOException
|
|
{
|
|
if (TextUtils.isEmpty(TextSecurePreferences.getLocalNumber(context))) return;
|
|
if (!Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) return;
|
|
|
|
List<Address> newlyActiveUsers = refreshDirectory(context, AccountManagerFactory.createManager(context));
|
|
|
|
if (!newlyActiveUsers.isEmpty() && TextSecurePreferences.isMultiDevice(context)) {
|
|
ApplicationContext.getInstance(context)
|
|
.getJobManager()
|
|
.add(new MultiDeviceContactUpdateJob(context));
|
|
}
|
|
|
|
if (notifyOfNewUsers) notifyNewUsers(context, masterSecret, newlyActiveUsers);
|
|
}
|
|
|
|
public static @NonNull List<Address> refreshDirectory(@NonNull Context context, @NonNull SignalServiceAccountManager accountManager)
|
|
throws IOException
|
|
{
|
|
if (TextUtils.isEmpty(TextSecurePreferences.getLocalNumber(context))) {
|
|
return new LinkedList<>();
|
|
}
|
|
|
|
if (!Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) {
|
|
return new LinkedList<>();
|
|
}
|
|
|
|
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
|
Stream<String> eligibleRecipientDatabaseContactNumbers = Stream.of(recipientDatabase.getAllAddresses()).filter(Address::isPhone).map(Address::toPhoneString);
|
|
Stream<String> eligibleSystemDatabaseContactNumbers = Stream.of(ContactAccessor.getInstance().getAllContactsWithNumbers(context)).map(Address::serialize);
|
|
Set<String> eligibleContactNumbers = Stream.concat(eligibleRecipientDatabaseContactNumbers, eligibleSystemDatabaseContactNumbers).collect(Collectors.toSet());
|
|
|
|
List<ContactTokenDetails> activeTokens = accountManager.getContacts(eligibleContactNumbers);
|
|
|
|
if (activeTokens != null) {
|
|
List<Address> activeAddresses = new LinkedList<>();
|
|
List<Address> inactiveAddresses = new LinkedList<>();
|
|
|
|
Set<String> inactiveContactNumbers = new HashSet<>(eligibleContactNumbers);
|
|
|
|
for (ContactTokenDetails activeToken : activeTokens) {
|
|
activeAddresses.add(Address.fromSerialized(activeToken.getNumber()));
|
|
inactiveContactNumbers.remove(activeToken.getNumber());
|
|
}
|
|
|
|
for (String inactiveContactNumber : inactiveContactNumbers) {
|
|
inactiveAddresses.add(Address.fromSerialized(inactiveContactNumber));
|
|
}
|
|
|
|
Set<Address> currentActiveAddresses = new HashSet<>(recipientDatabase.getRegistered());
|
|
List<Address> newlyActiveAddresses = Stream.of(activeAddresses)
|
|
.filter(address -> !currentActiveAddresses.contains(address))
|
|
.toList();
|
|
|
|
recipientDatabase.setRegistered(activeAddresses, inactiveAddresses);
|
|
updateContactsDatabase(context, activeAddresses, true);
|
|
|
|
if (TextSecurePreferences.hasSuccessfullyRetrievedDirectory(context)) {
|
|
return newlyActiveAddresses;
|
|
} else {
|
|
TextSecurePreferences.setHasSuccessfullyRetrievedDirectory(context, true);
|
|
return new LinkedList<>();
|
|
}
|
|
}
|
|
|
|
return new LinkedList<>();
|
|
}
|
|
|
|
public static RegisteredState refreshDirectoryFor(@NonNull Context context,
|
|
@Nullable MasterSecret masterSecret,
|
|
@NonNull Recipient recipient)
|
|
throws IOException
|
|
{
|
|
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
|
SignalServiceAccountManager accountManager = AccountManagerFactory.createManager(context);
|
|
boolean activeUser = recipient.resolve().getRegistered() == RegisteredState.REGISTERED;
|
|
String number = recipient.getAddress().serialize();
|
|
Optional<ContactTokenDetails> details = accountManager.getContact(number);
|
|
|
|
if (details.isPresent()) {
|
|
recipientDatabase.setRegistered(recipient, RegisteredState.REGISTERED);
|
|
|
|
if (Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) {
|
|
updateContactsDatabase(context, Util.asList(recipient.getAddress()), false);
|
|
}
|
|
|
|
if (!activeUser && TextSecurePreferences.isMultiDevice(context)) {
|
|
ApplicationContext.getInstance(context).getJobManager().add(new MultiDeviceContactUpdateJob(context));
|
|
}
|
|
|
|
if (!activeUser) {
|
|
notifyNewUsers(context, masterSecret, Collections.singletonList(recipient.getAddress()));
|
|
}
|
|
|
|
return RegisteredState.REGISTERED;
|
|
} else {
|
|
recipientDatabase.setRegistered(recipient, RegisteredState.NOT_REGISTERED);
|
|
return RegisteredState.NOT_REGISTERED;
|
|
}
|
|
}
|
|
|
|
private static void updateContactsDatabase(@NonNull Context context, @NonNull List<Address> activeAddresses, boolean removeMissing) {
|
|
Optional<AccountHolder> account = getOrCreateAccount(context);
|
|
|
|
if (account.isPresent()) {
|
|
try {
|
|
DatabaseFactory.getContactsDatabase(context).setRegisteredUsers(account.get().getAccount(), activeAddresses, removeMissing);
|
|
|
|
Cursor cursor = ContactAccessor.getInstance().getAllSystemContacts(context);
|
|
RecipientDatabase.BulkOperationsHandle handle = DatabaseFactory.getRecipientDatabase(context).resetAllSystemContactInfo();
|
|
|
|
try {
|
|
while (cursor != null && cursor.moveToNext()) {
|
|
String number = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER));
|
|
|
|
if (!TextUtils.isEmpty(number)) {
|
|
Address address = Address.fromExternal(context, number);
|
|
String displayName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
|
|
String contactPhotoUri = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.PHOTO_URI));
|
|
String contactLabel = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.LABEL));
|
|
Uri contactUri = ContactsContract.Contacts.getLookupUri(cursor.getLong(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone._ID)),
|
|
cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY)));
|
|
|
|
handle.setSystemContactInfo(address, displayName, contactPhotoUri, contactLabel, contactUri.toString());
|
|
}
|
|
}
|
|
} finally {
|
|
handle.finish();
|
|
}
|
|
|
|
} catch (RemoteException | OperationApplicationException e) {
|
|
Log.w(TAG, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void notifyNewUsers(@NonNull Context context,
|
|
@Nullable MasterSecret masterSecret,
|
|
@NonNull List<Address> newUsers)
|
|
{
|
|
if (!TextSecurePreferences.isNewContactsNotificationEnabled(context)) return;
|
|
|
|
for (Address newUser: newUsers) {
|
|
if (!SessionUtil.hasSession(context, masterSecret, newUser) && !Util.isOwnNumber(context, newUser)) {
|
|
IncomingJoinedMessage message = new IncomingJoinedMessage(newUser);
|
|
Optional<InsertResult> insertResult = DatabaseFactory.getSmsDatabase(context).insertMessageInbox(message);
|
|
|
|
if (insertResult.isPresent()) {
|
|
int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
|
|
if (hour >= 9 && hour < 23) {
|
|
MessageNotifier.updateNotification(context, masterSecret, insertResult.get().getThreadId(), true);
|
|
} else {
|
|
MessageNotifier.updateNotification(context, masterSecret, insertResult.get().getThreadId(), false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static Optional<AccountHolder> getOrCreateAccount(Context context) {
|
|
AccountManager accountManager = AccountManager.get(context);
|
|
Account[] accounts = accountManager.getAccountsByType("org.thoughtcrime.securesms");
|
|
|
|
Optional<AccountHolder> account;
|
|
|
|
if (accounts.length == 0) account = createAccount(context);
|
|
else account = Optional.of(new AccountHolder(accounts[0], false));
|
|
|
|
if (account.isPresent() && !ContentResolver.getSyncAutomatically(account.get().getAccount(), ContactsContract.AUTHORITY)) {
|
|
ContentResolver.setSyncAutomatically(account.get().getAccount(), ContactsContract.AUTHORITY, true);
|
|
}
|
|
|
|
return account;
|
|
}
|
|
|
|
private static Optional<AccountHolder> createAccount(Context context) {
|
|
AccountManager accountManager = AccountManager.get(context);
|
|
Account account = new Account(context.getString(R.string.app_name), "org.thoughtcrime.securesms");
|
|
|
|
if (accountManager.addAccountExplicitly(account, null, null)) {
|
|
Log.w(TAG, "Created new account...");
|
|
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1);
|
|
return Optional.of(new AccountHolder(account, true));
|
|
} else {
|
|
Log.w(TAG, "Failed to create account!");
|
|
return Optional.absent();
|
|
}
|
|
}
|
|
|
|
private static class AccountHolder {
|
|
|
|
private final boolean fresh;
|
|
private final Account account;
|
|
|
|
private AccountHolder(Account account, boolean fresh) {
|
|
this.fresh = fresh;
|
|
this.account = account;
|
|
}
|
|
|
|
@SuppressWarnings("unused")
|
|
public boolean isFresh() {
|
|
return fresh;
|
|
}
|
|
|
|
public Account getAccount() {
|
|
return account;
|
|
}
|
|
|
|
}
|
|
|
|
}
|