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.text.TextUtils; import org.thoughtcrime.securesms.logging.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.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, boolean notifyOfNewUsers) throws IOException { if (TextUtils.isEmpty(TextSecurePreferences.getLocalNumber(context))) return; if (!Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) return; List
newlyActiveUsers = refreshDirectory(context, AccountManagerFactory.createManager(context)); if (TextSecurePreferences.isMultiDevice(context)) { ApplicationContext.getInstance(context) .getJobManager() .add(new MultiDeviceContactUpdateJob(context)); } if (notifyOfNewUsers) notifyNewUsers(context, newlyActiveUsers); } private static @NonNull List
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 eligibleRecipientDatabaseContactNumbers = Stream.of(recipientDatabase.getAllAddresses()).filter(Address::isPhone).map(Address::toPhoneString); Stream eligibleSystemDatabaseContactNumbers = Stream.of(ContactAccessor.getInstance().getAllContactsWithNumbers(context)).map(Address::serialize); Set eligibleContactNumbers = Stream.concat(eligibleRecipientDatabaseContactNumbers, eligibleSystemDatabaseContactNumbers).collect(Collectors.toSet()); List activeTokens = accountManager.getContacts(eligibleContactNumbers); if (activeTokens != null) { List
activeAddresses = new LinkedList<>(); List
inactiveAddresses = new LinkedList<>(); Set 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
currentActiveAddresses = new HashSet<>(recipientDatabase.getRegistered()); Set
contactAddresses = new HashSet<>(recipientDatabase.getSystemContacts()); List
newlyActiveAddresses = Stream.of(activeAddresses) .filter(address -> !currentActiveAddresses.contains(address)) .filter(contactAddresses::contains) .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, @NonNull Recipient recipient) throws IOException { RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context); SignalServiceAccountManager accountManager = AccountManagerFactory.createManager(context); boolean activeUser = recipient.resolve().getRegistered() == RegisteredState.REGISTERED; boolean systemContact = recipient.isSystemContact(); String number = recipient.getAddress().serialize(); Optional 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 && systemContact && !TextSecurePreferences.getNeedsSqlCipherMigration(context)) { notifyNewUsers(context, 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
activeAddresses, boolean removeMissing) { Optional account = getOrCreateAccount(context); if (account.isPresent()) { try { DatabaseFactory.getContactsDatabase(context).removeDeletedRawContacts(account.get().getAccount()); 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, @NonNull List
newUsers) { if (!TextSecurePreferences.isNewContactsNotificationEnabled(context)) return; for (Address newUser: newUsers) { if (!SessionUtil.hasSession(context, newUser) && !Util.isOwnNumber(context, newUser)) { IncomingJoinedMessage message = new IncomingJoinedMessage(newUser); Optional 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, insertResult.get().getThreadId(), true); } else { MessageNotifier.updateNotification(context, insertResult.get().getThreadId(), false); } } } } } private static Optional getOrCreateAccount(Context context) { AccountManager accountManager = AccountManager.get(context); Account[] accounts = accountManager.getAccountsByType("org.thoughtcrime.securesms"); Optional 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 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; } } }