Move more system contact info into recipient database

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.
This commit is contained in:
Moxie Marlinspike 2017-11-26 10:45:39 -08:00
parent 64c8b4b2ef
commit 7a5846a6d4
14 changed files with 618 additions and 2571 deletions

View File

@ -79,7 +79,7 @@ public class ContactAccessor {
} }
public Cursor getAllSystemContacts(Context context) { public Cursor getAllSystemContacts(Context context) {
return context.getContentResolver().query(Phone.CONTENT_URI, new String[] {Phone.NUMBER, Phone.DISPLAY_NAME}, null, null, null); return context.getContentResolver().query(Phone.CONTENT_URI, new String[] {Phone.NUMBER, Phone.DISPLAY_NAME, Phone.LABEL, Phone.PHOTO_URI, Phone._ID, Phone.LOOKUP_KEY}, null, null, null);
} }
public boolean isSystemContact(Context context, String number) { public boolean isSystemContact(Context context, String number) {

View File

@ -16,6 +16,7 @@
*/ */
package org.thoughtcrime.securesms.database; package org.thoughtcrime.securesms.database;
import android.Manifest;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
@ -23,6 +24,8 @@ import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.provider.ContactsContract;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
@ -40,6 +43,7 @@ import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil; import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.DelimiterUtil; import org.thoughtcrime.securesms.util.DelimiterUtil;
import org.thoughtcrime.securesms.util.Hex; import org.thoughtcrime.securesms.util.Hex;
@ -110,7 +114,8 @@ public class DatabaseFactory {
private static final int READ_RECEIPTS = 44; private static final int READ_RECEIPTS = 44;
private static final int GROUP_RECEIPT_TRACKING = 45; private static final int GROUP_RECEIPT_TRACKING = 45;
private static final int UNREAD_COUNT_VERSION = 46; private static final int UNREAD_COUNT_VERSION = 46;
private static final int DATABASE_VERSION = 46; private static final int MORE_RECIPIENT_FIELDS = 47;
private static final int DATABASE_VERSION = 47;
private static final String DATABASE_NAME = "messages.db"; private static final String DATABASE_NAME = "messages.db";
private static final Object lock = new Object(); private static final Object lock = new Object();
@ -1374,6 +1379,42 @@ public class DatabaseFactory {
} }
} }
if (oldVersion < MORE_RECIPIENT_FIELDS) {
db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN system_contact_photo TEXT DEFAULT NULL");
db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN system_phone_label TEXT DEFAULT NULL");
db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN system_contact_uri TEXT DEFAULT NULL");
if (Permissions.hasAny(context, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) {
try (Cursor cursor = db.query("recipient_preferences", null, null, null, null, null, null)) {
while (cursor != null && cursor.moveToNext()) {
Address address = Address.fromSerialized(cursor.getString(cursor.getColumnIndexOrThrow("recipient_ids")));
if (address.isPhone() && !TextUtils.isEmpty(address.toPhoneString())) {
Uri lookup = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(address.toPhoneString()));
try (Cursor contactCursor = context.getContentResolver().query(lookup, new String[] {ContactsContract.PhoneLookup.DISPLAY_NAME,
ContactsContract.PhoneLookup.LOOKUP_KEY,
ContactsContract.PhoneLookup._ID,
ContactsContract.PhoneLookup.NUMBER,
ContactsContract.PhoneLookup.LABEL,
ContactsContract.PhoneLookup.PHOTO_URI},
null, null, null))
{
if (contactCursor != null && contactCursor.moveToFirst()) {
ContentValues contentValues = new ContentValues(3);
contentValues.put("system_contact_photo", contactCursor.getString(5));
contentValues.put("system_phone_label", contactCursor.getString(4));
contentValues.put("system_contact_uri", ContactsContract.Contacts.getLookupUri(contactCursor.getLong(2), contactCursor.getString(1)).toString());
db.update("recipient_preferences", contentValues, "recipient_ids = ?", new String[] {address.toPhoneString()});
}
}
}
}
}
}
}
db.setTransactionSuccessful(); db.setTransactionSuccessful();
db.endTransaction(); db.endTransaction();
} }

View File

@ -14,7 +14,6 @@ import android.text.TextUtils;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.contacts.avatars.GroupRecordContactPhoto;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.GroupUtil;
@ -175,12 +174,11 @@ public class GroupDatabase extends Database {
databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues); databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues);
Address address = Address.fromSerialized(groupId); Recipient.applyCached(Address.fromSerialized(groupId), recipient -> {
Recipient recipient = Recipient.from(context, Address.fromSerialized(groupId), false); recipient.setName(title);
recipient.setGroupAvatarId(avatar != null ? avatar.getId() : null);
recipient.setName(title); recipient.setParticipants(Stream.of(members).map(memberAddress -> Recipient.from(context, memberAddress, true)).toList());
if (avatar != null) recipient.setContactPhoto(new GroupRecordContactPhoto(address, avatar.getId())); });
recipient.setParticipants(Stream.of(members).map(memberAddress -> Recipient.from(context, memberAddress, true)).toList());
notifyConversationListListeners(); notifyConversationListListeners();
} }
@ -200,10 +198,10 @@ public class GroupDatabase extends Database {
GROUP_ID + " = ?", GROUP_ID + " = ?",
new String[] {groupId}); new String[] {groupId});
Address address = Address.fromSerialized(groupId); Recipient.applyCached(Address.fromSerialized(groupId), recipient -> {
Recipient recipient = Recipient.from(context, address, false); recipient.setName(title);
recipient.setName(title); recipient.setGroupAvatarId(avatar != null ? avatar.getId() : null);
if (avatar != null) recipient.setContactPhoto(new GroupRecordContactPhoto(address, avatar.getId())); });
notifyConversationListListeners(); notifyConversationListListeners();
} }
@ -231,9 +229,7 @@ public class GroupDatabase extends Database {
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ?", databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ?",
new String[] {groupId}); new String[] {groupId});
Address address = Address.fromSerialized(groupId); Recipient.applyCached(Address.fromSerialized(groupId), recipient -> recipient.setGroupAvatarId(avatarId));
Recipient recipient = Recipient.from(context, address, false);
recipient.setContactPhoto(new GroupRecordContactPhoto(address, avatarId));
} }
public void updateMembers(String groupId, List<Address> members) { public void updateMembers(String groupId, List<Address> members) {

View File

@ -15,19 +15,20 @@ import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.color.MaterialColor; import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.Base64;
import org.whispersystems.libsignal.util.Pair; import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
public class RecipientDatabase extends Database { public class RecipientDatabase extends Database {
private static final String TAG = RecipientDatabase.class.getSimpleName(); private static final String TAG = RecipientDatabase.class.getSimpleName();
private static final String RECIPIENT_PREFERENCES_URI = "content://textsecure/recipients/";
static final String TABLE_NAME = "recipient_preferences"; static final String TABLE_NAME = "recipient_preferences";
private static final String ID = "_id"; private static final String ID = "_id";
@ -43,13 +44,17 @@ public class RecipientDatabase extends Database {
private static final String REGISTERED = "registered"; private static final String REGISTERED = "registered";
private static final String PROFILE_KEY = "profile_key"; private static final String PROFILE_KEY = "profile_key";
private static final String SYSTEM_DISPLAY_NAME = "system_display_name"; private static final String SYSTEM_DISPLAY_NAME = "system_display_name";
private static final String SYSTEM_PHOTO_URI = "system_contact_photo";
private static final String SYSTEM_PHONE_LABEL = "system_phone_label";
private static final String SYSTEM_CONTACT_URI = "system_contact_uri";
private static final String SIGNAL_PROFILE_NAME = "signal_profile_name"; private static final String SIGNAL_PROFILE_NAME = "signal_profile_name";
private static final String SIGNAL_PROFILE_AVATAR = "signal_profile_avatar"; private static final String SIGNAL_PROFILE_AVATAR = "signal_profile_avatar";
private static final String PROFILE_SHARING = "profile_sharing_approval"; private static final String PROFILE_SHARING = "profile_sharing_approval";
private static final String[] RECIPIENT_PROJECTION = new String[] { private static final String[] RECIPIENT_PROJECTION = new String[] {
BLOCK, NOTIFICATION, VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, EXPIRE_MESSAGES, REGISTERED, BLOCK, NOTIFICATION, VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, EXPIRE_MESSAGES, REGISTERED,
PROFILE_KEY, SYSTEM_DISPLAY_NAME, SIGNAL_PROFILE_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING PROFILE_KEY, SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_CONTACT_URI,
SIGNAL_PROFILE_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING
}; };
static final List<String> TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION) static final List<String> TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION)
@ -106,6 +111,9 @@ public class RecipientDatabase extends Database {
EXPIRE_MESSAGES + " INTEGER DEFAULT 0, " + EXPIRE_MESSAGES + " INTEGER DEFAULT 0, " +
REGISTERED + " INTEGER DEFAULT 0, " + REGISTERED + " INTEGER DEFAULT 0, " +
SYSTEM_DISPLAY_NAME + " TEXT DEFAULT NULL, " + SYSTEM_DISPLAY_NAME + " TEXT DEFAULT NULL, " +
SYSTEM_PHOTO_URI + " TEXT DEFAULT NULL, " +
SYSTEM_PHONE_LABEL + " TEXT DEFAULT NULL, " +
SYSTEM_CONTACT_URI + " TEXT DEFAULT NULL, " +
PROFILE_KEY + " TEXT DEFAULT NULL, " + PROFILE_KEY + " TEXT DEFAULT NULL, " +
SIGNAL_PROFILE_NAME + " TEXT DEFAULT NULL, " + SIGNAL_PROFILE_NAME + " TEXT DEFAULT NULL, " +
SIGNAL_PROFILE_AVATAR + " TEXT DEFAULT NULL, " + SIGNAL_PROFILE_AVATAR + " TEXT DEFAULT NULL, " +
@ -118,11 +126,8 @@ public class RecipientDatabase extends Database {
public Cursor getBlocked() { public Cursor getBlocked() {
SQLiteDatabase database = databaseHelper.getReadableDatabase(); SQLiteDatabase database = databaseHelper.getReadableDatabase();
Cursor cursor = database.query(TABLE_NAME, new String[] {ID, ADDRESS}, BLOCK + " = 1", return database.query(TABLE_NAME, new String[] {ID, ADDRESS}, BLOCK + " = 1",
null, null, null, null, null); null, null, null, null, null);
cursor.setNotificationUri(context.getContentResolver(), Uri.parse(RECIPIENT_PREFERENCES_URI));
return cursor;
} }
public BlockedReader readerForBlocked(Cursor cursor) { public BlockedReader readerForBlocked(Cursor cursor) {
@ -153,13 +158,15 @@ public class RecipientDatabase extends Database {
int vibrateState = cursor.getInt(cursor.getColumnIndexOrThrow(VIBRATE)); int vibrateState = cursor.getInt(cursor.getColumnIndexOrThrow(VIBRATE));
long muteUntil = cursor.getLong(cursor.getColumnIndexOrThrow(MUTE_UNTIL)); long muteUntil = cursor.getLong(cursor.getColumnIndexOrThrow(MUTE_UNTIL));
String serializedColor = cursor.getString(cursor.getColumnIndexOrThrow(COLOR)); 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; boolean seenInviteReminder = cursor.getInt(cursor.getColumnIndexOrThrow(SEEN_INVITE_REMINDER)) == 1;
int defaultSubscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(DEFAULT_SUBSCRIPTION_ID)); int defaultSubscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(DEFAULT_SUBSCRIPTION_ID));
int expireMessages = cursor.getInt(cursor.getColumnIndexOrThrow(EXPIRE_MESSAGES)); int expireMessages = cursor.getInt(cursor.getColumnIndexOrThrow(EXPIRE_MESSAGES));
int registeredState = cursor.getInt(cursor.getColumnIndexOrThrow(REGISTERED)); int registeredState = cursor.getInt(cursor.getColumnIndexOrThrow(REGISTERED));
String profileKeyString = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_KEY)); String profileKeyString = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_KEY));
String systemDisplayName = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_DISPLAY_NAME)); String systemDisplayName = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_DISPLAY_NAME));
String systemContactPhoto = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_PHOTO_URI));
String systemPhoneLabel = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_PHONE_LABEL));
String systemContactUri = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_CONTACT_URI));
String signalProfileName = cursor.getString(cursor.getColumnIndexOrThrow(SIGNAL_PROFILE_NAME)); String signalProfileName = cursor.getString(cursor.getColumnIndexOrThrow(SIGNAL_PROFILE_NAME));
String signalProfileAvatar = cursor.getString(cursor.getColumnIndexOrThrow(SIGNAL_PROFILE_AVATAR)); String signalProfileAvatar = cursor.getString(cursor.getColumnIndexOrThrow(SIGNAL_PROFILE_AVATAR));
boolean profileSharing = cursor.getInt(cursor.getColumnIndexOrThrow(PROFILE_SHARING)) == 1; boolean profileSharing = cursor.getInt(cursor.getColumnIndexOrThrow(PROFILE_SHARING)) == 1;
@ -185,19 +192,23 @@ public class RecipientDatabase extends Database {
return Optional.of(new RecipientSettings(blocked, muteUntil, return Optional.of(new RecipientSettings(blocked, muteUntil,
VibrateState.fromId(vibrateState), VibrateState.fromId(vibrateState),
notificationUri, color, seenInviteReminder, Util.uri(notification), color, seenInviteReminder,
defaultSubscriptionId, expireMessages, defaultSubscriptionId, expireMessages,
RegisteredState.fromId(registeredState), RegisteredState.fromId(registeredState),
profileKey, systemDisplayName, signalProfileName, profileKey, systemDisplayName, systemContactPhoto,
signalProfileAvatar, profileSharing)); systemPhoneLabel, systemContactUri,
signalProfileName, signalProfileAvatar, profileSharing));
} }
public BulkOperationsHandle resetAllDisplayNames() { public BulkOperationsHandle resetAllSystemContactInfo() {
SQLiteDatabase database = databaseHelper.getWritableDatabase(); SQLiteDatabase database = databaseHelper.getWritableDatabase();
database.beginTransaction(); database.beginTransaction();
ContentValues contentValues = new ContentValues(1); ContentValues contentValues = new ContentValues(1);
contentValues.put(SYSTEM_DISPLAY_NAME, (String)null); contentValues.put(SYSTEM_DISPLAY_NAME, (String)null);
contentValues.put(SYSTEM_PHOTO_URI, (String)null);
contentValues.put(SYSTEM_PHONE_LABEL, (String)null);
contentValues.put(SYSTEM_CONTACT_URI, (String)null);
database.update(TABLE_NAME, contentValues, null, null); database.update(TABLE_NAME, contentValues, null, null);
@ -246,7 +257,7 @@ public class RecipientDatabase extends Database {
recipient.resolve().setMuted(until); recipient.resolve().setMuted(until);
} }
public void setSeenInviteReminder(@NonNull Recipient recipient, boolean seen) { public void setSeenInviteReminder(@NonNull Recipient recipient, @SuppressWarnings("SameParameterValue") boolean seen) {
ContentValues values = new ContentValues(1); ContentValues values = new ContentValues(1);
values.put(SEEN_INVITE_REMINDER, seen ? 1 : 0); values.put(SEEN_INVITE_REMINDER, seen ? 1 : 0);
updateOrInsert(recipient.getAddress(), values); updateOrInsert(recipient.getAddress(), values);
@ -283,20 +294,20 @@ public class RecipientDatabase extends Database {
recipient.resolve().setProfileAvatar(profileAvatar); recipient.resolve().setProfileAvatar(profileAvatar);
} }
public void setProfileSharing(@NonNull Recipient recipient, boolean enabled) { public void setProfileSharing(@NonNull Recipient recipient, @SuppressWarnings("SameParameterValue") boolean enabled) {
ContentValues contentValues = new ContentValues(1); ContentValues contentValues = new ContentValues(1);
contentValues.put(PROFILE_SHARING, enabled ? 1 : 0); contentValues.put(PROFILE_SHARING, enabled ? 1 : 0);
updateOrInsert(recipient.getAddress(), contentValues); updateOrInsert(recipient.getAddress(), contentValues);
recipient.setProfileSharing(enabled); recipient.setProfileSharing(enabled);
} }
public Set<Recipient> getAllRecipients() { public Set<Address> getAllAddresses() {
SQLiteDatabase db = databaseHelper.getReadableDatabase(); SQLiteDatabase db = databaseHelper.getReadableDatabase();
Set<Recipient> results = new HashSet<>(); Set<Address> results = new HashSet<>();
try (Cursor cursor = db.query(TABLE_NAME, new String[] {ADDRESS}, null, null, null, null, null)) { try (Cursor cursor = db.query(TABLE_NAME, new String[] {ADDRESS}, null, null, null, null, null)) {
while (cursor != null && cursor.moveToNext()) { while (cursor != null && cursor.moveToNext()) {
results.add(Recipient.from(context, Address.fromExternal(context, cursor.getString(0)), true)); results.add(Address.fromExternal(context, cursor.getString(0)));
} }
} }
@ -310,26 +321,24 @@ public class RecipientDatabase extends Database {
recipient.setRegistered(registeredState); recipient.setRegistered(registeredState);
} }
public void setRegistered(@NonNull List<Recipient> activeRecipients, public void setRegistered(@NonNull List<Address> activeAddresses,
@NonNull List<Recipient> inactiveRecipients) @NonNull List<Address> inactiveAddresses)
{ {
for (Recipient activeRecipient : activeRecipients) { for (Address activeAddress : activeAddresses) {
ContentValues contentValues = new ContentValues(1); ContentValues contentValues = new ContentValues(1);
contentValues.put(REGISTERED, RegisteredState.REGISTERED.getId()); contentValues.put(REGISTERED, RegisteredState.REGISTERED.getId());
updateOrInsert(activeRecipient.getAddress(), contentValues); updateOrInsert(activeAddress, contentValues);
activeRecipient.setRegistered(RegisteredState.REGISTERED); Recipient.applyCached(activeAddress, recipient -> recipient.setRegistered(RegisteredState.REGISTERED));
} }
for (Recipient inactiveRecipient : inactiveRecipients) { for (Address inactiveAddress : inactiveAddresses) {
ContentValues contentValues = new ContentValues(1); ContentValues contentValues = new ContentValues(1);
contentValues.put(REGISTERED, RegisteredState.NOT_REGISTERED.getId()); contentValues.put(REGISTERED, RegisteredState.NOT_REGISTERED.getId());
updateOrInsert(inactiveRecipient.getAddress(), contentValues); updateOrInsert(inactiveAddress, contentValues);
inactiveRecipient.setRegistered(RegisteredState.NOT_REGISTERED); Recipient.applyCached(inactiveAddress, recipient -> recipient.setRegistered(RegisteredState.NOT_REGISTERED));
} }
context.getContentResolver().notifyChange(Uri.parse(RECIPIENT_PREFERENCES_URI), null);
} }
public List<Address> getRegistered() { public List<Address> getRegistered() {
@ -360,15 +369,6 @@ public class RecipientDatabase extends Database {
database.beginTransaction(); database.beginTransaction();
updateOrInsert(database, address, contentValues);
database.setTransactionSuccessful();
database.endTransaction();
context.getContentResolver().notifyChange(Uri.parse(RECIPIENT_PREFERENCES_URI), null);
}
private void updateOrInsert(SQLiteDatabase database, Address address, ContentValues contentValues) {
int updated = database.update(TABLE_NAME, contentValues, ADDRESS + " = ?", int updated = database.update(TABLE_NAME, contentValues, ADDRESS + " = ?",
new String[] {address.serialize()}); new String[] {address.serialize()});
@ -376,32 +376,43 @@ public class RecipientDatabase extends Database {
contentValues.put(ADDRESS, address.serialize()); contentValues.put(ADDRESS, address.serialize());
database.insert(TABLE_NAME, null, contentValues); database.insert(TABLE_NAME, null, contentValues);
} }
database.setTransactionSuccessful();
database.endTransaction();
} }
public class BulkOperationsHandle { public class BulkOperationsHandle {
private final SQLiteDatabase database; private final SQLiteDatabase database;
private final List<Pair<Recipient, String>> pendingDisplayNames = new LinkedList<>(); private final Map<Address, PendingContactInfo> pendingContactInfoMap = new HashMap<>();
BulkOperationsHandle(SQLiteDatabase database) { BulkOperationsHandle(SQLiteDatabase database) {
this.database = database; this.database = database;
} }
public void setDisplayName(@NonNull Recipient recipient, @Nullable String displayName) { public void setSystemContactInfo(@NonNull Address address, @Nullable String displayName, @Nullable String photoUri, @Nullable String systemPhoneLabel, @Nullable String systemContactUri) {
ContentValues contentValues = new ContentValues(1); ContentValues contentValues = new ContentValues(1);
contentValues.put(SYSTEM_DISPLAY_NAME, displayName); contentValues.put(SYSTEM_DISPLAY_NAME, displayName);
updateOrInsert(recipient.getAddress(), contentValues); contentValues.put(SYSTEM_PHOTO_URI, photoUri);
pendingDisplayNames.add(new Pair<>(recipient, displayName)); contentValues.put(SYSTEM_PHONE_LABEL, systemPhoneLabel);
contentValues.put(SYSTEM_CONTACT_URI, systemContactUri);
updateOrInsert(address, contentValues);
pendingContactInfoMap.put(address, new PendingContactInfo(displayName, photoUri, systemPhoneLabel, systemContactUri));
} }
public void finish() { public void finish() {
database.setTransactionSuccessful(); database.setTransactionSuccessful();
database.endTransaction(); database.endTransaction();
Stream.of(pendingDisplayNames).forEach(pair -> pair.first().resolve().setName(pair.second())); Stream.of(pendingContactInfoMap.entrySet())
.forEach(entry -> Recipient.applyCached(entry.getKey(), recipient -> {
context.getContentResolver().notifyChange(Uri.parse(RECIPIENT_PREFERENCES_URI), null); recipient.setName(entry.getValue().displayName);
recipient.setSystemContactPhoto(Util.uri(entry.getValue().photoUri));
recipient.setCustomLabel(entry.getValue().phoneLabel);
recipient.setContactUri(Util.uri(entry.getValue().contactUri));
}));
} }
} }
@ -417,6 +428,9 @@ public class RecipientDatabase extends Database {
private final RegisteredState registered; private final RegisteredState registered;
private final byte[] profileKey; private final byte[] profileKey;
private final String systemDisplayName; private final String systemDisplayName;
private final String systemContactPhoto;
private final String systemPhoneLabel;
private final String systemContactUri;
private final String signalProfileName; private final String signalProfileName;
private final String signalProfileAvatar; private final String signalProfileAvatar;
private final boolean profileSharing; private final boolean profileSharing;
@ -431,6 +445,9 @@ public class RecipientDatabase extends Database {
@NonNull RegisteredState registered, @NonNull RegisteredState registered,
@Nullable byte[] profileKey, @Nullable byte[] profileKey,
@Nullable String systemDisplayName, @Nullable String systemDisplayName,
@Nullable String systemContactPhoto,
@Nullable String systemPhoneLabel,
@Nullable String systemContactUri,
@Nullable String signalProfileName, @Nullable String signalProfileName,
@Nullable String signalProfileAvatar, @Nullable String signalProfileAvatar,
boolean profileSharing) boolean profileSharing)
@ -446,6 +463,9 @@ public class RecipientDatabase extends Database {
this.registered = registered; this.registered = registered;
this.profileKey = profileKey; this.profileKey = profileKey;
this.systemDisplayName = systemDisplayName; this.systemDisplayName = systemDisplayName;
this.systemContactPhoto = systemContactPhoto;
this.systemPhoneLabel = systemPhoneLabel;
this.systemContactUri = systemContactUri;
this.signalProfileName = signalProfileName; this.signalProfileName = signalProfileName;
this.signalProfileAvatar = signalProfileAvatar; this.signalProfileAvatar = signalProfileAvatar;
this.profileSharing = profileSharing; this.profileSharing = profileSharing;
@ -495,6 +515,18 @@ public class RecipientDatabase extends Database {
return systemDisplayName; return systemDisplayName;
} }
public @Nullable String getSystemContactPhotoUri() {
return systemContactPhoto;
}
public @Nullable String getSystemPhoneLabel() {
return systemPhoneLabel;
}
public @Nullable String getSystemContactUri() {
return systemContactUri;
}
public @Nullable String getProfileName() { public @Nullable String getProfileName() {
return signalProfileName; return signalProfileName;
} }
@ -531,4 +563,20 @@ public class RecipientDatabase extends Database {
return getCurrent(); return getCurrent();
} }
} }
private static class PendingContactInfo {
private final String displayName;
private final String photoUri;
private final String phoneLabel;
private final String contactUri;
private PendingContactInfo(String displayName, String photoUri, String phoneLabel, String contactUri) {
this.displayName = displayName;
this.photoUri = photoUri;
this.phoneLabel = phoneLabel;
this.contactUri = contactUri;
}
}
} }

View File

@ -5,7 +5,6 @@ import android.content.Context;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.dependencies.InjectableType; import org.thoughtcrime.securesms.dependencies.InjectableType;
@ -83,10 +82,6 @@ public class RetrieveProfileAvatarJob extends ContextJob implements InjectableTy
} }
database.setProfileAvatar(recipient, profileAvatar); database.setProfileAvatar(recipient, profileAvatar);
if (recipient.resolve().getContactPhoto() == null) {
recipient.setContactPhoto(new ProfileContactPhoto(recipient.getAddress(), profileAvatar));
}
} }
@Override @Override

View File

@ -22,14 +22,21 @@ import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import com.annimon.stream.Stream; import com.annimon.stream.function.Consumer;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.color.MaterialColor; import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.contacts.avatars.ContactColors; import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto; import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto; import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.GroupRecordContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.SystemContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.TransparentContactPhoto; import org.thoughtcrime.securesms.contacts.avatars.TransparentContactPhoto;
import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.GroupDatabase;
@ -42,7 +49,6 @@ import org.thoughtcrime.securesms.util.ListenableFutureTask;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
@ -65,8 +71,8 @@ public class Recipient implements RecipientModifiedListener {
private @Nullable String customLabel; private @Nullable String customLabel;
private boolean resolving; private boolean resolving;
private @Nullable ContactPhoto contactPhoto; private @Nullable Uri systemContactPhoto;
private @NonNull FallbackContactPhoto fallbackContactPhoto; private @Nullable Long groupAvatarId;
private Uri contactUri; private Uri contactUri;
private @Nullable Uri ringtone = null; private @Nullable Uri ringtone = null;
private long mutedUntil = 0; private long mutedUntil = 0;
@ -97,21 +103,25 @@ public class Recipient implements RecipientModifiedListener {
return provider.getRecipient(context, address, settings, groupRecord, asynchronous); return provider.getRecipient(context, address, settings, groupRecord, asynchronous);
} }
public static void applyCached(@NonNull Address address, Consumer<Recipient> consumer) {
Optional<Recipient> recipient = provider.getCached(address);
if (recipient.isPresent()) consumer.accept(recipient.get());
}
Recipient(@NonNull Address address, Recipient(@NonNull Address address,
@Nullable Recipient stale, @Nullable Recipient stale,
@NonNull Optional<RecipientDetails> details, @NonNull Optional<RecipientDetails> details,
@NonNull ListenableFutureTask<RecipientDetails> future) @NonNull ListenableFutureTask<RecipientDetails> future)
{ {
this.address = address; this.address = address;
this.fallbackContactPhoto = new TransparentContactPhoto();
this.color = null; this.color = null;
this.resolving = true; this.resolving = true;
if (stale != null) { if (stale != null) {
this.name = stale.name; this.name = stale.name;
this.contactUri = stale.contactUri; this.contactUri = stale.contactUri;
this.contactPhoto = stale.contactPhoto; this.systemContactPhoto = stale.systemContactPhoto;
this.fallbackContactPhoto = stale.fallbackContactPhoto; this.groupAvatarId = stale.groupAvatarId;
this.color = stale.color; this.color = stale.color;
this.customLabel = stale.customLabel; this.customLabel = stale.customLabel;
this.ringtone = stale.ringtone; this.ringtone = stale.ringtone;
@ -133,8 +143,8 @@ public class Recipient implements RecipientModifiedListener {
if (details.isPresent()) { if (details.isPresent()) {
this.name = details.get().name; this.name = details.get().name;
this.contactPhoto = details.get().avatar; this.systemContactPhoto = details.get().systemContactPhoto;
this.fallbackContactPhoto = details.get().fallbackAvatar; this.groupAvatarId = details.get().groupAvatarId;
this.color = details.get().color; this.color = details.get().color;
this.ringtone = details.get().ringtone; this.ringtone = details.get().ringtone;
this.mutedUntil = details.get().mutedUntil; this.mutedUntil = details.get().mutedUntil;
@ -160,8 +170,8 @@ public class Recipient implements RecipientModifiedListener {
synchronized (Recipient.this) { synchronized (Recipient.this) {
Recipient.this.name = result.name; Recipient.this.name = result.name;
Recipient.this.contactUri = result.contactUri; Recipient.this.contactUri = result.contactUri;
Recipient.this.contactPhoto = result.avatar; Recipient.this.systemContactPhoto = result.systemContactPhoto;
Recipient.this.fallbackContactPhoto = result.fallbackAvatar; Recipient.this.groupAvatarId = result.groupAvatarId;
Recipient.this.color = result.color; Recipient.this.color = result.color;
Recipient.this.customLabel = result.customLabel; Recipient.this.customLabel = result.customLabel;
Recipient.this.ringtone = result.ringtone; Recipient.this.ringtone = result.ringtone;
@ -205,8 +215,8 @@ public class Recipient implements RecipientModifiedListener {
this.address = address; this.address = address;
this.contactUri = details.contactUri; this.contactUri = details.contactUri;
this.name = details.name; this.name = details.name;
this.contactPhoto = details.avatar; this.systemContactPhoto = details.systemContactPhoto;
this.fallbackContactPhoto = details.fallbackAvatar; this.groupAvatarId = details.groupAvatarId;
this.color = details.color; this.color = details.color;
this.customLabel = details.customLabel; this.customLabel = details.customLabel;
this.ringtone = details.ringtone; this.ringtone = details.ringtone;
@ -230,6 +240,19 @@ public class Recipient implements RecipientModifiedListener {
return this.contactUri; return this.contactUri;
} }
public void setContactUri(@Nullable Uri contactUri) {
boolean notify = false;
synchronized (this) {
if (!Util.equals(contactUri, this.contactUri)) {
this.contactUri = contactUri;
notify = true;
}
}
if (notify) notifyListeners();
}
public synchronized @Nullable String getName() { public synchronized @Nullable String getName() {
if (this.name == null && isMmsGroupRecipient()) { if (this.name == null && isMmsGroupRecipient()) {
List<String> names = new LinkedList<>(); List<String> names = new LinkedList<>();
@ -276,10 +299,23 @@ public class Recipient implements RecipientModifiedListener {
return address; return address;
} }
public @Nullable String getCustomLabel() { public synchronized @Nullable String getCustomLabel() {
return customLabel; return customLabel;
} }
public void setCustomLabel(@Nullable String customLabel) {
boolean notify = false;
synchronized (this) {
if (!Util.equals(customLabel, this.customLabel)) {
this.customLabel = customLabel;
notify = true;
}
}
if (notify) notifyListeners();
}
public synchronized Optional<Integer> getDefaultSubscriptionId() { public synchronized Optional<Integer> getDefaultSubscriptionId() {
return defaultSubscriptionId; return defaultSubscriptionId;
} }
@ -377,19 +413,43 @@ public class Recipient implements RecipientModifiedListener {
} }
public synchronized @NonNull FallbackContactPhoto getFallbackContactPhoto() { public synchronized @NonNull FallbackContactPhoto getFallbackContactPhoto() {
return fallbackContactPhoto; if (isResolving()) return new TransparentContactPhoto();
else if (isGroupRecipient()) return new ResourceContactPhoto(R.drawable.ic_group_white_24dp, R.drawable.ic_group_large);
else if (!TextUtils.isEmpty(name)) return new GeneratedContactPhoto(name);
else return new GeneratedContactPhoto("#");
} }
public synchronized @Nullable ContactPhoto getContactPhoto() { public synchronized @Nullable ContactPhoto getContactPhoto() {
return contactPhoto; if (isGroupRecipient() && groupAvatarId != null) return new GroupRecordContactPhoto(address, groupAvatarId);
else if (systemContactPhoto != null) return new SystemContactPhoto(address, systemContactPhoto, 0);
else if (profileAvatar != null) return new ProfileContactPhoto(address, profileAvatar);
else return null;
} }
public void setContactPhoto(@NonNull ContactPhoto contactPhoto) { public void setSystemContactPhoto(@Nullable Uri systemContactPhoto) {
boolean notify = false;
synchronized (this) { synchronized (this) {
this.contactPhoto = contactPhoto; if (!Util.equals(systemContactPhoto, this.systemContactPhoto)) {
this.systemContactPhoto = systemContactPhoto;
notify = true;
}
} }
notifyListeners(); if (notify) notifyListeners();
}
public void setGroupAvatarId(@Nullable Long groupAvatarId) {
boolean notify = false;
synchronized (this) {
if (!Util.equals(this.groupAvatarId, groupAvatarId)) {
this.groupAvatarId = groupAvatarId;
notify = true;
}
}
if (notify) notifyListeners();
} }
public synchronized @Nullable Uri getRingtone() { public synchronized @Nullable Uri getRingtone() {

View File

@ -17,33 +17,21 @@
package org.thoughtcrime.securesms.recipients; package org.thoughtcrime.securesms.recipients;
import android.content.Context; import android.content.Context;
import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.PhoneLookup;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.color.MaterialColor; import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.GroupRecordContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.SystemContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.TransparentContactPhoto;
import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord; import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings; import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState; import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState; import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState;
import org.thoughtcrime.securesms.util.LRUCache;
import org.thoughtcrime.securesms.util.ListenableFutureTask; import org.thoughtcrime.securesms.util.ListenableFutureTask;
import org.thoughtcrime.securesms.util.SoftHashMap;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
@ -56,25 +44,17 @@ import java.util.concurrent.ExecutorService;
class RecipientProvider { class RecipientProvider {
@SuppressWarnings("unused")
private static final String TAG = RecipientProvider.class.getSimpleName(); private static final String TAG = RecipientProvider.class.getSimpleName();
private static final RecipientCache recipientCache = new RecipientCache(); private static final RecipientCache recipientCache = new RecipientCache();
private static final ExecutorService asyncRecipientResolver = Util.newSingleThreadedLifoExecutor(); private static final ExecutorService asyncRecipientResolver = Util.newSingleThreadedLifoExecutor();
private static final String[] CALLER_ID_PROJECTION = new String[] {
PhoneLookup.DISPLAY_NAME,
PhoneLookup.LOOKUP_KEY,
PhoneLookup._ID,
PhoneLookup.NUMBER,
PhoneLookup.LABEL,
PhoneLookup.PHOTO_URI
};
private static final Map<String, RecipientDetails> STATIC_DETAILS = new HashMap<String, RecipientDetails>() {{ private static final Map<String, RecipientDetails> STATIC_DETAILS = new HashMap<String, RecipientDetails>() {{
put("262966", new RecipientDetails("Amazon", null, null, null, new ResourceContactPhoto(R.drawable.ic_amazon), false, null, null)); put("262966", new RecipientDetails("Amazon", null, false, null, null));
}}; }};
@NonNull Recipient getRecipient(Context context, Address address, Optional<RecipientSettings> settings, Optional<GroupRecord> groupRecord, boolean asynchronous) { @NonNull Recipient getRecipient(@NonNull Context context, @NonNull Address address, @NonNull Optional<RecipientSettings> settings, @NonNull Optional<GroupRecord> groupRecord, boolean asynchronous) {
Recipient cachedRecipient = recipientCache.get(address); Recipient cachedRecipient = recipientCache.get(address);
if (cachedRecipient != null && (asynchronous || !cachedRecipient.isResolving()) && ((!groupRecord.isPresent() && !settings.isPresent()) || !cachedRecipient.isResolving() || cachedRecipient.getName() != null)) { if (cachedRecipient != null && (asynchronous || !cachedRecipient.isResolving()) && ((!groupRecord.isPresent() && !settings.isPresent()) || !cachedRecipient.isResolving() || cachedRecipient.getName() != null)) {
@ -93,6 +73,10 @@ class RecipientProvider {
return cachedRecipient; return cachedRecipient;
} }
@NonNull Optional<Recipient> getCached(@NonNull Address address) {
return Optional.fromNullable(recipientCache.get(address));
}
private @NonNull Optional<RecipientDetails> createPrefetchedRecipientDetails(@NonNull Context context, @NonNull Address address, private @NonNull Optional<RecipientDetails> createPrefetchedRecipientDetails(@NonNull Context context, @NonNull Address address,
@NonNull Optional<RecipientSettings> settings, @NonNull Optional<RecipientSettings> settings,
@NonNull Optional<GroupRecord> groupRecord) @NonNull Optional<GroupRecord> groupRecord)
@ -100,7 +84,7 @@ class RecipientProvider {
if (address.isGroup() && settings.isPresent() && groupRecord.isPresent()) { if (address.isGroup() && settings.isPresent() && groupRecord.isPresent()) {
return Optional.of(getGroupRecipientDetails(context, address, groupRecord, settings, true)); return Optional.of(getGroupRecipientDetails(context, address, groupRecord, settings, true));
} else if (!address.isGroup() && settings.isPresent()) { } else if (!address.isGroup() && settings.isPresent()) {
return Optional.of(new RecipientDetails(null, null, null, null, new TransparentContactPhoto(), !TextUtils.isEmpty(settings.get().getSystemDisplayName()), settings.get(), null)); return Optional.of(new RecipientDetails(null, null, !TextUtils.isEmpty(settings.get().getSystemDisplayName()), settings.get(), null));
} }
return Optional.absent(); return Optional.absent();
@ -108,12 +92,7 @@ class RecipientProvider {
private @NonNull ListenableFutureTask<RecipientDetails> getRecipientDetailsAsync(final Context context, final @NonNull Address address, final @NonNull Optional<RecipientSettings> settings, final @NonNull Optional<GroupRecord> groupRecord) private @NonNull ListenableFutureTask<RecipientDetails> getRecipientDetailsAsync(final Context context, final @NonNull Address address, final @NonNull Optional<RecipientSettings> settings, final @NonNull Optional<GroupRecord> groupRecord)
{ {
Callable<RecipientDetails> task = new Callable<RecipientDetails>() { Callable<RecipientDetails> task = () -> getRecipientDetailsSync(context, address, settings, groupRecord, true);
@Override
public RecipientDetails call() throws Exception {
return getRecipientDetailsSync(context, address, settings, groupRecord, true);
}
};
ListenableFutureTask<RecipientDetails> future = new ListenableFutureTask<>(task); ListenableFutureTask<RecipientDetails> future = new ListenableFutureTask<>(task);
asyncRecipientResolver.submit(future); asyncRecipientResolver.submit(future);
@ -126,53 +105,18 @@ class RecipientProvider {
} }
private @NonNull RecipientDetails getIndividualRecipientDetails(Context context, @NonNull Address address, Optional<RecipientSettings> settings) { private @NonNull RecipientDetails getIndividualRecipientDetails(Context context, @NonNull Address address, Optional<RecipientSettings> settings) {
ContactPhoto contactPhoto = null;
FallbackContactPhoto fallbackContactPhoto = new GeneratedContactPhoto("#");
if (!settings.isPresent()) { if (!settings.isPresent()) {
settings = DatabaseFactory.getRecipientDatabase(context).getRecipientSettings(address); settings = DatabaseFactory.getRecipientDatabase(context).getRecipientSettings(address);
} }
if (settings.isPresent() && !TextUtils.isEmpty(settings.get().getProfileAvatar())) { if (!settings.isPresent() && STATIC_DETAILS.containsKey(address.serialize())) {
contactPhoto = new ProfileContactPhoto(address, settings.get().getProfileAvatar()); return STATIC_DETAILS.get(address.serialize());
} else {
return new RecipientDetails(null, null, false, settings.orNull(), null);
} }
if (address.isPhone() && !TextUtils.isEmpty(address.toPhoneString())) {
Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(address.toPhoneString()));
try (Cursor cursor = context.getContentResolver().query(uri, CALLER_ID_PROJECTION, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
final String resultNumber = cursor.getString(3);
if (resultNumber != null) {
Uri contactUri = Contacts.getLookupUri(cursor.getLong(2), cursor.getString(1));
String name = resultNumber.equals(cursor.getString(0)) ? null : cursor.getString(0);
String photoUri = cursor.getString(5);
if (!TextUtils.isEmpty(photoUri)) {
contactPhoto = new SystemContactPhoto(address, Uri.parse(photoUri), 0);
}
if (!TextUtils.isEmpty(name)) {
fallbackContactPhoto = new GeneratedContactPhoto(name);
}
return new RecipientDetails(cursor.getString(0), cursor.getString(4), contactUri, contactPhoto, fallbackContactPhoto, true, settings.orNull(), null);
} else {
Log.w(TAG, "resultNumber is null");
}
}
} catch (SecurityException se) {
Log.w(TAG, se);
}
}
if (STATIC_DETAILS.containsKey(address.serialize())) return STATIC_DETAILS.get(address.serialize());
else return new RecipientDetails(null, null, null, contactPhoto, fallbackContactPhoto, false, settings.orNull(), null);
} }
private @NonNull RecipientDetails getGroupRecipientDetails(Context context, Address groupId, Optional<GroupRecord> groupRecord, Optional<RecipientSettings> settings, boolean asynchronous) { private @NonNull RecipientDetails getGroupRecipientDetails(Context context, Address groupId, Optional<GroupRecord> groupRecord, Optional<RecipientSettings> settings, boolean asynchronous) {
ContactPhoto contactPhoto = null;
FallbackContactPhoto fallbackContactPhoto = new ResourceContactPhoto(R.drawable.ic_group_white_24dp, R.drawable.ic_group_large);
if (!groupRecord.isPresent()) { if (!groupRecord.isPresent()) {
groupRecord = DatabaseFactory.getGroupDatabase(context).getGroup(groupId.toGroupString()); groupRecord = DatabaseFactory.getGroupDatabase(context).getGroup(groupId.toGroupString());
@ -192,65 +136,59 @@ class RecipientProvider {
} }
if (!groupId.isMmsGroup() && title == null) { if (!groupId.isMmsGroup() && title == null) {
title = context.getString(R.string.RecipientProvider_unnamed_group);; title = context.getString(R.string.RecipientProvider_unnamed_group);
} }
if (groupRecord.get().getAvatar() != null) { return new RecipientDetails(title, groupRecord.get().getAvatarId(), false, settings.orNull(), members);
contactPhoto = new GroupRecordContactPhoto(groupId, groupRecord.get().getAvatarId());
}
return new RecipientDetails(title, null, null, contactPhoto, fallbackContactPhoto, false, settings.orNull(), members);
} }
return new RecipientDetails(context.getString(R.string.RecipientProvider_unnamed_group), null, null, contactPhoto, fallbackContactPhoto, false, settings.orNull(), null); return new RecipientDetails(context.getString(R.string.RecipientProvider_unnamed_group), null, false, settings.orNull(), null);
} }
static class RecipientDetails { static class RecipientDetails {
@Nullable public final String name; @Nullable final String name;
@Nullable public final String customLabel; @Nullable final String customLabel;
@Nullable public final ContactPhoto avatar; @Nullable final Uri systemContactPhoto;
@NonNull public final FallbackContactPhoto fallbackAvatar; @Nullable final Uri contactUri;
@Nullable public final Uri contactUri; @Nullable final Long groupAvatarId;
@Nullable public final MaterialColor color; @Nullable final MaterialColor color;
@Nullable public final Uri ringtone; @Nullable final Uri ringtone;
public final long mutedUntil; final long mutedUntil;
@Nullable public final VibrateState vibrateState; @Nullable final VibrateState vibrateState;
public final boolean blocked; final boolean blocked;
public final int expireMessages; final int expireMessages;
@NonNull public final List<Recipient> participants; @NonNull final List<Recipient> participants;
@Nullable public final String profileName; @Nullable final String profileName;
public final boolean seenInviteReminder; final boolean seenInviteReminder;
public final Optional<Integer> defaultSubscriptionId; final Optional<Integer> defaultSubscriptionId;
@NonNull public final RegisteredState registered; @NonNull final RegisteredState registered;
@Nullable public final byte[] profileKey; @Nullable final byte[] profileKey;
@Nullable public final String profileAvatar; @Nullable final String profileAvatar;
public final boolean profileSharing; final boolean profileSharing;
public final boolean systemContact; final boolean systemContact;
public RecipientDetails(@Nullable String name, @Nullable String customLabel, RecipientDetails(@Nullable String name, @Nullable Long groupAvatarId,
@Nullable Uri contactUri, @Nullable ContactPhoto avatar, boolean systemContact, @Nullable RecipientSettings settings,
@NonNull FallbackContactPhoto fallbackAvatar, @Nullable List<Recipient> participants)
boolean systemContact, @Nullable RecipientSettings settings,
@Nullable List<Recipient> participants)
{ {
this.customLabel = customLabel; this.groupAvatarId = groupAvatarId;
this.avatar = avatar; this.systemContactPhoto = settings != null ? Util.uri(settings.getSystemContactPhotoUri()) : null;
this.fallbackAvatar = fallbackAvatar; this.customLabel = settings != null ? settings.getSystemPhoneLabel() : null;
this.contactUri = contactUri; this.contactUri = settings != null ? Util.uri(settings.getSystemContactUri()) : null;
this.color = settings != null ? settings.getColor() : null; this.color = settings != null ? settings.getColor() : null;
this.ringtone = settings != null ? settings.getRingtone() : null; this.ringtone = settings != null ? settings.getRingtone() : null;
this.mutedUntil = settings != null ? settings.getMuteUntil() : 0; this.mutedUntil = settings != null ? settings.getMuteUntil() : 0;
this.vibrateState = settings != null ? settings.getVibrateState() : null; this.vibrateState = settings != null ? settings.getVibrateState() : null;
this.blocked = settings != null && settings.isBlocked(); this.blocked = settings != null && settings.isBlocked();
this.expireMessages = settings != null ? settings.getExpireMessages() : 0; this.expireMessages = settings != null ? settings.getExpireMessages() : 0;
this.participants = participants == null ? new LinkedList<Recipient>() : participants; this.participants = participants == null ? new LinkedList<>() : participants;
this.profileName = settings != null ? settings.getProfileName() : null; this.profileName = settings != null ? settings.getProfileName() : null;
this.seenInviteReminder = settings != null && settings.hasSeenInviteReminder(); this.seenInviteReminder = settings != null && settings.hasSeenInviteReminder();
this.defaultSubscriptionId = settings != null ? settings.getDefaultSubscriptionId() : Optional.absent(); this.defaultSubscriptionId = settings != null ? settings.getDefaultSubscriptionId() : Optional.absent();
this.registered = settings != null ? settings.getRegistered() : RegisteredState.UNKNOWN; this.registered = settings != null ? settings.getRegistered() : RegisteredState.UNKNOWN;
this.profileKey = settings != null ? settings.getProfileKey() : null; this.profileKey = settings != null ? settings.getProfileKey() : null;
this.profileAvatar = settings != null ? settings.getProfileAvatar() : null; this.profileAvatar = settings != null ? settings.getProfileAvatar() : null;
this.profileSharing = settings != null && settings.isProfileSharing(); this.profileSharing = settings != null && settings.isProfileSharing();
this.systemContact = systemContact; this.systemContact = systemContact;
if (name == null && settings != null) this.name = settings.getSystemDisplayName(); if (name == null && settings != null) this.name = settings.getSystemDisplayName();
@ -260,7 +198,7 @@ class RecipientProvider {
private static class RecipientCache { private static class RecipientCache {
private final Map<Address,Recipient> cache = new LRUCache<>(1000); private final Map<Address,Recipient> cache = new SoftHashMap<>(1000);
public synchronized Recipient get(Address address) { public synchronized Recipient get(Address address) {
return cache.get(address); return cache.get(address);

View File

@ -7,6 +7,7 @@ import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.OperationApplicationException; import android.content.OperationApplicationException;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri;
import android.os.RemoteException; import android.os.RemoteException;
import android.provider.ContactsContract; import android.provider.ContactsContract;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
@ -78,35 +79,34 @@ public class DirectoryHelper {
} }
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context); RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
Stream<String> eligibleRecipientDatabaseContactNumbers = Stream.of(recipientDatabase.getAllRecipients()).map(recipient -> recipient.getAddress().serialize()); 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); Stream<String> eligibleSystemDatabaseContactNumbers = Stream.of(ContactAccessor.getInstance().getAllContactsWithNumbers(context)).map(Address::serialize);
Set<String> eligibleContactNumbers = Stream.concat(eligibleRecipientDatabaseContactNumbers, eligibleSystemDatabaseContactNumbers).collect(Collectors.toSet()); Set<String> eligibleContactNumbers = Stream.concat(eligibleRecipientDatabaseContactNumbers, eligibleSystemDatabaseContactNumbers).collect(Collectors.toSet());
List<ContactTokenDetails> activeTokens = accountManager.getContacts(eligibleContactNumbers); List<ContactTokenDetails> activeTokens = accountManager.getContacts(eligibleContactNumbers);
if (activeTokens != null) { if (activeTokens != null) {
List<Recipient> activeRecipients = new LinkedList<>(); List<Address> activeAddresses = new LinkedList<>();
List<Recipient> inactiveRecipients = new LinkedList<>(); List<Address> inactiveAddresses = new LinkedList<>();
Set<String> inactiveContactNumbers = new HashSet<>(eligibleContactNumbers); Set<String> inactiveContactNumbers = new HashSet<>(eligibleContactNumbers);
for (ContactTokenDetails activeToken : activeTokens) { for (ContactTokenDetails activeToken : activeTokens) {
activeRecipients.add(Recipient.from(context, Address.fromSerialized(activeToken.getNumber()), true)); activeAddresses.add(Address.fromSerialized(activeToken.getNumber()));
inactiveContactNumbers.remove(activeToken.getNumber()); inactiveContactNumbers.remove(activeToken.getNumber());
} }
for (String inactiveContactNumber : inactiveContactNumbers) { for (String inactiveContactNumber : inactiveContactNumbers) {
inactiveRecipients.add(Recipient.from(context, Address.fromSerialized(inactiveContactNumber), true)); inactiveAddresses.add(Address.fromSerialized(inactiveContactNumber));
} }
Set<Address> currentActiveAddresses = new HashSet<>(recipientDatabase.getRegistered()); Set<Address> currentActiveAddresses = new HashSet<>(recipientDatabase.getRegistered());
List<Address> newlyActiveAddresses = Stream.of(activeRecipients) List<Address> newlyActiveAddresses = Stream.of(activeAddresses)
.map(Recipient::getAddress)
.filter(address -> !currentActiveAddresses.contains(address)) .filter(address -> !currentActiveAddresses.contains(address))
.toList(); .toList();
recipientDatabase.setRegistered(activeRecipients, inactiveRecipients); recipientDatabase.setRegistered(activeAddresses, inactiveAddresses);
updateContactsDatabase(context, Stream.of(activeRecipients).map(Recipient::getAddress).toList(), true); updateContactsDatabase(context, activeAddresses, true);
if (TextSecurePreferences.hasSuccessfullyRetrievedDirectory(context)) { if (TextSecurePreferences.hasSuccessfullyRetrievedDirectory(context)) {
return newlyActiveAddresses; return newlyActiveAddresses;
@ -160,18 +160,21 @@ public class DirectoryHelper {
DatabaseFactory.getContactsDatabase(context).setRegisteredUsers(account.get().getAccount(), activeAddresses, removeMissing); DatabaseFactory.getContactsDatabase(context).setRegisteredUsers(account.get().getAccount(), activeAddresses, removeMissing);
Cursor cursor = ContactAccessor.getInstance().getAllSystemContacts(context); Cursor cursor = ContactAccessor.getInstance().getAllSystemContacts(context);
RecipientDatabase.BulkOperationsHandle handle = DatabaseFactory.getRecipientDatabase(context).resetAllDisplayNames(); RecipientDatabase.BulkOperationsHandle handle = DatabaseFactory.getRecipientDatabase(context).resetAllSystemContactInfo();
try { try {
while (cursor != null && cursor.moveToNext()) { while (cursor != null && cursor.moveToNext()) {
String number = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER)); String number = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER));
if (!TextUtils.isEmpty(number)) { if (!TextUtils.isEmpty(number)) {
Address address = Address.fromExternal(context, number); Address address = Address.fromExternal(context, number);
Recipient recipient = Recipient.from(context, address, true); String displayName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
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.setDisplayName(recipient, displayName); handle.setSystemContactInfo(address, displayName, contactPhotoUri, contactLabel, contactUri.toString());
} }
} }
} finally { } finally {
@ -247,6 +250,7 @@ public class DirectoryHelper {
this.account = account; this.account = account;
} }
@SuppressWarnings("unused")
public boolean isFresh() { public boolean isFresh() {
return fresh; return fresh;
} }

View File

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.util; package org.thoughtcrime.securesms.util;
import org.thoughtcrime.securesms.util.deque.LinkedBlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
public class LinkedBlockingLifoQueue<E> extends LinkedBlockingDeque<E> { public class LinkedBlockingLifoQueue<E> extends LinkedBlockingDeque<E> {
@Override @Override

View File

@ -0,0 +1,319 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.thoughtcrime.securesms.util;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.locks.ReentrantLock;
/**
* A <code><em>Soft</em>HashMap</code> is a memory-constrained map that stores its <em>values</em> in
* {@link SoftReference SoftReference}s. (Contrast this with the JDK's
* {@link WeakHashMap WeakHashMap}, which uses weak references for its <em>keys</em>, which is of little value if you
* want the cache to auto-resize itself based on memory constraints).
* <p/>
* Having the values wrapped by soft references allows the cache to automatically reduce its size based on memory
* limitations and garbage collection. This ensures that the cache will not cause memory leaks by holding strong
* references to all of its values.
* <p/>
* This class is a generics-enabled Map based on initial ideas from Heinz Kabutz's and Sydney Redelinghuys's
* <a href="http://www.javaspecialists.eu/archive/Issue015.html">publicly posted version (with their approval)</a>, with
* continued modifications.
* <p/>
* This implementation is thread-safe and usable in concurrent environments.
*
* @since 1.0
*/
public class SoftHashMap<K, V> implements Map<K, V> {
/**
* The default value of the RETENTION_SIZE attribute, equal to 100.
*/
private static final int DEFAULT_RETENTION_SIZE = 100;
/**
* The internal HashMap that will hold the SoftReference.
*/
private final Map<K, SoftValue<V, K>> map;
/**
* The number of strong references to hold internally, that is, the number of instances to prevent
* from being garbage collected automatically (unlike other soft references).
*/
private final int RETENTION_SIZE;
/**
* The FIFO list of strong references (not to be garbage collected), order of last access.
*/
private final Queue<V> strongReferences; //guarded by 'strongReferencesLock'
private final ReentrantLock strongReferencesLock;
/**
* Reference queue for cleared SoftReference objects.
*/
private final ReferenceQueue<? super V> queue;
/**
* Creates a new SoftHashMap with a default retention size size of
* {@link #DEFAULT_RETENTION_SIZE DEFAULT_RETENTION_SIZE} (100 entries).
*
* @see #SoftHashMap(int)
*/
public SoftHashMap() {
this(DEFAULT_RETENTION_SIZE);
}
/**
* Creates a new SoftHashMap with the specified retention size.
* <p/>
* The retention size (n) is the total number of most recent entries in the map that will be strongly referenced
* (ie 'retained') to prevent them from being eagerly garbage collected. That is, the point of a SoftHashMap is to
* allow the garbage collector to remove as many entries from this map as it desires, but there will always be (n)
* elements retained after a GC due to the strong references.
* <p/>
* Note that in a highly concurrent environments the exact total number of strong references may differ slightly
* than the actual <code>retentionSize</code> value. This number is intended to be a best-effort retention low
* water mark.
*
* @param retentionSize the total number of most recent entries in the map that will be strongly referenced
* (retained), preventing them from being eagerly garbage collected by the JVM.
*/
@SuppressWarnings({"unchecked"})
public SoftHashMap(int retentionSize) {
super();
RETENTION_SIZE = Math.max(0, retentionSize);
queue = new ReferenceQueue<V>();
strongReferencesLock = new ReentrantLock();
map = new ConcurrentHashMap<K, SoftValue<V, K>>();
strongReferences = new ConcurrentLinkedQueue<V>();
}
/**
* Creates a {@code SoftHashMap} backed by the specified {@code source}, with a default retention
* size of {@link #DEFAULT_RETENTION_SIZE DEFAULT_RETENTION_SIZE} (100 entries).
*
* @param source the backing map to populate this {@code SoftHashMap}
* @see #SoftHashMap(Map,int)
*/
public SoftHashMap(Map<K, V> source) {
this(DEFAULT_RETENTION_SIZE);
putAll(source);
}
/**
* Creates a {@code SoftHashMap} backed by the specified {@code source}, with the specified retention size.
* <p/>
* The retention size (n) is the total number of most recent entries in the map that will be strongly referenced
* (ie 'retained') to prevent them from being eagerly garbage collected. That is, the point of a SoftHashMap is to
* allow the garbage collector to remove as many entries from this map as it desires, but there will always be (n)
* elements retained after a GC due to the strong references.
* <p/>
* Note that in a highly concurrent environments the exact total number of strong references may differ slightly
* than the actual <code>retentionSize</code> value. This number is intended to be a best-effort retention low
* water mark.
*
* @param source the backing map to populate this {@code SoftHashMap}
* @param retentionSize the total number of most recent entries in the map that will be strongly referenced
* (retained), preventing them from being eagerly garbage collected by the JVM.
*/
public SoftHashMap(Map<K, V> source, int retentionSize) {
this(retentionSize);
putAll(source);
}
public V get(Object key) {
processQueue();
V result = null;
SoftValue<V, K> value = map.get(key);
if (value != null) {
//unwrap the 'real' value from the SoftReference
result = value.get();
if (result == null) {
//The wrapped value was garbage collected, so remove this entry from the backing map:
//noinspection SuspiciousMethodCalls
map.remove(key);
} else {
//Add this value to the beginning of the strong reference queue (FIFO).
addToStrongReferences(result);
}
}
return result;
}
private void addToStrongReferences(V result) {
strongReferencesLock.lock();
try {
strongReferences.add(result);
trimStrongReferencesIfNecessary();
} finally {
strongReferencesLock.unlock();
}
}
//Guarded by the strongReferencesLock in the addToStrongReferences method
private void trimStrongReferencesIfNecessary() {
//trim the strong ref queue if necessary:
while (strongReferences.size() > RETENTION_SIZE) {
strongReferences.poll();
}
}
/**
* Traverses the ReferenceQueue and removes garbage-collected SoftValue objects from the backing map
* by looking them up using the SoftValue.key data member.
*/
private void processQueue() {
SoftValue sv;
while ((sv = (SoftValue) queue.poll()) != null) {
//noinspection SuspiciousMethodCalls
map.remove(sv.key); // we can access private data!
}
}
public boolean isEmpty() {
processQueue();
return map.isEmpty();
}
public boolean containsKey(Object key) {
processQueue();
return map.containsKey(key);
}
public boolean containsValue(Object value) {
processQueue();
Collection values = values();
return values != null && values.contains(value);
}
public void putAll(Map<? extends K, ? extends V> m) {
if (m == null || m.isEmpty()) {
processQueue();
return;
}
for (Map.Entry<? extends K, ? extends V> entry : m.entrySet()) {
put(entry.getKey(), entry.getValue());
}
}
public Set<K> keySet() {
processQueue();
return map.keySet();
}
public Collection<V> values() {
processQueue();
Collection<K> keys = map.keySet();
if (keys.isEmpty()) {
//noinspection unchecked
return Collections.EMPTY_SET;
}
Collection<V> values = new ArrayList<V>(keys.size());
for (K key : keys) {
V v = get(key);
if (v != null) {
values.add(v);
}
}
return values;
}
/**
* Creates a new entry, but wraps the value in a SoftValue instance to enable auto garbage collection.
*/
public V put(K key, V value) {
processQueue(); // throw out garbage collected values first
SoftValue<V, K> sv = new SoftValue<V, K>(value, key, queue);
SoftValue<V, K> previous = map.put(key, sv);
addToStrongReferences(value);
return previous != null ? previous.get() : null;
}
public V remove(Object key) {
processQueue(); // throw out garbage collected values first
SoftValue<V, K> raw = map.remove(key);
return raw != null ? raw.get() : null;
}
public void clear() {
strongReferencesLock.lock();
try {
strongReferences.clear();
} finally {
strongReferencesLock.unlock();
}
processQueue(); // throw out garbage collected values
map.clear();
}
public int size() {
processQueue(); // throw out garbage collected values first
return map.size();
}
public Set<Map.Entry<K, V>> entrySet() {
processQueue(); // throw out garbage collected values first
Collection<K> keys = map.keySet();
if (keys.isEmpty()) {
//noinspection unchecked
return Collections.EMPTY_SET;
}
Map<K, V> kvPairs = new HashMap<K, V>(keys.size());
for (K key : keys) {
V v = get(key);
if (v != null) {
kvPairs.put(key, v);
}
}
return kvPairs.entrySet();
}
/**
* We define our own subclass of SoftReference which contains
* not only the value but also the key to make it easier to find
* the entry in the HashMap after it's been garbage collected.
*/
private static class SoftValue<V, K> extends SoftReference<V> {
private final K key;
/**
* Constructs a new instance, wrapping the value, key, and queue, as
* required by the superclass.
*
* @param value the map value
* @param key the map key
* @param queue the soft reference queue to poll to determine if the entry had been reaped by the GC.
*/
private SoftValue(V value, K key, ReferenceQueue<? super V> queue) {
super(value, queue);
this.key = key;
}
}
}

View File

@ -24,6 +24,7 @@ import android.content.ClipboardManager;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.graphics.Typeface; import android.graphics.Typeface;
import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Build.VERSION; import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES; import android.os.Build.VERSION_CODES;
@ -422,6 +423,11 @@ public class Util {
return Arrays.hashCode(objects); return Arrays.hashCode(objects);
} }
public static @Nullable Uri uri(@Nullable String uri) {
if (uri == null) return null;
else return Uri.parse(uri);
}
@TargetApi(VERSION_CODES.KITKAT) @TargetApi(VERSION_CODES.KITKAT)
public static boolean isLowMemory(Context context) { public static boolean isLowMemory(Context context) {
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);

View File

@ -1,616 +0,0 @@
/*
* Written by Doug Lea with assistance from members of JCP JSR-166
* Expert Group and released to the public domain, as explained at
* http://creativecommons.org/licenses/publicdomain
*/
package org.thoughtcrime.securesms.util.deque;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* A {@link Deque} that additionally supports blocking operations that wait
* for the deque to become non-empty when retrieving an element, and wait for
* space to become available in the deque when storing an element.
*
* <p><tt>BlockingDeque</tt> methods come in four forms, with different ways
* of handling operations that cannot be satisfied immediately, but may be
* satisfied at some point in the future:
* one throws an exception, the second returns a special value (either
* <tt>null</tt> or <tt>false</tt>, depending on the operation), the third
* blocks the current thread indefinitely until the operation can succeed,
* and the fourth blocks for only a given maximum time limit before giving
* up. These methods are summarized in the following table:
*
* <p>
* <table BORDER CELLPADDING=3 CELLSPACING=1>
* <tr>
* <td ALIGN=CENTER COLSPAN = 5> <b>First Element (Head)</b></td>
* </tr>
* <tr>
* <td></td>
* <td ALIGN=CENTER><em>Throws exception</em></td>
* <td ALIGN=CENTER><em>Special value</em></td>
* <td ALIGN=CENTER><em>Blocks</em></td>
* <td ALIGN=CENTER><em>Times out</em></td>
* </tr>
* <tr>
* <td><b>Insert</b></td>
* <td>{@link #addFirst addFirst(e)}</td>
* <td>{@link #offerFirst offerFirst(e)}</td>
* <td>{@link #putFirst putFirst(e)}</td>
* <td>{@link #offerFirst offerFirst(e, time, unit)}</td>
* </tr>
* <tr>
* <td><b>Remove</b></td>
* <td>{@link #removeFirst removeFirst()}</td>
* <td>{@link #pollFirst pollFirst()}</td>
* <td>{@link #takeFirst takeFirst()}</td>
* <td>{@link #pollFirst(long, TimeUnit) pollFirst(time, unit)}</td>
* </tr>
* <tr>
* <td><b>Examine</b></td>
* <td>{@link #getFirst getFirst()}</td>
* <td>{@link #peekFirst peekFirst()}</td>
* <td><em>not applicable</em></td>
* <td><em>not applicable</em></td>
* </tr>
* <tr>
* <td ALIGN=CENTER COLSPAN = 5> <b>Last Element (Tail)</b></td>
* </tr>
* <tr>
* <td></td>
* <td ALIGN=CENTER><em>Throws exception</em></td>
* <td ALIGN=CENTER><em>Special value</em></td>
* <td ALIGN=CENTER><em>Blocks</em></td>
* <td ALIGN=CENTER><em>Times out</em></td>
* </tr>
* <tr>
* <td><b>Insert</b></td>
* <td>{@link #addLast addLast(e)}</td>
* <td>{@link #offerLast offerLast(e)}</td>
* <td>{@link #putLast putLast(e)}</td>
* <td>{@link #offerLast offerLast(e, time, unit)}</td>
* </tr>
* <tr>
* <td><b>Remove</b></td>
* <td>{@link #removeLast() removeLast()}</td>
* <td>{@link #pollLast() pollLast()}</td>
* <td>{@link #takeLast takeLast()}</td>
* <td>{@link #pollLast(long, TimeUnit) pollLast(time, unit)}</td>
* </tr>
* <tr>
* <td><b>Examine</b></td>
* <td>{@link #getLast getLast()}</td>
* <td>{@link #peekLast peekLast()}</td>
* <td><em>not applicable</em></td>
* <td><em>not applicable</em></td>
* </tr>
* </table>
*
* <p>Like any {@link BlockingQueue}, a <tt>BlockingDeque</tt> is thread safe,
* does not permit null elements, and may (or may not) be
* capacity-constrained.
*
* <p>A <tt>BlockingDeque</tt> implementation may be used directly as a FIFO
* <tt>BlockingQueue</tt>. The methods inherited from the
* <tt>BlockingQueue</tt> interface are precisely equivalent to
* <tt>BlockingDeque</tt> methods as indicated in the following table:
*
* <p>
* <table BORDER CELLPADDING=3 CELLSPACING=1>
* <tr>
* <td ALIGN=CENTER> <b><tt>BlockingQueue</tt> Method</b></td>
* <td ALIGN=CENTER> <b>Equivalent <tt>BlockingDeque</tt> Method</b></td>
* </tr>
* <tr>
* <td ALIGN=CENTER COLSPAN = 2> <b>Insert</b></td>
* </tr>
* <tr>
* <td>{@link #add add(e)}</td>
* <td>{@link #addLast addLast(e)}</td>
* </tr>
* <tr>
* <td>{@link #offer offer(e)}</td>
* <td>{@link #offerLast offerLast(e)}</td>
* </tr>
* <tr>
* <td>{@link #put put(e)}</td>
* <td>{@link #putLast putLast(e)}</td>
* </tr>
* <tr>
* <td>{@link #offer offer(e, time, unit)}</td>
* <td>{@link #offerLast offerLast(e, time, unit)}</td>
* </tr>
* <tr>
* <td ALIGN=CENTER COLSPAN = 2> <b>Remove</b></td>
* </tr>
* <tr>
* <td>{@link #remove() remove()}</td>
* <td>{@link #removeFirst() removeFirst()}</td>
* </tr>
* <tr>
* <td>{@link #poll() poll()}</td>
* <td>{@link #pollFirst() pollFirst()}</td>
* </tr>
* <tr>
* <td>{@link #take() take()}</td>
* <td>{@link #takeFirst() takeFirst()}</td>
* </tr>
* <tr>
* <td>{@link #poll(long, TimeUnit) poll(time, unit)}</td>
* <td>{@link #pollFirst(long, TimeUnit) pollFirst(time, unit)}</td>
* </tr>
* <tr>
* <td ALIGN=CENTER COLSPAN = 2> <b>Examine</b></td>
* </tr>
* <tr>
* <td>{@link #element() element()}</td>
* <td>{@link #getFirst() getFirst()}</td>
* </tr>
* <tr>
* <td>{@link #peek() peek()}</td>
* <td>{@link #peekFirst() peekFirst()}</td>
* </tr>
* </table>
*
* <p>Memory consistency effects: As with other concurrent
* collections, actions in a thread prior to placing an object into a
* {@code BlockingDeque}
* <a href="package-summary.html#MemoryVisibility"><i>happen-before</i></a>
* actions subsequent to the access or removal of that element from
* the {@code BlockingDeque} in another thread.
*
* <p>This interface is a member of the
* <a href="{@docRoot}/../technotes/guides/collections/index.html">
* Java Collections Framework</a>.
*
* @since 1.6
* @author Doug Lea
* @param <E> the type of elements held in this collection
*/
public interface BlockingDeque<E> extends BlockingQueue<E>, Deque<E> {
/*
* We have "diamond" multiple interface inheritance here, and that
* introduces ambiguities. Methods might end up with different
* specs depending on the branch chosen by javadoc. Thus a lot of
* methods specs here are copied from superinterfaces.
*/
/**
* Inserts the specified element at the front of this deque if it is
* possible to do so immediately without violating capacity restrictions,
* throwing an <tt>IllegalStateException</tt> if no space is currently
* available. When using a capacity-restricted deque, it is generally
* preferable to use {@link #offerFirst offerFirst}.
*
* @param e the element to add
* @throws IllegalStateException {@inheritDoc}
* @throws ClassCastException {@inheritDoc}
* @throws NullPointerException if the specified element is null
* @throws IllegalArgumentException {@inheritDoc}
*/
void addFirst(E e);
/**
* Inserts the specified element at the end of this deque if it is
* possible to do so immediately without violating capacity restrictions,
* throwing an <tt>IllegalStateException</tt> if no space is currently
* available. When using a capacity-restricted deque, it is generally
* preferable to use {@link #offerLast offerLast}.
*
* @param e the element to add
* @throws IllegalStateException {@inheritDoc}
* @throws ClassCastException {@inheritDoc}
* @throws NullPointerException if the specified element is null
* @throws IllegalArgumentException {@inheritDoc}
*/
void addLast(E e);
/**
* Inserts the specified element at the front of this deque if it is
* possible to do so immediately without violating capacity restrictions,
* returning <tt>true</tt> upon success and <tt>false</tt> if no space is
* currently available.
* When using a capacity-restricted deque, this method is generally
* preferable to the {@link #addFirst addFirst} method, which can
* fail to insert an element only by throwing an exception.
*
* @param e the element to add
* @throws ClassCastException {@inheritDoc}
* @throws NullPointerException if the specified element is null
* @throws IllegalArgumentException {@inheritDoc}
*/
boolean offerFirst(E e);
/**
* Inserts the specified element at the end of this deque if it is
* possible to do so immediately without violating capacity restrictions,
* returning <tt>true</tt> upon success and <tt>false</tt> if no space is
* currently available.
* When using a capacity-restricted deque, this method is generally
* preferable to the {@link #addLast addLast} method, which can
* fail to insert an element only by throwing an exception.
*
* @param e the element to add
* @throws ClassCastException {@inheritDoc}
* @throws NullPointerException if the specified element is null
* @throws IllegalArgumentException {@inheritDoc}
*/
boolean offerLast(E e);
/**
* Inserts the specified element at the front of this deque,
* waiting if necessary for space to become available.
*
* @param e the element to add
* @throws InterruptedException if interrupted while waiting
* @throws ClassCastException if the class of the specified element
* prevents it from being added to this deque
* @throws NullPointerException if the specified element is null
* @throws IllegalArgumentException if some property of the specified
* element prevents it from being added to this deque
*/
void putFirst(E e) throws InterruptedException;
/**
* Inserts the specified element at the end of this deque,
* waiting if necessary for space to become available.
*
* @param e the element to add
* @throws InterruptedException if interrupted while waiting
* @throws ClassCastException if the class of the specified element
* prevents it from being added to this deque
* @throws NullPointerException if the specified element is null
* @throws IllegalArgumentException if some property of the specified
* element prevents it from being added to this deque
*/
void putLast(E e) throws InterruptedException;
/**
* Inserts the specified element at the front of this deque,
* waiting up to the specified wait time if necessary for space to
* become available.
*
* @param e the element to add
* @param timeout how long to wait before giving up, in units of
* <tt>unit</tt>
* @param unit a <tt>TimeUnit</tt> determining how to interpret the
* <tt>timeout</tt> parameter
* @return <tt>true</tt> if successful, or <tt>false</tt> if
* the specified waiting time elapses before space is available
* @throws InterruptedException if interrupted while waiting
* @throws ClassCastException if the class of the specified element
* prevents it from being added to this deque
* @throws NullPointerException if the specified element is null
* @throws IllegalArgumentException if some property of the specified
* element prevents it from being added to this deque
*/
boolean offerFirst(E e, long timeout, TimeUnit unit)
throws InterruptedException;
/**
* Inserts the specified element at the end of this deque,
* waiting up to the specified wait time if necessary for space to
* become available.
*
* @param e the element to add
* @param timeout how long to wait before giving up, in units of
* <tt>unit</tt>
* @param unit a <tt>TimeUnit</tt> determining how to interpret the
* <tt>timeout</tt> parameter
* @return <tt>true</tt> if successful, or <tt>false</tt> if
* the specified waiting time elapses before space is available
* @throws InterruptedException if interrupted while waiting
* @throws ClassCastException if the class of the specified element
* prevents it from being added to this deque
* @throws NullPointerException if the specified element is null
* @throws IllegalArgumentException if some property of the specified
* element prevents it from being added to this deque
*/
boolean offerLast(E e, long timeout, TimeUnit unit)
throws InterruptedException;
/**
* Retrieves and removes the first element of this deque, waiting
* if necessary until an element becomes available.
*
* @return the head of this deque
* @throws InterruptedException if interrupted while waiting
*/
E takeFirst() throws InterruptedException;
/**
* Retrieves and removes the last element of this deque, waiting
* if necessary until an element becomes available.
*
* @return the tail of this deque
* @throws InterruptedException if interrupted while waiting
*/
E takeLast() throws InterruptedException;
/**
* Retrieves and removes the first element of this deque, waiting
* up to the specified wait time if necessary for an element to
* become available.
*
* @param timeout how long to wait before giving up, in units of
* <tt>unit</tt>
* @param unit a <tt>TimeUnit</tt> determining how to interpret the
* <tt>timeout</tt> parameter
* @return the head of this deque, or <tt>null</tt> if the specified
* waiting time elapses before an element is available
* @throws InterruptedException if interrupted while waiting
*/
E pollFirst(long timeout, TimeUnit unit)
throws InterruptedException;
/**
* Retrieves and removes the last element of this deque, waiting
* up to the specified wait time if necessary for an element to
* become available.
*
* @param timeout how long to wait before giving up, in units of
* <tt>unit</tt>
* @param unit a <tt>TimeUnit</tt> determining how to interpret the
* <tt>timeout</tt> parameter
* @return the tail of this deque, or <tt>null</tt> if the specified
* waiting time elapses before an element is available
* @throws InterruptedException if interrupted while waiting
*/
E pollLast(long timeout, TimeUnit unit)
throws InterruptedException;
/**
* Removes the first occurrence of the specified element from this deque.
* If the deque does not contain the element, it is unchanged.
* More formally, removes the first element <tt>e</tt> such that
* <tt>o.equals(e)</tt> (if such an element exists).
* Returns <tt>true</tt> if this deque contained the specified element
* (or equivalently, if this deque changed as a result of the call).
*
* @param o element to be removed from this deque, if present
* @return <tt>true</tt> if an element was removed as a result of this call
* @throws ClassCastException if the class of the specified element
* is incompatible with this deque (optional)
* @throws NullPointerException if the specified element is null (optional)
*/
boolean removeFirstOccurrence(Object o);
/**
* Removes the last occurrence of the specified element from this deque.
* If the deque does not contain the element, it is unchanged.
* More formally, removes the last element <tt>e</tt> such that
* <tt>o.equals(e)</tt> (if such an element exists).
* Returns <tt>true</tt> if this deque contained the specified element
* (or equivalently, if this deque changed as a result of the call).
*
* @param o element to be removed from this deque, if present
* @return <tt>true</tt> if an element was removed as a result of this call
* @throws ClassCastException if the class of the specified element
* is incompatible with this deque (optional)
* @throws NullPointerException if the specified element is null (optional)
*/
boolean removeLastOccurrence(Object o);
// *** BlockingQueue methods ***
/**
* Inserts the specified element into the queue represented by this deque
* (in other words, at the tail of this deque) if it is possible to do so
* immediately without violating capacity restrictions, returning
* <tt>true</tt> upon success and throwing an
* <tt>IllegalStateException</tt> if no space is currently available.
* When using a capacity-restricted deque, it is generally preferable to
* use {@link #offer offer}.
*
* <p>This method is equivalent to {@link #addLast addLast}.
*
* @param e the element to add
* @throws IllegalStateException {@inheritDoc}
* @throws ClassCastException if the class of the specified element
* prevents it from being added to this deque
* @throws NullPointerException if the specified element is null
* @throws IllegalArgumentException if some property of the specified
* element prevents it from being added to this deque
*/
boolean add(E e);
/**
* Inserts the specified element into the queue represented by this deque
* (in other words, at the tail of this deque) if it is possible to do so
* immediately without violating capacity restrictions, returning
* <tt>true</tt> upon success and <tt>false</tt> if no space is currently
* available. When using a capacity-restricted deque, this method is
* generally preferable to the {@link #add} method, which can fail to
* insert an element only by throwing an exception.
*
* <p>This method is equivalent to {@link #offerLast offerLast}.
*
* @param e the element to add
* @throws ClassCastException if the class of the specified element
* prevents it from being added to this deque
* @throws NullPointerException if the specified element is null
* @throws IllegalArgumentException if some property of the specified
* element prevents it from being added to this deque
*/
boolean offer(E e);
/**
* Inserts the specified element into the queue represented by this deque
* (in other words, at the tail of this deque), waiting if necessary for
* space to become available.
*
* <p>This method is equivalent to {@link #putLast putLast}.
*
* @param e the element to add
* @throws InterruptedException {@inheritDoc}
* @throws ClassCastException if the class of the specified element
* prevents it from being added to this deque
* @throws NullPointerException if the specified element is null
* @throws IllegalArgumentException if some property of the specified
* element prevents it from being added to this deque
*/
void put(E e) throws InterruptedException;
/**
* Inserts the specified element into the queue represented by this deque
* (in other words, at the tail of this deque), waiting up to the
* specified wait time if necessary for space to become available.
*
* <p>This method is equivalent to
* {@link #offerLast offerLast}.
*
* @param e the element to add
* @return <tt>true</tt> if the element was added to this deque, else
* <tt>false</tt>
* @throws InterruptedException {@inheritDoc}
* @throws ClassCastException if the class of the specified element
* prevents it from being added to this deque
* @throws NullPointerException if the specified element is null
* @throws IllegalArgumentException if some property of the specified
* element prevents it from being added to this deque
*/
boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException;
/**
* Retrieves and removes the head of the queue represented by this deque
* (in other words, the first element of this deque).
* This method differs from {@link #poll poll} only in that it
* throws an exception if this deque is empty.
*
* <p>This method is equivalent to {@link #removeFirst() removeFirst}.
*
* @return the head of the queue represented by this deque
* @throws NoSuchElementException if this deque is empty
*/
E remove();
/**
* Retrieves and removes the head of the queue represented by this deque
* (in other words, the first element of this deque), or returns
* <tt>null</tt> if this deque is empty.
*
* <p>This method is equivalent to {@link #pollFirst()}.
*
* @return the head of this deque, or <tt>null</tt> if this deque is empty
*/
E poll();
/**
* Retrieves and removes the head of the queue represented by this deque
* (in other words, the first element of this deque), waiting if
* necessary until an element becomes available.
*
* <p>This method is equivalent to {@link #takeFirst() takeFirst}.
*
* @return the head of this deque
* @throws InterruptedException if interrupted while waiting
*/
E take() throws InterruptedException;
/**
* Retrieves and removes the head of the queue represented by this deque
* (in other words, the first element of this deque), waiting up to the
* specified wait time if necessary for an element to become available.
*
* <p>This method is equivalent to
* {@link #pollFirst(long,TimeUnit) pollFirst}.
*
* @return the head of this deque, or <tt>null</tt> if the
* specified waiting time elapses before an element is available
* @throws InterruptedException if interrupted while waiting
*/
E poll(long timeout, TimeUnit unit)
throws InterruptedException;
/**
* Retrieves, but does not remove, the head of the queue represented by
* this deque (in other words, the first element of this deque).
* This method differs from {@link #peek peek} only in that it throws an
* exception if this deque is empty.
*
* <p>This method is equivalent to {@link #getFirst() getFirst}.
*
* @return the head of this deque
* @throws NoSuchElementException if this deque is empty
*/
E element();
/**
* Retrieves, but does not remove, the head of the queue represented by
* this deque (in other words, the first element of this deque), or
* returns <tt>null</tt> if this deque is empty.
*
* <p>This method is equivalent to {@link #peekFirst() peekFirst}.
*
* @return the head of this deque, or <tt>null</tt> if this deque is empty
*/
E peek();
/**
* Removes the first occurrence of the specified element from this deque.
* If the deque does not contain the element, it is unchanged.
* More formally, removes the first element <tt>e</tt> such that
* <tt>o.equals(e)</tt> (if such an element exists).
* Returns <tt>true</tt> if this deque contained the specified element
* (or equivalently, if this deque changed as a result of the call).
*
* <p>This method is equivalent to
* {@link #removeFirstOccurrence removeFirstOccurrence}.
*
* @param o element to be removed from this deque, if present
* @return <tt>true</tt> if this deque changed as a result of the call
* @throws ClassCastException if the class of the specified element
* is incompatible with this deque (optional)
* @throws NullPointerException if the specified element is null (optional)
*/
boolean remove(Object o);
/**
* Returns <tt>true</tt> if this deque contains the specified element.
* More formally, returns <tt>true</tt> if and only if this deque contains
* at least one element <tt>e</tt> such that <tt>o.equals(e)</tt>.
*
* @param o object to be checked for containment in this deque
* @return <tt>true</tt> if this deque contains the specified element
* @throws ClassCastException if the class of the specified element
* is incompatible with this deque (optional)
* @throws NullPointerException if the specified element is null (optional)
*/
public boolean contains(Object o);
/**
* Returns the number of elements in this deque.
*
* @return the number of elements in this deque
*/
public int size();
/**
* Returns an iterator over the elements in this deque in proper sequence.
* The elements will be returned in order from first (head) to last (tail).
*
* @return an iterator over the elements in this deque in proper sequence
*/
Iterator<E> iterator();
// *** Stack methods ***
/**
* Pushes an element onto the stack represented by this deque. In other
* words, inserts the element at the front of this deque unless it would
* violate capacity restrictions.
*
* <p>This method is equivalent to {@link #addFirst addFirst}.
*
* @throws IllegalStateException {@inheritDoc}
* @throws ClassCastException {@inheritDoc}
* @throws NullPointerException if the specified element is null
* @throws IllegalArgumentException {@inheritDoc}
*/
void push(E e);
}

View File

@ -1,555 +0,0 @@
/*
* Written by Doug Lea and Josh Bloch with assistance from members of
* JCP JSR-166 Expert Group and released to the public domain, as explained
* at http://creativecommons.org/licenses/publicdomain
*/
package org.thoughtcrime.securesms.util.deque;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Queue;
import java.util.Stack;
// BEGIN android-note
// removed link to collections framework docs
// changed {@link #offer(Object)} to {@link #offer} to satisfy DroidDoc
// END android-note
/**
* A linear collection that supports element insertion and removal at
* both ends. The name <i>deque</i> is short for "double ended queue"
* and is usually pronounced "deck". Most <tt>Deque</tt>
* implementations place no fixed limits on the number of elements
* they may contain, but this interface supports capacity-restricted
* deques as well as those with no fixed size limit.
*
* <p>This interface defines methods to access the elements at both
* ends of the deque. Methods are provided to insert, remove, and
* examine the element. Each of these methods exists in two forms:
* one throws an exception if the operation fails, the other returns a
* special value (either <tt>null</tt> or <tt>false</tt>, depending on
* the operation). The latter form of the insert operation is
* designed specifically for use with capacity-restricted
* <tt>Deque</tt> implementations; in most implementations, insert
* operations cannot fail.
*
* <p>The twelve methods described above are summarized in the
* following table:
*
* <p>
* <table BORDER CELLPADDING=3 CELLSPACING=1>
* <tr>
* <td></td>
* <td ALIGN=CENTER COLSPAN = 2> <b>First Element (Head)</b></td>
* <td ALIGN=CENTER COLSPAN = 2> <b>Last Element (Tail)</b></td>
* </tr>
* <tr>
* <td></td>
* <td ALIGN=CENTER><em>Throws exception</em></td>
* <td ALIGN=CENTER><em>Special value</em></td>
* <td ALIGN=CENTER><em>Throws exception</em></td>
* <td ALIGN=CENTER><em>Special value</em></td>
* </tr>
* <tr>
* <td><b>Insert</b></td>
* <td>{@link #addFirst addFirst(e)}</td>
* <td>{@link #offerFirst offerFirst(e)}</td>
* <td>{@link #addLast addLast(e)}</td>
* <td>{@link #offerLast offerLast(e)}</td>
* </tr>
* <tr>
* <td><b>Remove</b></td>
* <td>{@link #removeFirst removeFirst()}</td>
* <td>{@link #pollFirst pollFirst()}</td>
* <td>{@link #removeLast removeLast()}</td>
* <td>{@link #pollLast pollLast()}</td>
* </tr>
* <tr>
* <td><b>Examine</b></td>
* <td>{@link #getFirst getFirst()}</td>
* <td>{@link #peekFirst peekFirst()}</td>
* <td>{@link #getLast getLast()}</td>
* <td>{@link #peekLast peekLast()}</td>
* </tr>
* </table>
*
* <p>This interface extends the {@link Queue} interface. When a deque is
* used as a queue, FIFO (First-In-First-Out) behavior results. Elements are
* added at the end of the deque and removed from the beginning. The methods
* inherited from the <tt>Queue</tt> interface are precisely equivalent to
* <tt>Deque</tt> methods as indicated in the following table:
*
* <p>
* <table BORDER CELLPADDING=3 CELLSPACING=1>
* <tr>
* <td ALIGN=CENTER> <b><tt>Queue</tt> Method</b></td>
* <td ALIGN=CENTER> <b>Equivalent <tt>Deque</tt> Method</b></td>
* </tr>
* <tr>
* <td>{@link java.util.Queue#add add(e)}</td>
* <td>{@link #addLast addLast(e)}</td>
* </tr>
* <tr>
* <td>{@link java.util.Queue#offer offer(e)}</td>
* <td>{@link #offerLast offerLast(e)}</td>
* </tr>
* <tr>
* <td>{@link java.util.Queue#remove remove()}</td>
* <td>{@link #removeFirst removeFirst()}</td>
* </tr>
* <tr>
* <td>{@link java.util.Queue#poll poll()}</td>
* <td>{@link #pollFirst pollFirst()}</td>
* </tr>
* <tr>
* <td>{@link java.util.Queue#element element()}</td>
* <td>{@link #getFirst getFirst()}</td>
* </tr>
* <tr>
* <td>{@link java.util.Queue#peek peek()}</td>
* <td>{@link #peek peekFirst()}</td>
* </tr>
* </table>
*
* <p>Deques can also be used as LIFO (Last-In-First-Out) stacks. This
* interface should be used in preference to the legacy {@link Stack} class.
* When a deque is used as a stack, elements are pushed and popped from the
* beginning of the deque. Stack methods are precisely equivalent to
* <tt>Deque</tt> methods as indicated in the table below:
*
* <p>
* <table BORDER CELLPADDING=3 CELLSPACING=1>
* <tr>
* <td ALIGN=CENTER> <b>Stack Method</b></td>
* <td ALIGN=CENTER> <b>Equivalent <tt>Deque</tt> Method</b></td>
* </tr>
* <tr>
* <td>{@link #push push(e)}</td>
* <td>{@link #addFirst addFirst(e)}</td>
* </tr>
* <tr>
* <td>{@link #pop pop()}</td>
* <td>{@link #removeFirst removeFirst()}</td>
* </tr>
* <tr>
* <td>{@link #peek peek()}</td>
* <td>{@link #peekFirst peekFirst()}</td>
* </tr>
* </table>
*
* <p>Note that the {@link #peek peek} method works equally well when
* a deque is used as a queue or a stack; in either case, elements are
* drawn from the beginning of the deque.
*
* <p>This interface provides two methods to remove interior
* elements, {@link #removeFirstOccurrence removeFirstOccurrence} and
* {@link #removeLastOccurrence removeLastOccurrence}.
*
* <p>Unlike the {@link List} interface, this interface does not
* provide support for indexed access to elements.
*
* <p>While <tt>Deque</tt> implementations are not strictly required
* to prohibit the insertion of null elements, they are strongly
* encouraged to do so. Users of any <tt>Deque</tt> implementations
* that do allow null elements are strongly encouraged <i>not</i> to
* take advantage of the ability to insert nulls. This is so because
* <tt>null</tt> is used as a special return value by various methods
* to indicated that the deque is empty.
*
* <p><tt>Deque</tt> implementations generally do not define
* element-based versions of the <tt>equals</tt> and <tt>hashCode</tt>
* methods, but instead inherit the identity-based versions from class
* <tt>Object</tt>.
*
* @author Doug Lea
* @author Josh Bloch
* @since 1.6
* @param <E> the type of elements held in this collection
*/
public interface Deque<E> extends Queue<E> {
/**
* Inserts the specified element at the front of this deque if it is
* possible to do so immediately without violating capacity restrictions.
* When using a capacity-restricted deque, it is generally preferable to
* use method {@link #offerFirst}.
*
* @param e the element to add
* @throws IllegalStateException if the element cannot be added at this
* time due to capacity restrictions
* @throws ClassCastException if the class of the specified element
* prevents it from being added to this deque
* @throws NullPointerException if the specified element is null and this
* deque does not permit null elements
* @throws IllegalArgumentException if some property of the specified
* element prevents it from being added to this deque
*/
void addFirst(E e);
/**
* Inserts the specified element at the end of this deque if it is
* possible to do so immediately without violating capacity restrictions.
* When using a capacity-restricted deque, it is generally preferable to
* use method {@link #offerLast}.
*
* <p>This method is equivalent to {@link #add}.
*
* @param e the element to add
* @throws IllegalStateException if the element cannot be added at this
* time due to capacity restrictions
* @throws ClassCastException if the class of the specified element
* prevents it from being added to this deque
* @throws NullPointerException if the specified element is null and this
* deque does not permit null elements
* @throws IllegalArgumentException if some property of the specified
* element prevents it from being added to this deque
*/
void addLast(E e);
/**
* Inserts the specified element at the front of this deque unless it would
* violate capacity restrictions. When using a capacity-restricted deque,
* this method is generally preferable to the {@link #addFirst} method,
* which can fail to insert an element only by throwing an exception.
*
* @param e the element to add
* @return <tt>true</tt> if the element was added to this deque, else
* <tt>false</tt>
* @throws ClassCastException if the class of the specified element
* prevents it from being added to this deque
* @throws NullPointerException if the specified element is null and this
* deque does not permit null elements
* @throws IllegalArgumentException if some property of the specified
* element prevents it from being added to this deque
*/
boolean offerFirst(E e);
/**
* Inserts the specified element at the end of this deque unless it would
* violate capacity restrictions. When using a capacity-restricted deque,
* this method is generally preferable to the {@link #addLast} method,
* which can fail to insert an element only by throwing an exception.
*
* @param e the element to add
* @return <tt>true</tt> if the element was added to this deque, else
* <tt>false</tt>
* @throws ClassCastException if the class of the specified element
* prevents it from being added to this deque
* @throws NullPointerException if the specified element is null and this
* deque does not permit null elements
* @throws IllegalArgumentException if some property of the specified
* element prevents it from being added to this deque
*/
boolean offerLast(E e);
/**
* Retrieves and removes the first element of this deque. This method
* differs from {@link #pollFirst pollFirst} only in that it throws an
* exception if this deque is empty.
*
* @return the head of this deque
* @throws NoSuchElementException if this deque is empty
*/
E removeFirst();
/**
* Retrieves and removes the last element of this deque. This method
* differs from {@link #pollLast pollLast} only in that it throws an
* exception if this deque is empty.
*
* @return the tail of this deque
* @throws NoSuchElementException if this deque is empty
*/
E removeLast();
/**
* Retrieves and removes the first element of this deque,
* or returns <tt>null</tt> if this deque is empty.
*
* @return the head of this deque, or <tt>null</tt> if this deque is empty
*/
E pollFirst();
/**
* Retrieves and removes the last element of this deque,
* or returns <tt>null</tt> if this deque is empty.
*
* @return the tail of this deque, or <tt>null</tt> if this deque is empty
*/
E pollLast();
/**
* Retrieves, but does not remove, the first element of this deque.
*
* This method differs from {@link #peekFirst peekFirst} only in that it
* throws an exception if this deque is empty.
*
* @return the head of this deque
* @throws NoSuchElementException if this deque is empty
*/
E getFirst();
/**
* Retrieves, but does not remove, the last element of this deque.
* This method differs from {@link #peekLast peekLast} only in that it
* throws an exception if this deque is empty.
*
* @return the tail of this deque
* @throws NoSuchElementException if this deque is empty
*/
E getLast();
/**
* Retrieves, but does not remove, the first element of this deque,
* or returns <tt>null</tt> if this deque is empty.
*
* @return the head of this deque, or <tt>null</tt> if this deque is empty
*/
E peekFirst();
/**
* Retrieves, but does not remove, the last element of this deque,
* or returns <tt>null</tt> if this deque is empty.
*
* @return the tail of this deque, or <tt>null</tt> if this deque is empty
*/
E peekLast();
/**
* Removes the first occurrence of the specified element from this deque.
* If the deque does not contain the element, it is unchanged.
* More formally, removes the first element <tt>e</tt> such that
* <tt>(o==null&nbsp;?&nbsp;e==null&nbsp;:&nbsp;o.equals(e))</tt>
* (if such an element exists).
* Returns <tt>true</tt> if this deque contained the specified element
* (or equivalently, if this deque changed as a result of the call).
*
* @param o element to be removed from this deque, if present
* @return <tt>true</tt> if an element was removed as a result of this call
* @throws ClassCastException if the class of the specified element
* is incompatible with this deque (optional)
* @throws NullPointerException if the specified element is null and this
* deque does not permit null elements (optional)
*/
boolean removeFirstOccurrence(Object o);
/**
* Removes the last occurrence of the specified element from this deque.
* If the deque does not contain the element, it is unchanged.
* More formally, removes the last element <tt>e</tt> such that
* <tt>(o==null&nbsp;?&nbsp;e==null&nbsp;:&nbsp;o.equals(e))</tt>
* (if such an element exists).
* Returns <tt>true</tt> if this deque contained the specified element
* (or equivalently, if this deque changed as a result of the call).
*
* @param o element to be removed from this deque, if present
* @return <tt>true</tt> if an element was removed as a result of this call
* @throws ClassCastException if the class of the specified element
* is incompatible with this deque (optional)
* @throws NullPointerException if the specified element is null and this
* deque does not permit null elements (optional)
*/
boolean removeLastOccurrence(Object o);
// *** Queue methods ***
/**
* Inserts the specified element into the queue represented by this deque
* (in other words, at the tail of this deque) if it is possible to do so
* immediately without violating capacity restrictions, returning
* <tt>true</tt> upon success and throwing an
* <tt>IllegalStateException</tt> if no space is currently available.
* When using a capacity-restricted deque, it is generally preferable to
* use {@link #offer offer}.
*
* <p>This method is equivalent to {@link #addLast}.
*
* @param e the element to add
* @return <tt>true</tt> (as specified by {@link Collection#add})
* @throws IllegalStateException if the element cannot be added at this
* time due to capacity restrictions
* @throws ClassCastException if the class of the specified element
* prevents it from being added to this deque
* @throws NullPointerException if the specified element is null and this
* deque does not permit null elements
* @throws IllegalArgumentException if some property of the specified
* element prevents it from being added to this deque
*/
boolean add(E e);
/**
* Inserts the specified element into the queue represented by this deque
* (in other words, at the tail of this deque) if it is possible to do so
* immediately without violating capacity restrictions, returning
* <tt>true</tt> upon success and <tt>false</tt> if no space is currently
* available. When using a capacity-restricted deque, this method is
* generally preferable to the {@link #add} method, which can fail to
* insert an element only by throwing an exception.
*
* <p>This method is equivalent to {@link #offerLast}.
*
* @param e the element to add
* @return <tt>true</tt> if the element was added to this deque, else
* <tt>false</tt>
* @throws ClassCastException if the class of the specified element
* prevents it from being added to this deque
* @throws NullPointerException if the specified element is null and this
* deque does not permit null elements
* @throws IllegalArgumentException if some property of the specified
* element prevents it from being added to this deque
*/
boolean offer(E e);
/**
* Retrieves and removes the head of the queue represented by this deque
* (in other words, the first element of this deque).
* This method differs from {@link #poll poll} only in that it throws an
* exception if this deque is empty.
*
* <p>This method is equivalent to {@link #removeFirst()}.
*
* @return the head of the queue represented by this deque
* @throws NoSuchElementException if this deque is empty
*/
E remove();
/**
* Retrieves and removes the head of the queue represented by this deque
* (in other words, the first element of this deque), or returns
* <tt>null</tt> if this deque is empty.
*
* <p>This method is equivalent to {@link #pollFirst()}.
*
* @return the first element of this deque, or <tt>null</tt> if
* this deque is empty
*/
E poll();
/**
* Retrieves, but does not remove, the head of the queue represented by
* this deque (in other words, the first element of this deque).
* This method differs from {@link #peek peek} only in that it throws an
* exception if this deque is empty.
*
* <p>This method is equivalent to {@link #getFirst()}.
*
* @return the head of the queue represented by this deque
* @throws NoSuchElementException if this deque is empty
*/
E element();
/**
* Retrieves, but does not remove, the head of the queue represented by
* this deque (in other words, the first element of this deque), or
* returns <tt>null</tt> if this deque is empty.
*
* <p>This method is equivalent to {@link #peekFirst()}.
*
* @return the head of the queue represented by this deque, or
* <tt>null</tt> if this deque is empty
*/
E peek();
// *** Stack methods ***
/**
* Pushes an element onto the stack represented by this deque (in other
* words, at the head of this deque) if it is possible to do so
* immediately without violating capacity restrictions, returning
* <tt>true</tt> upon success and throwing an
* <tt>IllegalStateException</tt> if no space is currently available.
*
* <p>This method is equivalent to {@link #addFirst}.
*
* @param e the element to push
* @throws IllegalStateException if the element cannot be added at this
* time due to capacity restrictions
* @throws ClassCastException if the class of the specified element
* prevents it from being added to this deque
* @throws NullPointerException if the specified element is null and this
* deque does not permit null elements
* @throws IllegalArgumentException if some property of the specified
* element prevents it from being added to this deque
*/
void push(E e);
/**
* Pops an element from the stack represented by this deque. In other
* words, removes and returns the first element of this deque.
*
* <p>This method is equivalent to {@link #removeFirst()}.
*
* @return the element at the front of this deque (which is the top
* of the stack represented by this deque)
* @throws NoSuchElementException if this deque is empty
*/
E pop();
// *** Collection methods ***
/**
* Removes the first occurrence of the specified element from this deque.
* If the deque does not contain the element, it is unchanged.
* More formally, removes the first element <tt>e</tt> such that
* <tt>(o==null&nbsp;?&nbsp;e==null&nbsp;:&nbsp;o.equals(e))</tt>
* (if such an element exists).
* Returns <tt>true</tt> if this deque contained the specified element
* (or equivalently, if this deque changed as a result of the call).
*
* <p>This method is equivalent to {@link #removeFirstOccurrence}.
*
* @param o element to be removed from this deque, if present
* @return <tt>true</tt> if an element was removed as a result of this call
* @throws ClassCastException if the class of the specified element
* is incompatible with this deque (optional)
* @throws NullPointerException if the specified element is null and this
* deque does not permit null elements (optional)
*/
boolean remove(Object o);
/**
* Returns <tt>true</tt> if this deque contains the specified element.
* More formally, returns <tt>true</tt> if and only if this deque contains
* at least one element <tt>e</tt> such that
* <tt>(o==null&nbsp;?&nbsp;e==null&nbsp;:&nbsp;o.equals(e))</tt>.
*
* @param o element whose presence in this deque is to be tested
* @return <tt>true</tt> if this deque contains the specified element
* @throws ClassCastException if the type of the specified element
* is incompatible with this deque (optional)
* @throws NullPointerException if the specified element is null and this
* deque does not permit null elements (optional)
*/
boolean contains(Object o);
/**
* Returns the number of elements in this deque.
*
* @return the number of elements in this deque
*/
public int size();
/**
* Returns an iterator over the elements in this deque in proper sequence.
* The elements will be returned in order from first (head) to last (tail).
*
* @return an iterator over the elements in this deque in proper sequence
*/
Iterator<E> iterator();
/**
* Returns an iterator over the elements in this deque in reverse
* sequential order. The elements will be returned in order from
* last (tail) to first (head).
*
* @return an iterator over the elements in this deque in reverse
* sequence
*/
Iterator<E> descendingIterator();
}