diff --git a/src/org/thoughtcrime/securesms/contacts/ContactAccessor.java b/src/org/thoughtcrime/securesms/contacts/ContactAccessor.java index f5ac5e0d17..1551cd3e0f 100644 --- a/src/org/thoughtcrime/securesms/contacts/ContactAccessor.java +++ b/src/org/thoughtcrime/securesms/contacts/ContactAccessor.java @@ -79,7 +79,7 @@ public class ContactAccessor { } 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) { diff --git a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java index 01512adac6..dbb8826232 100644 --- a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java +++ b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java @@ -16,6 +16,7 @@ */ package org.thoughtcrime.securesms.database; +import android.Manifest; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; @@ -23,6 +24,8 @@ import android.database.sqlite.SQLiteConstraintException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.database.sqlite.SQLiteOpenHelper; +import android.net.Uri; +import android.provider.ContactsContract; import android.support.annotation.Nullable; import android.text.TextUtils; 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.MasterSecretUtil; import org.thoughtcrime.securesms.notifications.MessageNotifier; +import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.DelimiterUtil; import org.thoughtcrime.securesms.util.Hex; @@ -110,7 +114,8 @@ public class DatabaseFactory { private static final int READ_RECEIPTS = 44; private static final int GROUP_RECEIPT_TRACKING = 45; 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 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.endTransaction(); } diff --git a/src/org/thoughtcrime/securesms/database/GroupDatabase.java b/src/org/thoughtcrime/securesms/database/GroupDatabase.java index 99576a1fc3..3ef8ca55c6 100644 --- a/src/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/src/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -14,7 +14,6 @@ import android.text.TextUtils; import com.annimon.stream.Stream; -import org.thoughtcrime.securesms.contacts.avatars.GroupRecordContactPhoto; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.GroupUtil; @@ -175,12 +174,11 @@ public class GroupDatabase extends Database { databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues); - Address address = Address.fromSerialized(groupId); - Recipient recipient = Recipient.from(context, Address.fromSerialized(groupId), false); - - recipient.setName(title); - if (avatar != null) recipient.setContactPhoto(new GroupRecordContactPhoto(address, avatar.getId())); - recipient.setParticipants(Stream.of(members).map(memberAddress -> Recipient.from(context, memberAddress, true)).toList()); + Recipient.applyCached(Address.fromSerialized(groupId), recipient -> { + recipient.setName(title); + recipient.setGroupAvatarId(avatar != null ? avatar.getId() : null); + recipient.setParticipants(Stream.of(members).map(memberAddress -> Recipient.from(context, memberAddress, true)).toList()); + }); notifyConversationListListeners(); } @@ -200,10 +198,10 @@ public class GroupDatabase extends Database { GROUP_ID + " = ?", new String[] {groupId}); - Address address = Address.fromSerialized(groupId); - Recipient recipient = Recipient.from(context, address, false); - recipient.setName(title); - if (avatar != null) recipient.setContactPhoto(new GroupRecordContactPhoto(address, avatar.getId())); + Recipient.applyCached(Address.fromSerialized(groupId), recipient -> { + recipient.setName(title); + recipient.setGroupAvatarId(avatar != null ? avatar.getId() : null); + }); notifyConversationListListeners(); } @@ -231,9 +229,7 @@ public class GroupDatabase extends Database { databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ?", new String[] {groupId}); - Address address = Address.fromSerialized(groupId); - Recipient recipient = Recipient.from(context, address, false); - recipient.setContactPhoto(new GroupRecordContactPhoto(address, avatarId)); + Recipient.applyCached(Address.fromSerialized(groupId), recipient -> recipient.setGroupAvatarId(avatarId)); } public void updateMembers(String groupId, List
members) { diff --git a/src/org/thoughtcrime/securesms/database/RecipientDatabase.java b/src/org/thoughtcrime/securesms/database/RecipientDatabase.java index c349c35e48..a8f8cb5efe 100644 --- a/src/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/src/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -15,19 +15,20 @@ import com.annimon.stream.Stream; import org.thoughtcrime.securesms.color.MaterialColor; import org.thoughtcrime.securesms.recipients.Recipient; 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 java.io.IOException; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Set; public class RecipientDatabase extends Database { 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"; 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 PROFILE_KEY = "profile_key"; 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_AVATAR = "signal_profile_avatar"; private static final String PROFILE_SHARING = "profile_sharing_approval"; private static final String[] RECIPIENT_PROJECTION = new String[] { 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 TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION) @@ -106,6 +111,9 @@ public class RecipientDatabase extends Database { EXPIRE_MESSAGES + " INTEGER DEFAULT 0, " + REGISTERED + " INTEGER DEFAULT 0, " + 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, " + SIGNAL_PROFILE_NAME + " TEXT DEFAULT NULL, " + SIGNAL_PROFILE_AVATAR + " TEXT DEFAULT NULL, " + @@ -118,11 +126,8 @@ public class RecipientDatabase extends Database { public Cursor getBlocked() { SQLiteDatabase database = databaseHelper.getReadableDatabase(); - Cursor cursor = database.query(TABLE_NAME, new String[] {ID, ADDRESS}, BLOCK + " = 1", - null, null, null, null, null); - cursor.setNotificationUri(context.getContentResolver(), Uri.parse(RECIPIENT_PREFERENCES_URI)); - - return cursor; + return database.query(TABLE_NAME, new String[] {ID, ADDRESS}, BLOCK + " = 1", + null, null, null, null, null); } public BlockedReader readerForBlocked(Cursor cursor) { @@ -153,13 +158,15 @@ public class RecipientDatabase extends Database { int vibrateState = cursor.getInt(cursor.getColumnIndexOrThrow(VIBRATE)); long muteUntil = cursor.getLong(cursor.getColumnIndexOrThrow(MUTE_UNTIL)); String serializedColor = cursor.getString(cursor.getColumnIndexOrThrow(COLOR)); - Uri notificationUri = notification == null ? null : Uri.parse(notification); boolean seenInviteReminder = cursor.getInt(cursor.getColumnIndexOrThrow(SEEN_INVITE_REMINDER)) == 1; int defaultSubscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(DEFAULT_SUBSCRIPTION_ID)); int expireMessages = cursor.getInt(cursor.getColumnIndexOrThrow(EXPIRE_MESSAGES)); int registeredState = cursor.getInt(cursor.getColumnIndexOrThrow(REGISTERED)); String profileKeyString = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_KEY)); 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 signalProfileAvatar = cursor.getString(cursor.getColumnIndexOrThrow(SIGNAL_PROFILE_AVATAR)); 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, VibrateState.fromId(vibrateState), - notificationUri, color, seenInviteReminder, + Util.uri(notification), color, seenInviteReminder, defaultSubscriptionId, expireMessages, RegisteredState.fromId(registeredState), - profileKey, systemDisplayName, signalProfileName, - signalProfileAvatar, profileSharing)); + profileKey, systemDisplayName, systemContactPhoto, + systemPhoneLabel, systemContactUri, + signalProfileName, signalProfileAvatar, profileSharing)); } - public BulkOperationsHandle resetAllDisplayNames() { + public BulkOperationsHandle resetAllSystemContactInfo() { SQLiteDatabase database = databaseHelper.getWritableDatabase(); database.beginTransaction(); ContentValues contentValues = new ContentValues(1); 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); @@ -246,7 +257,7 @@ public class RecipientDatabase extends Database { 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); values.put(SEEN_INVITE_REMINDER, seen ? 1 : 0); updateOrInsert(recipient.getAddress(), values); @@ -283,20 +294,20 @@ public class RecipientDatabase extends Database { 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.put(PROFILE_SHARING, enabled ? 1 : 0); updateOrInsert(recipient.getAddress(), contentValues); recipient.setProfileSharing(enabled); } - public Set getAllRecipients() { + public Set
getAllAddresses() { SQLiteDatabase db = databaseHelper.getReadableDatabase(); - Set results = new HashSet<>(); + Set
results = new HashSet<>(); try (Cursor cursor = db.query(TABLE_NAME, new String[] {ADDRESS}, null, null, null, null, null)) { 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); } - public void setRegistered(@NonNull List activeRecipients, - @NonNull List inactiveRecipients) + public void setRegistered(@NonNull List
activeAddresses, + @NonNull List
inactiveAddresses) { - for (Recipient activeRecipient : activeRecipients) { + for (Address activeAddress : activeAddresses) { ContentValues contentValues = new ContentValues(1); contentValues.put(REGISTERED, RegisteredState.REGISTERED.getId()); - updateOrInsert(activeRecipient.getAddress(), contentValues); - activeRecipient.setRegistered(RegisteredState.REGISTERED); + updateOrInsert(activeAddress, contentValues); + Recipient.applyCached(activeAddress, recipient -> recipient.setRegistered(RegisteredState.REGISTERED)); } - for (Recipient inactiveRecipient : inactiveRecipients) { + for (Address inactiveAddress : inactiveAddresses) { ContentValues contentValues = new ContentValues(1); contentValues.put(REGISTERED, RegisteredState.NOT_REGISTERED.getId()); - updateOrInsert(inactiveRecipient.getAddress(), contentValues); - inactiveRecipient.setRegistered(RegisteredState.NOT_REGISTERED); + updateOrInsert(inactiveAddress, contentValues); + Recipient.applyCached(inactiveAddress, recipient -> recipient.setRegistered(RegisteredState.NOT_REGISTERED)); } - - context.getContentResolver().notifyChange(Uri.parse(RECIPIENT_PREFERENCES_URI), null); } public List
getRegistered() { @@ -360,15 +369,6 @@ public class RecipientDatabase extends Database { 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 + " = ?", new String[] {address.serialize()}); @@ -376,32 +376,43 @@ public class RecipientDatabase extends Database { contentValues.put(ADDRESS, address.serialize()); database.insert(TABLE_NAME, null, contentValues); } + + database.setTransactionSuccessful(); + database.endTransaction(); } public class BulkOperationsHandle { private final SQLiteDatabase database; - private final List> pendingDisplayNames = new LinkedList<>(); + private final Map pendingContactInfoMap = new HashMap<>(); BulkOperationsHandle(SQLiteDatabase 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.put(SYSTEM_DISPLAY_NAME, displayName); - updateOrInsert(recipient.getAddress(), contentValues); - pendingDisplayNames.add(new Pair<>(recipient, displayName)); + contentValues.put(SYSTEM_PHOTO_URI, photoUri); + 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() { database.setTransactionSuccessful(); database.endTransaction(); - Stream.of(pendingDisplayNames).forEach(pair -> pair.first().resolve().setName(pair.second())); - - context.getContentResolver().notifyChange(Uri.parse(RECIPIENT_PREFERENCES_URI), null); + Stream.of(pendingContactInfoMap.entrySet()) + .forEach(entry -> Recipient.applyCached(entry.getKey(), recipient -> { + 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 byte[] profileKey; private final String systemDisplayName; + private final String systemContactPhoto; + private final String systemPhoneLabel; + private final String systemContactUri; private final String signalProfileName; private final String signalProfileAvatar; private final boolean profileSharing; @@ -431,6 +445,9 @@ public class RecipientDatabase extends Database { @NonNull RegisteredState registered, @Nullable byte[] profileKey, @Nullable String systemDisplayName, + @Nullable String systemContactPhoto, + @Nullable String systemPhoneLabel, + @Nullable String systemContactUri, @Nullable String signalProfileName, @Nullable String signalProfileAvatar, boolean profileSharing) @@ -446,6 +463,9 @@ public class RecipientDatabase extends Database { this.registered = registered; this.profileKey = profileKey; this.systemDisplayName = systemDisplayName; + this.systemContactPhoto = systemContactPhoto; + this.systemPhoneLabel = systemPhoneLabel; + this.systemContactUri = systemContactUri; this.signalProfileName = signalProfileName; this.signalProfileAvatar = signalProfileAvatar; this.profileSharing = profileSharing; @@ -495,6 +515,18 @@ public class RecipientDatabase extends Database { return systemDisplayName; } + public @Nullable String getSystemContactPhotoUri() { + return systemContactPhoto; + } + + public @Nullable String getSystemPhoneLabel() { + return systemPhoneLabel; + } + + public @Nullable String getSystemContactUri() { + return systemContactUri; + } + public @Nullable String getProfileName() { return signalProfileName; } @@ -531,4 +563,20 @@ public class RecipientDatabase extends Database { 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; + } + } + } diff --git a/src/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java b/src/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java index f4f12e706a..dbc25d2b95 100644 --- a/src/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java +++ b/src/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java @@ -5,7 +5,6 @@ import android.content.Context; import android.text.TextUtils; import android.util.Log; -import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.dependencies.InjectableType; @@ -83,10 +82,6 @@ public class RetrieveProfileAvatarJob extends ContextJob implements InjectableTy } database.setProfileAvatar(recipient, profileAvatar); - - if (recipient.resolve().getContactPhoto() == null) { - recipient.setContactPhoto(new ProfileContactPhoto(recipient.getAddress(), profileAvatar)); - } } @Override diff --git a/src/org/thoughtcrime/securesms/recipients/Recipient.java b/src/org/thoughtcrime/securesms/recipients/Recipient.java index 5e9b673e3f..aa97f1201e 100644 --- a/src/org/thoughtcrime/securesms/recipients/Recipient.java +++ b/src/org/thoughtcrime/securesms/recipients/Recipient.java @@ -22,14 +22,21 @@ import android.graphics.drawable.Drawable; import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.text.TextUtils; 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.contacts.avatars.ContactColors; 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.GroupDatabase; @@ -42,7 +49,6 @@ import org.thoughtcrime.securesms.util.ListenableFutureTask; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.guava.Optional; -import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; @@ -65,8 +71,8 @@ public class Recipient implements RecipientModifiedListener { private @Nullable String customLabel; private boolean resolving; - private @Nullable ContactPhoto contactPhoto; - private @NonNull FallbackContactPhoto fallbackContactPhoto; + private @Nullable Uri systemContactPhoto; + private @Nullable Long groupAvatarId; private Uri contactUri; private @Nullable Uri ringtone = null; private long mutedUntil = 0; @@ -97,21 +103,25 @@ public class Recipient implements RecipientModifiedListener { return provider.getRecipient(context, address, settings, groupRecord, asynchronous); } + public static void applyCached(@NonNull Address address, Consumer consumer) { + Optional recipient = provider.getCached(address); + if (recipient.isPresent()) consumer.accept(recipient.get()); + } + Recipient(@NonNull Address address, @Nullable Recipient stale, @NonNull Optional details, @NonNull ListenableFutureTask future) { this.address = address; - this.fallbackContactPhoto = new TransparentContactPhoto(); this.color = null; this.resolving = true; if (stale != null) { this.name = stale.name; this.contactUri = stale.contactUri; - this.contactPhoto = stale.contactPhoto; - this.fallbackContactPhoto = stale.fallbackContactPhoto; + this.systemContactPhoto = stale.systemContactPhoto; + this.groupAvatarId = stale.groupAvatarId; this.color = stale.color; this.customLabel = stale.customLabel; this.ringtone = stale.ringtone; @@ -133,8 +143,8 @@ public class Recipient implements RecipientModifiedListener { if (details.isPresent()) { this.name = details.get().name; - this.contactPhoto = details.get().avatar; - this.fallbackContactPhoto = details.get().fallbackAvatar; + this.systemContactPhoto = details.get().systemContactPhoto; + this.groupAvatarId = details.get().groupAvatarId; this.color = details.get().color; this.ringtone = details.get().ringtone; this.mutedUntil = details.get().mutedUntil; @@ -160,8 +170,8 @@ public class Recipient implements RecipientModifiedListener { synchronized (Recipient.this) { Recipient.this.name = result.name; Recipient.this.contactUri = result.contactUri; - Recipient.this.contactPhoto = result.avatar; - Recipient.this.fallbackContactPhoto = result.fallbackAvatar; + Recipient.this.systemContactPhoto = result.systemContactPhoto; + Recipient.this.groupAvatarId = result.groupAvatarId; Recipient.this.color = result.color; Recipient.this.customLabel = result.customLabel; Recipient.this.ringtone = result.ringtone; @@ -205,8 +215,8 @@ public class Recipient implements RecipientModifiedListener { this.address = address; this.contactUri = details.contactUri; this.name = details.name; - this.contactPhoto = details.avatar; - this.fallbackContactPhoto = details.fallbackAvatar; + this.systemContactPhoto = details.systemContactPhoto; + this.groupAvatarId = details.groupAvatarId; this.color = details.color; this.customLabel = details.customLabel; this.ringtone = details.ringtone; @@ -230,6 +240,19 @@ public class Recipient implements RecipientModifiedListener { 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() { if (this.name == null && isMmsGroupRecipient()) { List names = new LinkedList<>(); @@ -276,10 +299,23 @@ public class Recipient implements RecipientModifiedListener { return address; } - public @Nullable String getCustomLabel() { + public synchronized @Nullable String getCustomLabel() { 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 getDefaultSubscriptionId() { return defaultSubscriptionId; } @@ -377,19 +413,43 @@ public class Recipient implements RecipientModifiedListener { } 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() { - 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) { - 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() { diff --git a/src/org/thoughtcrime/securesms/recipients/RecipientProvider.java b/src/org/thoughtcrime/securesms/recipients/RecipientProvider.java index 9f3cee64dc..e34ecd8d1f 100644 --- a/src/org/thoughtcrime/securesms/recipients/RecipientProvider.java +++ b/src/org/thoughtcrime/securesms/recipients/RecipientProvider.java @@ -17,33 +17,21 @@ package org.thoughtcrime.securesms.recipients; import android.content.Context; -import android.database.Cursor; import android.net.Uri; -import android.provider.ContactsContract.Contacts; -import android.provider.ContactsContract.PhoneLookup; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; -import android.util.Log; import org.thoughtcrime.securesms.R; 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.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord; import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings; import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState; import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState; -import org.thoughtcrime.securesms.util.LRUCache; import org.thoughtcrime.securesms.util.ListenableFutureTask; +import org.thoughtcrime.securesms.util.SoftHashMap; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.guava.Optional; @@ -56,25 +44,17 @@ import java.util.concurrent.ExecutorService; class RecipientProvider { + @SuppressWarnings("unused") private static final String TAG = RecipientProvider.class.getSimpleName(); private static final RecipientCache recipientCache = new RecipientCache(); 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 STATIC_DETAILS = new HashMap() {{ - 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 settings, Optional groupRecord, boolean asynchronous) { + @NonNull Recipient getRecipient(@NonNull Context context, @NonNull Address address, @NonNull Optional settings, @NonNull Optional groupRecord, boolean asynchronous) { Recipient cachedRecipient = recipientCache.get(address); if (cachedRecipient != null && (asynchronous || !cachedRecipient.isResolving()) && ((!groupRecord.isPresent() && !settings.isPresent()) || !cachedRecipient.isResolving() || cachedRecipient.getName() != null)) { @@ -93,6 +73,10 @@ class RecipientProvider { return cachedRecipient; } + @NonNull Optional getCached(@NonNull Address address) { + return Optional.fromNullable(recipientCache.get(address)); + } + private @NonNull Optional createPrefetchedRecipientDetails(@NonNull Context context, @NonNull Address address, @NonNull Optional settings, @NonNull Optional groupRecord) @@ -100,7 +84,7 @@ class RecipientProvider { if (address.isGroup() && settings.isPresent() && groupRecord.isPresent()) { return Optional.of(getGroupRecipientDetails(context, address, groupRecord, settings, true)); } 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(); @@ -108,12 +92,7 @@ class RecipientProvider { private @NonNull ListenableFutureTask getRecipientDetailsAsync(final Context context, final @NonNull Address address, final @NonNull Optional settings, final @NonNull Optional groupRecord) { - Callable task = new Callable() { - @Override - public RecipientDetails call() throws Exception { - return getRecipientDetailsSync(context, address, settings, groupRecord, true); - } - }; + Callable task = () -> getRecipientDetailsSync(context, address, settings, groupRecord, true); ListenableFutureTask future = new ListenableFutureTask<>(task); asyncRecipientResolver.submit(future); @@ -126,53 +105,18 @@ class RecipientProvider { } private @NonNull RecipientDetails getIndividualRecipientDetails(Context context, @NonNull Address address, Optional settings) { - ContactPhoto contactPhoto = null; - FallbackContactPhoto fallbackContactPhoto = new GeneratedContactPhoto("#"); - if (!settings.isPresent()) { settings = DatabaseFactory.getRecipientDatabase(context).getRecipientSettings(address); } - if (settings.isPresent() && !TextUtils.isEmpty(settings.get().getProfileAvatar())) { - contactPhoto = new ProfileContactPhoto(address, settings.get().getProfileAvatar()); + if (!settings.isPresent() && STATIC_DETAILS.containsKey(address.serialize())) { + 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, Optional settings, boolean asynchronous) { - ContactPhoto contactPhoto = null; - FallbackContactPhoto fallbackContactPhoto = new ResourceContactPhoto(R.drawable.ic_group_white_24dp, R.drawable.ic_group_large); if (!groupRecord.isPresent()) { groupRecord = DatabaseFactory.getGroupDatabase(context).getGroup(groupId.toGroupString()); @@ -192,65 +136,59 @@ class RecipientProvider { } 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) { - contactPhoto = new GroupRecordContactPhoto(groupId, groupRecord.get().getAvatarId()); - } - - return new RecipientDetails(title, null, null, contactPhoto, fallbackContactPhoto, false, settings.orNull(), members); + return new RecipientDetails(title, groupRecord.get().getAvatarId(), 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 { - @Nullable public final String name; - @Nullable public final String customLabel; - @Nullable public final ContactPhoto avatar; - @NonNull public final FallbackContactPhoto fallbackAvatar; - @Nullable public final Uri contactUri; - @Nullable public final MaterialColor color; - @Nullable public final Uri ringtone; - public final long mutedUntil; - @Nullable public final VibrateState vibrateState; - public final boolean blocked; - public final int expireMessages; - @NonNull public final List participants; - @Nullable public final String profileName; - public final boolean seenInviteReminder; - public final Optional defaultSubscriptionId; - @NonNull public final RegisteredState registered; - @Nullable public final byte[] profileKey; - @Nullable public final String profileAvatar; - public final boolean profileSharing; - public final boolean systemContact; + @Nullable final String name; + @Nullable final String customLabel; + @Nullable final Uri systemContactPhoto; + @Nullable final Uri contactUri; + @Nullable final Long groupAvatarId; + @Nullable final MaterialColor color; + @Nullable final Uri ringtone; + final long mutedUntil; + @Nullable final VibrateState vibrateState; + final boolean blocked; + final int expireMessages; + @NonNull final List participants; + @Nullable final String profileName; + final boolean seenInviteReminder; + final Optional defaultSubscriptionId; + @NonNull final RegisteredState registered; + @Nullable final byte[] profileKey; + @Nullable final String profileAvatar; + final boolean profileSharing; + final boolean systemContact; - public RecipientDetails(@Nullable String name, @Nullable String customLabel, - @Nullable Uri contactUri, @Nullable ContactPhoto avatar, - @NonNull FallbackContactPhoto fallbackAvatar, - boolean systemContact, @Nullable RecipientSettings settings, - @Nullable List participants) + RecipientDetails(@Nullable String name, @Nullable Long groupAvatarId, + boolean systemContact, @Nullable RecipientSettings settings, + @Nullable List participants) { - this.customLabel = customLabel; - this.avatar = avatar; - this.fallbackAvatar = fallbackAvatar; - this.contactUri = contactUri; + this.groupAvatarId = groupAvatarId; + this.systemContactPhoto = settings != null ? Util.uri(settings.getSystemContactPhotoUri()) : null; + this.customLabel = settings != null ? settings.getSystemPhoneLabel() : null; + this.contactUri = settings != null ? Util.uri(settings.getSystemContactUri()) : null; this.color = settings != null ? settings.getColor() : null; this.ringtone = settings != null ? settings.getRingtone() : null; this.mutedUntil = settings != null ? settings.getMuteUntil() : 0; 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.participants = participants == null ? new LinkedList() : participants; + this.participants = participants == null ? new LinkedList<>() : participants; 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.registered = settings != null ? settings.getRegistered() : RegisteredState.UNKNOWN; this.profileKey = settings != null ? settings.getProfileKey() : null; this.profileAvatar = settings != null ? settings.getProfileAvatar() : null; - this.profileSharing = settings != null && settings.isProfileSharing(); + this.profileSharing = settings != null && settings.isProfileSharing(); this.systemContact = systemContact; if (name == null && settings != null) this.name = settings.getSystemDisplayName(); @@ -260,7 +198,7 @@ class RecipientProvider { private static class RecipientCache { - private final Map cache = new LRUCache<>(1000); + private final Map cache = new SoftHashMap<>(1000); public synchronized Recipient get(Address address) { return cache.get(address); diff --git a/src/org/thoughtcrime/securesms/util/DirectoryHelper.java b/src/org/thoughtcrime/securesms/util/DirectoryHelper.java index 9f393c80e3..b66a6964b2 100644 --- a/src/org/thoughtcrime/securesms/util/DirectoryHelper.java +++ b/src/org/thoughtcrime/securesms/util/DirectoryHelper.java @@ -7,6 +7,7 @@ import android.content.ContentResolver; import android.content.Context; import android.content.OperationApplicationException; import android.database.Cursor; +import android.net.Uri; import android.os.RemoteException; import android.provider.ContactsContract; import android.support.annotation.NonNull; @@ -78,35 +79,34 @@ public class DirectoryHelper { } RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context); - Stream eligibleRecipientDatabaseContactNumbers = Stream.of(recipientDatabase.getAllRecipients()).map(recipient -> recipient.getAddress().serialize()); + Stream eligibleRecipientDatabaseContactNumbers = Stream.of(recipientDatabase.getAllAddresses()).filter(Address::isPhone).map(Address::toPhoneString); Stream eligibleSystemDatabaseContactNumbers = Stream.of(ContactAccessor.getInstance().getAllContactsWithNumbers(context)).map(Address::serialize); Set eligibleContactNumbers = Stream.concat(eligibleRecipientDatabaseContactNumbers, eligibleSystemDatabaseContactNumbers).collect(Collectors.toSet()); List activeTokens = accountManager.getContacts(eligibleContactNumbers); if (activeTokens != null) { - List activeRecipients = new LinkedList<>(); - List inactiveRecipients = new LinkedList<>(); + List
activeAddresses = new LinkedList<>(); + List
inactiveAddresses = new LinkedList<>(); Set inactiveContactNumbers = new HashSet<>(eligibleContactNumbers); for (ContactTokenDetails activeToken : activeTokens) { - activeRecipients.add(Recipient.from(context, Address.fromSerialized(activeToken.getNumber()), true)); + activeAddresses.add(Address.fromSerialized(activeToken.getNumber())); inactiveContactNumbers.remove(activeToken.getNumber()); } for (String inactiveContactNumber : inactiveContactNumbers) { - inactiveRecipients.add(Recipient.from(context, Address.fromSerialized(inactiveContactNumber), true)); + inactiveAddresses.add(Address.fromSerialized(inactiveContactNumber)); } Set
currentActiveAddresses = new HashSet<>(recipientDatabase.getRegistered()); - List
newlyActiveAddresses = Stream.of(activeRecipients) - .map(Recipient::getAddress) + List
newlyActiveAddresses = Stream.of(activeAddresses) .filter(address -> !currentActiveAddresses.contains(address)) .toList(); - recipientDatabase.setRegistered(activeRecipients, inactiveRecipients); - updateContactsDatabase(context, Stream.of(activeRecipients).map(Recipient::getAddress).toList(), true); + recipientDatabase.setRegistered(activeAddresses, inactiveAddresses); + updateContactsDatabase(context, activeAddresses, true); if (TextSecurePreferences.hasSuccessfullyRetrievedDirectory(context)) { return newlyActiveAddresses; @@ -160,18 +160,21 @@ public class DirectoryHelper { DatabaseFactory.getContactsDatabase(context).setRegisteredUsers(account.get().getAccount(), activeAddresses, removeMissing); Cursor cursor = ContactAccessor.getInstance().getAllSystemContacts(context); - RecipientDatabase.BulkOperationsHandle handle = DatabaseFactory.getRecipientDatabase(context).resetAllDisplayNames(); + RecipientDatabase.BulkOperationsHandle handle = DatabaseFactory.getRecipientDatabase(context).resetAllSystemContactInfo(); try { while (cursor != null && cursor.moveToNext()) { String number = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER)); if (!TextUtils.isEmpty(number)) { - Address address = Address.fromExternal(context, number); - Recipient recipient = Recipient.from(context, address, true); - String displayName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)); + Address address = Address.fromExternal(context, number); + String displayName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)); + String contactPhotoUri = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.PHOTO_URI)); + String contactLabel = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.LABEL)); + Uri contactUri = ContactsContract.Contacts.getLookupUri(cursor.getLong(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone._ID)), + cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY))); - handle.setDisplayName(recipient, displayName); + handle.setSystemContactInfo(address, displayName, contactPhotoUri, contactLabel, contactUri.toString()); } } } finally { @@ -247,6 +250,7 @@ public class DirectoryHelper { this.account = account; } + @SuppressWarnings("unused") public boolean isFresh() { return fresh; } diff --git a/src/org/thoughtcrime/securesms/util/LinkedBlockingLifoQueue.java b/src/org/thoughtcrime/securesms/util/LinkedBlockingLifoQueue.java index c7cd36b853..1b07afee64 100644 --- a/src/org/thoughtcrime/securesms/util/LinkedBlockingLifoQueue.java +++ b/src/org/thoughtcrime/securesms/util/LinkedBlockingLifoQueue.java @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.util; -import org.thoughtcrime.securesms.util.deque.LinkedBlockingDeque; + +import java.util.concurrent.LinkedBlockingDeque; public class LinkedBlockingLifoQueue extends LinkedBlockingDeque { @Override diff --git a/src/org/thoughtcrime/securesms/util/SoftHashMap.java b/src/org/thoughtcrime/securesms/util/SoftHashMap.java new file mode 100644 index 0000000000..64553f5963 --- /dev/null +++ b/src/org/thoughtcrime/securesms/util/SoftHashMap.java @@ -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 SoftHashMap is a memory-constrained map that stores its values in + * {@link SoftReference SoftReference}s. (Contrast this with the JDK's + * {@link WeakHashMap WeakHashMap}, which uses weak references for its keys, which is of little value if you + * want the cache to auto-resize itself based on memory constraints). + *

+ * 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. + *

+ * This class is a generics-enabled Map based on initial ideas from Heinz Kabutz's and Sydney Redelinghuys's + * publicly posted version (with their approval), with + * continued modifications. + *

+ * This implementation is thread-safe and usable in concurrent environments. + * + * @since 1.0 + */ +public class SoftHashMap implements Map { + + /** + * 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> 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 strongReferences; //guarded by 'strongReferencesLock' + private final ReentrantLock strongReferencesLock; + + /** + * Reference queue for cleared SoftReference objects. + */ + private final ReferenceQueue 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. + *

+ * 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. + *

+ * Note that in a highly concurrent environments the exact total number of strong references may differ slightly + * than the actual retentionSize 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(); + strongReferencesLock = new ReentrantLock(); + map = new ConcurrentHashMap>(); + strongReferences = new ConcurrentLinkedQueue(); + } + + /** + * 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 source) { + this(DEFAULT_RETENTION_SIZE); + putAll(source); + } + + /** + * Creates a {@code SoftHashMap} backed by the specified {@code source}, with the specified retention size. + *

+ * 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. + *

+ * Note that in a highly concurrent environments the exact total number of strong references may differ slightly + * than the actual retentionSize 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 source, int retentionSize) { + this(retentionSize); + putAll(source); + } + + public V get(Object key) { + processQueue(); + + V result = null; + SoftValue 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 m) { + if (m == null || m.isEmpty()) { + processQueue(); + return; + } + for (Map.Entry entry : m.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + public Set keySet() { + processQueue(); + return map.keySet(); + } + + public Collection values() { + processQueue(); + Collection keys = map.keySet(); + if (keys.isEmpty()) { + //noinspection unchecked + return Collections.EMPTY_SET; + } + Collection values = new ArrayList(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 sv = new SoftValue(value, key, queue); + SoftValue 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 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> entrySet() { + processQueue(); // throw out garbage collected values first + Collection keys = map.keySet(); + if (keys.isEmpty()) { + //noinspection unchecked + return Collections.EMPTY_SET; + } + + Map kvPairs = new HashMap(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 extends SoftReference { + + 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 queue) { + super(value, queue); + this.key = key; + } + + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/util/Util.java b/src/org/thoughtcrime/securesms/util/Util.java index 9656aa2ef1..c20b707f35 100644 --- a/src/org/thoughtcrime/securesms/util/Util.java +++ b/src/org/thoughtcrime/securesms/util/Util.java @@ -24,6 +24,7 @@ import android.content.ClipboardManager; import android.content.Context; import android.content.pm.PackageManager; import android.graphics.Typeface; +import android.net.Uri; import android.os.Build; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; @@ -422,6 +423,11 @@ public class Util { 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) public static boolean isLowMemory(Context context) { ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); diff --git a/src/org/thoughtcrime/securesms/util/deque/BlockingDeque.java b/src/org/thoughtcrime/securesms/util/deque/BlockingDeque.java deleted file mode 100644 index a486fbd7ce..0000000000 --- a/src/org/thoughtcrime/securesms/util/deque/BlockingDeque.java +++ /dev/null @@ -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. - * - *

BlockingDeque 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 - * null or false, 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: - * - *

- * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
First Element (Head)
Throws exceptionSpecial valueBlocksTimes out
Insert{@link #addFirst addFirst(e)}{@link #offerFirst offerFirst(e)}{@link #putFirst putFirst(e)}{@link #offerFirst offerFirst(e, time, unit)}
Remove{@link #removeFirst removeFirst()}{@link #pollFirst pollFirst()}{@link #takeFirst takeFirst()}{@link #pollFirst(long, TimeUnit) pollFirst(time, unit)}
Examine{@link #getFirst getFirst()}{@link #peekFirst peekFirst()}not applicablenot applicable
Last Element (Tail)
Throws exceptionSpecial valueBlocksTimes out
Insert{@link #addLast addLast(e)}{@link #offerLast offerLast(e)}{@link #putLast putLast(e)}{@link #offerLast offerLast(e, time, unit)}
Remove{@link #removeLast() removeLast()}{@link #pollLast() pollLast()}{@link #takeLast takeLast()}{@link #pollLast(long, TimeUnit) pollLast(time, unit)}
Examine{@link #getLast getLast()}{@link #peekLast peekLast()}not applicablenot applicable
- * - *

Like any {@link BlockingQueue}, a BlockingDeque is thread safe, - * does not permit null elements, and may (or may not) be - * capacity-constrained. - * - *

A BlockingDeque implementation may be used directly as a FIFO - * BlockingQueue. The methods inherited from the - * BlockingQueue interface are precisely equivalent to - * BlockingDeque methods as indicated in the following table: - * - *

- * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
BlockingQueue Method Equivalent BlockingDeque Method
Insert
{@link #add add(e)}{@link #addLast addLast(e)}
{@link #offer offer(e)}{@link #offerLast offerLast(e)}
{@link #put put(e)}{@link #putLast putLast(e)}
{@link #offer offer(e, time, unit)}{@link #offerLast offerLast(e, time, unit)}
Remove
{@link #remove() remove()}{@link #removeFirst() removeFirst()}
{@link #poll() poll()}{@link #pollFirst() pollFirst()}
{@link #take() take()}{@link #takeFirst() takeFirst()}
{@link #poll(long, TimeUnit) poll(time, unit)}{@link #pollFirst(long, TimeUnit) pollFirst(time, unit)}
Examine
{@link #element() element()}{@link #getFirst() getFirst()}
{@link #peek() peek()}{@link #peekFirst() peekFirst()}
- * - *

Memory consistency effects: As with other concurrent - * collections, actions in a thread prior to placing an object into a - * {@code BlockingDeque} - * happen-before - * actions subsequent to the access or removal of that element from - * the {@code BlockingDeque} in another thread. - * - *

This interface is a member of the - * - * Java Collections Framework. - * - * @since 1.6 - * @author Doug Lea - * @param the type of elements held in this collection - */ -public interface BlockingDeque extends BlockingQueue, Deque { - /* - * 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 IllegalStateException 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 IllegalStateException 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 true upon success and false 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 true upon success and false 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 - * unit - * @param unit a TimeUnit determining how to interpret the - * timeout parameter - * @return true if successful, or false 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 - * unit - * @param unit a TimeUnit determining how to interpret the - * timeout parameter - * @return true if successful, or false 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 - * unit - * @param unit a TimeUnit determining how to interpret the - * timeout parameter - * @return the head of this deque, or null 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 - * unit - * @param unit a TimeUnit determining how to interpret the - * timeout parameter - * @return the tail of this deque, or null 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 e such that - * o.equals(e) (if such an element exists). - * Returns true 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 true 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 e such that - * o.equals(e) (if such an element exists). - * Returns true 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 true 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 - * true upon success and throwing an - * IllegalStateException if no space is currently available. - * When using a capacity-restricted deque, it is generally preferable to - * use {@link #offer offer}. - * - *

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 - * true upon success and false 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. - * - *

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. - * - *

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. - * - *

This method is equivalent to - * {@link #offerLast offerLast}. - * - * @param e the element to add - * @return true if the element was added to this deque, else - * false - * @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. - * - *

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 - * null if this deque is empty. - * - *

This method is equivalent to {@link #pollFirst()}. - * - * @return the head of this deque, or null 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. - * - *

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. - * - *

This method is equivalent to - * {@link #pollFirst(long,TimeUnit) pollFirst}. - * - * @return the head of this deque, or null 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. - * - *

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 null if this deque is empty. - * - *

This method is equivalent to {@link #peekFirst() peekFirst}. - * - * @return the head of this deque, or null 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 e such that - * o.equals(e) (if such an element exists). - * Returns true if this deque contained the specified element - * (or equivalently, if this deque changed as a result of the call). - * - *

This method is equivalent to - * {@link #removeFirstOccurrence removeFirstOccurrence}. - * - * @param o element to be removed from this deque, if present - * @return true 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 true if this deque contains the specified element. - * More formally, returns true if and only if this deque contains - * at least one element e such that o.equals(e). - * - * @param o object to be checked for containment in this deque - * @return true 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 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. - * - *

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); -} diff --git a/src/org/thoughtcrime/securesms/util/deque/Deque.java b/src/org/thoughtcrime/securesms/util/deque/Deque.java deleted file mode 100644 index c260941c98..0000000000 --- a/src/org/thoughtcrime/securesms/util/deque/Deque.java +++ /dev/null @@ -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 deque is short for "double ended queue" - * and is usually pronounced "deck". Most Deque - * 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. - * - *

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 null or false, depending on - * the operation). The latter form of the insert operation is - * designed specifically for use with capacity-restricted - * Deque implementations; in most implementations, insert - * operations cannot fail. - * - *

The twelve methods described above are summarized in the - * following table: - * - *

- * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
First Element (Head) Last Element (Tail)
Throws exceptionSpecial valueThrows exceptionSpecial value
Insert{@link #addFirst addFirst(e)}{@link #offerFirst offerFirst(e)}{@link #addLast addLast(e)}{@link #offerLast offerLast(e)}
Remove{@link #removeFirst removeFirst()}{@link #pollFirst pollFirst()}{@link #removeLast removeLast()}{@link #pollLast pollLast()}
Examine{@link #getFirst getFirst()}{@link #peekFirst peekFirst()}{@link #getLast getLast()}{@link #peekLast peekLast()}
- * - *

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 Queue interface are precisely equivalent to - * Deque methods as indicated in the following table: - * - *

- * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
Queue Method Equivalent Deque Method
{@link java.util.Queue#add add(e)}{@link #addLast addLast(e)}
{@link java.util.Queue#offer offer(e)}{@link #offerLast offerLast(e)}
{@link java.util.Queue#remove remove()}{@link #removeFirst removeFirst()}
{@link java.util.Queue#poll poll()}{@link #pollFirst pollFirst()}
{@link java.util.Queue#element element()}{@link #getFirst getFirst()}
{@link java.util.Queue#peek peek()}{@link #peek peekFirst()}
- * - *

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 - * Deque methods as indicated in the table below: - * - *

- * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
Stack Method Equivalent Deque Method
{@link #push push(e)}{@link #addFirst addFirst(e)}
{@link #pop pop()}{@link #removeFirst removeFirst()}
{@link #peek peek()}{@link #peekFirst peekFirst()}
- * - *

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. - * - *

This interface provides two methods to remove interior - * elements, {@link #removeFirstOccurrence removeFirstOccurrence} and - * {@link #removeLastOccurrence removeLastOccurrence}. - * - *

Unlike the {@link List} interface, this interface does not - * provide support for indexed access to elements. - * - *

While Deque implementations are not strictly required - * to prohibit the insertion of null elements, they are strongly - * encouraged to do so. Users of any Deque implementations - * that do allow null elements are strongly encouraged not to - * take advantage of the ability to insert nulls. This is so because - * null is used as a special return value by various methods - * to indicated that the deque is empty. - * - *

Deque implementations generally do not define - * element-based versions of the equals and hashCode - * methods, but instead inherit the identity-based versions from class - * Object. - * - * @author Doug Lea - * @author Josh Bloch - * @since 1.6 - * @param the type of elements held in this collection - */ - -public interface Deque extends Queue { - /** - * 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}. - * - *

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 true if the element was added to this deque, else - * false - * @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 true if the element was added to this deque, else - * false - * @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 null if this deque is empty. - * - * @return the head of this deque, or null if this deque is empty - */ - E pollFirst(); - - /** - * Retrieves and removes the last element of this deque, - * or returns null if this deque is empty. - * - * @return the tail of this deque, or null 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 null if this deque is empty. - * - * @return the head of this deque, or null if this deque is empty - */ - E peekFirst(); - - /** - * Retrieves, but does not remove, the last element of this deque, - * or returns null if this deque is empty. - * - * @return the tail of this deque, or null 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 e such that - * (o==null ? e==null : o.equals(e)) - * (if such an element exists). - * Returns true 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 true 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 e such that - * (o==null ? e==null : o.equals(e)) - * (if such an element exists). - * Returns true 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 true 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 - * true upon success and throwing an - * IllegalStateException if no space is currently available. - * When using a capacity-restricted deque, it is generally preferable to - * use {@link #offer offer}. - * - *

This method is equivalent to {@link #addLast}. - * - * @param e the element to add - * @return true (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 - * true upon success and false 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. - * - *

This method is equivalent to {@link #offerLast}. - * - * @param e the element to add - * @return true if the element was added to this deque, else - * false - * @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. - * - *

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 - * null if this deque is empty. - * - *

This method is equivalent to {@link #pollFirst()}. - * - * @return the first element of this deque, or null 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. - * - *

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 null if this deque is empty. - * - *

This method is equivalent to {@link #peekFirst()}. - * - * @return the head of the queue represented by this deque, or - * null 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 - * true upon success and throwing an - * IllegalStateException if no space is currently available. - * - *

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. - * - *

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 e such that - * (o==null ? e==null : o.equals(e)) - * (if such an element exists). - * Returns true if this deque contained the specified element - * (or equivalently, if this deque changed as a result of the call). - * - *

This method is equivalent to {@link #removeFirstOccurrence}. - * - * @param o element to be removed from this deque, if present - * @return true 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 true if this deque contains the specified element. - * More formally, returns true if and only if this deque contains - * at least one element e such that - * (o==null ? e==null : o.equals(e)). - * - * @param o element whose presence in this deque is to be tested - * @return true 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 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 descendingIterator(); - -} diff --git a/src/org/thoughtcrime/securesms/util/deque/LinkedBlockingDeque.java b/src/org/thoughtcrime/securesms/util/deque/LinkedBlockingDeque.java deleted file mode 100644 index de006ea58a..0000000000 --- a/src/org/thoughtcrime/securesms/util/deque/LinkedBlockingDeque.java +++ /dev/null @@ -1,1190 +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 - */ - -/* - * This wasn't included until Android API level 9, so we're duplicating - * it here for backwards compatibility. - */ - -package org.thoughtcrime.securesms.util.deque; - -import java.util.AbstractQueue; -import java.util.Collection; -import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.Condition; -import java.util.concurrent.locks.ReentrantLock; - -/** - * An optionally-bounded {@linkplain BlockingDeque blocking deque} based on - * linked nodes. - * - *

The optional capacity bound constructor argument serves as a - * way to prevent excessive expansion. The capacity, if unspecified, - * is equal to {@link Integer#MAX_VALUE}. Linked nodes are - * dynamically created upon each insertion unless this would bring the - * deque above capacity. - * - *

Most operations run in constant time (ignoring time spent - * blocking). Exceptions include {@link #remove(Object) remove}, - * {@link #removeFirstOccurrence removeFirstOccurrence}, {@link - * #removeLastOccurrence removeLastOccurrence}, {@link #contains - * contains}, {@link #iterator iterator.remove()}, and the bulk - * operations, all of which run in linear time. - * - *

This class and its iterator implement all of the - * optional methods of the {@link Collection} and {@link - * Iterator} interfaces. - * - *

This class is a member of the - * - * Java Collections Framework. - * - * @since 1.6 - * @author Doug Lea - * @param the type of elements held in this collection - */ -public class LinkedBlockingDeque - extends AbstractQueue - implements BlockingDeque, java.io.Serializable { - - /* - * Implemented as a simple doubly-linked list protected by a - * single lock and using conditions to manage blocking. - * - * To implement weakly consistent iterators, it appears we need to - * keep all Nodes GC-reachable from a predecessor dequeued Node. - * That would cause two problems: - * - allow a rogue Iterator to cause unbounded memory retention - * - cause cross-generational linking of old Nodes to new Nodes if - * a Node was tenured while live, which generational GCs have a - * hard time dealing with, causing repeated major collections. - * However, only non-deleted Nodes need to be reachable from - * dequeued Nodes, and reachability does not necessarily have to - * be of the kind understood by the GC. We use the trick of - * linking a Node that has just been dequeued to itself. Such a - * self-link implicitly means to jump to "first" (for next links) - * or "last" (for prev links). - */ - - /* - * We have "diamond" multiple interface/abstract class inheritance - * here, and that introduces ambiguities. Often we want the - * BlockingDeque javadoc combined with the AbstractQueue - * implementation, so a lot of method specs are duplicated here. - */ - - private static final long serialVersionUID = -387911632671998426L; - - /** Doubly-linked list node class */ - static final class Node { - /** - * The item, or null if this node has been removed. - */ - E item; - - /** - * One of: - * - the real predecessor Node - * - this Node, meaning the predecessor is tail - * - null, meaning there is no predecessor - */ - Node prev; - - /** - * One of: - * - the real successor Node - * - this Node, meaning the successor is head - * - null, meaning there is no successor - */ - Node next; - - Node(E x) { - item = x; - } - } - - /** - * Pointer to first node. - * Invariant: (first == null && last == null) || - * (first.prev == null && first.item != null) - */ - transient Node first; - - /** - * Pointer to last node. - * Invariant: (first == null && last == null) || - * (last.next == null && last.item != null) - */ - transient Node last; - - /** Number of items in the deque */ - private transient int count; - - /** Maximum number of items in the deque */ - private final int capacity; - - /** Main lock guarding all access */ - final ReentrantLock lock = new ReentrantLock(); - - /** Condition for waiting takes */ - private final Condition notEmpty = lock.newCondition(); - - /** Condition for waiting puts */ - private final Condition notFull = lock.newCondition(); - - /** - * Creates a {@code LinkedBlockingDeque} with a capacity of - * {@link Integer#MAX_VALUE}. - */ - public LinkedBlockingDeque() { - this(Integer.MAX_VALUE); - } - - /** - * Creates a {@code LinkedBlockingDeque} with the given (fixed) capacity. - * - * @param capacity the capacity of this deque - * @throws IllegalArgumentException if {@code capacity} is less than 1 - */ - public LinkedBlockingDeque(int capacity) { - if (capacity <= 0) throw new IllegalArgumentException(); - this.capacity = capacity; - } - - /** - * Creates a {@code LinkedBlockingDeque} with a capacity of - * {@link Integer#MAX_VALUE}, initially containing the elements of - * the given collection, added in traversal order of the - * collection's iterator. - * - * @param c the collection of elements to initially contain - * @throws NullPointerException if the specified collection or any - * of its elements are null - */ - public LinkedBlockingDeque(Collection c) { - this(Integer.MAX_VALUE); - final ReentrantLock lock = this.lock; - lock.lock(); // Never contended, but necessary for visibility - try { - for (E e : c) { - if (e == null) - throw new NullPointerException(); - if (!linkLast(new Node(e))) - throw new IllegalStateException("Deque full"); - } - } finally { - lock.unlock(); - } - } - - - // Basic linking and unlinking operations, called only while holding lock - - /** - * Links node as first element, or returns false if full. - */ - private boolean linkFirst(Node node) { - // assert lock.isHeldByCurrentThread(); - if (count >= capacity) - return false; - Node f = first; - node.next = f; - first = node; - if (last == null) - last = node; - else - f.prev = node; - ++count; - notEmpty.signal(); - return true; - } - - /** - * Links node as last element, or returns false if full. - */ - private boolean linkLast(Node node) { - // assert lock.isHeldByCurrentThread(); - if (count >= capacity) - return false; - Node l = last; - node.prev = l; - last = node; - if (first == null) - first = node; - else - l.next = node; - ++count; - notEmpty.signal(); - return true; - } - - /** - * Removes and returns first element, or null if empty. - */ - private E unlinkFirst() { - // assert lock.isHeldByCurrentThread(); - Node f = first; - if (f == null) - return null; - Node n = f.next; - E item = f.item; - f.item = null; - f.next = f; // help GC - first = n; - if (n == null) - last = null; - else - n.prev = null; - --count; - notFull.signal(); - return item; - } - - /** - * Removes and returns last element, or null if empty. - */ - private E unlinkLast() { - // assert lock.isHeldByCurrentThread(); - Node l = last; - if (l == null) - return null; - Node p = l.prev; - E item = l.item; - l.item = null; - l.prev = l; // help GC - last = p; - if (p == null) - first = null; - else - p.next = null; - --count; - notFull.signal(); - return item; - } - - /** - * Unlinks x. - */ - void unlink(Node x) { - // assert lock.isHeldByCurrentThread(); - Node p = x.prev; - Node n = x.next; - if (p == null) { - unlinkFirst(); - } else if (n == null) { - unlinkLast(); - } else { - p.next = n; - n.prev = p; - x.item = null; - // Don't mess with x's links. They may still be in use by - // an iterator. - --count; - notFull.signal(); - } - } - - // BlockingDeque methods - - /** - * @throws IllegalStateException {@inheritDoc} - * @throws NullPointerException {@inheritDoc} - */ - public void addFirst(E e) { - if (!offerFirst(e)) - throw new IllegalStateException("Deque full"); - } - - /** - * @throws IllegalStateException {@inheritDoc} - * @throws NullPointerException {@inheritDoc} - */ - public void addLast(E e) { - if (!offerLast(e)) - throw new IllegalStateException("Deque full"); - } - - /** - * @throws NullPointerException {@inheritDoc} - */ - public boolean offerFirst(E e) { - if (e == null) throw new NullPointerException(); - Node node = new Node(e); - final ReentrantLock lock = this.lock; - lock.lock(); - try { - return linkFirst(node); - } finally { - lock.unlock(); - } - } - - /** - * @throws NullPointerException {@inheritDoc} - */ - public boolean offerLast(E e) { - if (e == null) throw new NullPointerException(); - Node node = new Node(e); - final ReentrantLock lock = this.lock; - lock.lock(); - try { - return linkLast(node); - } finally { - lock.unlock(); - } - } - - /** - * @throws NullPointerException {@inheritDoc} - * @throws InterruptedException {@inheritDoc} - */ - public void putFirst(E e) throws InterruptedException { - if (e == null) throw new NullPointerException(); - Node node = new Node(e); - final ReentrantLock lock = this.lock; - lock.lock(); - try { - while (!linkFirst(node)) - notFull.await(); - } finally { - lock.unlock(); - } - } - - /** - * @throws NullPointerException {@inheritDoc} - * @throws InterruptedException {@inheritDoc} - */ - public void putLast(E e) throws InterruptedException { - if (e == null) throw new NullPointerException(); - Node node = new Node(e); - final ReentrantLock lock = this.lock; - lock.lock(); - try { - while (!linkLast(node)) - notFull.await(); - } finally { - lock.unlock(); - } - } - - /** - * @throws NullPointerException {@inheritDoc} - * @throws InterruptedException {@inheritDoc} - */ - public boolean offerFirst(E e, long timeout, TimeUnit unit) - throws InterruptedException { - if (e == null) throw new NullPointerException(); - Node node = new Node(e); - long nanos = unit.toNanos(timeout); - final ReentrantLock lock = this.lock; - lock.lockInterruptibly(); - try { - while (!linkFirst(node)) { - if (nanos <= 0) - return false; - nanos = notFull.awaitNanos(nanos); - } - return true; - } finally { - lock.unlock(); - } - } - - /** - * @throws NullPointerException {@inheritDoc} - * @throws InterruptedException {@inheritDoc} - */ - public boolean offerLast(E e, long timeout, TimeUnit unit) - throws InterruptedException { - if (e == null) throw new NullPointerException(); - Node node = new Node(e); - long nanos = unit.toNanos(timeout); - final ReentrantLock lock = this.lock; - lock.lockInterruptibly(); - try { - while (!linkLast(node)) { - if (nanos <= 0) - return false; - nanos = notFull.awaitNanos(nanos); - } - return true; - } finally { - lock.unlock(); - } - } - - /** - * @throws NoSuchElementException {@inheritDoc} - */ - public E removeFirst() { - E x = pollFirst(); - if (x == null) throw new NoSuchElementException(); - return x; - } - - /** - * @throws NoSuchElementException {@inheritDoc} - */ - public E removeLast() { - E x = pollLast(); - if (x == null) throw new NoSuchElementException(); - return x; - } - - public E pollFirst() { - final ReentrantLock lock = this.lock; - lock.lock(); - try { - return unlinkFirst(); - } finally { - lock.unlock(); - } - } - - public E pollLast() { - final ReentrantLock lock = this.lock; - lock.lock(); - try { - return unlinkLast(); - } finally { - lock.unlock(); - } - } - - public E takeFirst() throws InterruptedException { - final ReentrantLock lock = this.lock; - lock.lock(); - try { - E x; - while ( (x = unlinkFirst()) == null) - notEmpty.await(); - return x; - } finally { - lock.unlock(); - } - } - - public E takeLast() throws InterruptedException { - final ReentrantLock lock = this.lock; - lock.lock(); - try { - E x; - while ( (x = unlinkLast()) == null) - notEmpty.await(); - return x; - } finally { - lock.unlock(); - } - } - - public E pollFirst(long timeout, TimeUnit unit) - throws InterruptedException { - long nanos = unit.toNanos(timeout); - final ReentrantLock lock = this.lock; - lock.lockInterruptibly(); - try { - E x; - while ( (x = unlinkFirst()) == null) { - if (nanos <= 0) - return null; - nanos = notEmpty.awaitNanos(nanos); - } - return x; - } finally { - lock.unlock(); - } - } - - public E pollLast(long timeout, TimeUnit unit) - throws InterruptedException { - long nanos = unit.toNanos(timeout); - final ReentrantLock lock = this.lock; - lock.lockInterruptibly(); - try { - E x; - while ( (x = unlinkLast()) == null) { - if (nanos <= 0) - return null; - nanos = notEmpty.awaitNanos(nanos); - } - return x; - } finally { - lock.unlock(); - } - } - - /** - * @throws NoSuchElementException {@inheritDoc} - */ - public E getFirst() { - E x = peekFirst(); - if (x == null) throw new NoSuchElementException(); - return x; - } - - /** - * @throws NoSuchElementException {@inheritDoc} - */ - public E getLast() { - E x = peekLast(); - if (x == null) throw new NoSuchElementException(); - return x; - } - - public E peekFirst() { - final ReentrantLock lock = this.lock; - lock.lock(); - try { - return (first == null) ? null : first.item; - } finally { - lock.unlock(); - } - } - - public E peekLast() { - final ReentrantLock lock = this.lock; - lock.lock(); - try { - return (last == null) ? null : last.item; - } finally { - lock.unlock(); - } - } - - public boolean removeFirstOccurrence(Object o) { - if (o == null) return false; - final ReentrantLock lock = this.lock; - lock.lock(); - try { - for (Node p = first; p != null; p = p.next) { - if (o.equals(p.item)) { - unlink(p); - return true; - } - } - return false; - } finally { - lock.unlock(); - } - } - - public boolean removeLastOccurrence(Object o) { - if (o == null) return false; - final ReentrantLock lock = this.lock; - lock.lock(); - try { - for (Node p = last; p != null; p = p.prev) { - if (o.equals(p.item)) { - unlink(p); - return true; - } - } - return false; - } finally { - lock.unlock(); - } - } - - // BlockingQueue methods - - /** - * Inserts the specified element at the end of this deque unless it would - * violate capacity restrictions. When using a capacity-restricted deque, - * it is generally preferable to use method {@link #offer offer}. - * - *

This method is equivalent to {@link #addLast}. - * - * @throws IllegalStateException if the element cannot be added at this - * time due to capacity restrictions - * @throws NullPointerException if the specified element is null - */ - @Override - public boolean add(E e) { - addLast(e); - return true; - } - - /** - * @throws NullPointerException if the specified element is null - */ - public boolean offer(E e) { - return offerLast(e); - } - - /** - * @throws NullPointerException {@inheritDoc} - * @throws InterruptedException {@inheritDoc} - */ - public void put(E e) throws InterruptedException { - putLast(e); - } - - /** - * @throws NullPointerException {@inheritDoc} - * @throws InterruptedException {@inheritDoc} - */ - public boolean offer(E e, long timeout, TimeUnit unit) - throws InterruptedException { - return offerLast(e, timeout, unit); - } - - /** - * Retrieves and removes the head of the queue represented by this deque. - * This method differs from {@link #poll poll} only in that it throws an - * exception if this deque is empty. - * - *

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 - */ - @Override - public E remove() { - return removeFirst(); - } - - public E poll() { - return pollFirst(); - } - - public E take() throws InterruptedException { - return takeFirst(); - } - - public E poll(long timeout, TimeUnit unit) throws InterruptedException { - return pollFirst(timeout, unit); - } - - /** - * Retrieves, but does not remove, the head of the queue represented by - * this deque. This method differs from {@link #peek peek} only in that - * it throws an exception if this deque is empty. - * - *

This method is equivalent to {@link #getFirst() getFirst}. - * - * @return the head of the queue represented by this deque - * @throws NoSuchElementException if this deque is empty - */ - @Override - public E element() { - return getFirst(); - } - - public E peek() { - return peekFirst(); - } - - /** - * Returns the number of additional elements that this deque can ideally - * (in the absence of memory or resource constraints) accept without - * blocking. This is always equal to the initial capacity of this deque - * less the current {@code size} of this deque. - * - *

Note that you cannot always tell if an attempt to insert - * an element will succeed by inspecting {@code remainingCapacity} - * because it may be the case that another thread is about to - * insert or remove an element. - */ - public int remainingCapacity() { - final ReentrantLock lock = this.lock; - lock.lock(); - try { - return capacity - count; - } finally { - lock.unlock(); - } - } - - /** - * @throws UnsupportedOperationException {@inheritDoc} - * @throws ClassCastException {@inheritDoc} - * @throws NullPointerException {@inheritDoc} - * @throws IllegalArgumentException {@inheritDoc} - */ - public int drainTo(Collection c) { - return drainTo(c, Integer.MAX_VALUE); - } - - /** - * @throws UnsupportedOperationException {@inheritDoc} - * @throws ClassCastException {@inheritDoc} - * @throws NullPointerException {@inheritDoc} - * @throws IllegalArgumentException {@inheritDoc} - */ - public int drainTo(Collection c, int maxElements) { - if (c == null) - throw new NullPointerException(); - if (c == this) - throw new IllegalArgumentException(); - final ReentrantLock lock = this.lock; - lock.lock(); - try { - int n = Math.min(maxElements, count); - for (int i = 0; i < n; i++) { - c.add(first.item); // In this order, in case add() throws. - unlinkFirst(); - } - return n; - } finally { - lock.unlock(); - } - } - - // Stack methods - - /** - * @throws IllegalStateException {@inheritDoc} - * @throws NullPointerException {@inheritDoc} - */ - public void push(E e) { - addFirst(e); - } - - /** - * @throws NoSuchElementException {@inheritDoc} - */ - public E pop() { - return removeFirst(); - } - - // 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 {@code e} such that - * {@code o.equals(e)} (if such an element exists). - * Returns {@code true} if this deque contained the specified element - * (or equivalently, if this deque changed as a result of the call). - * - *

This method is equivalent to - * {@link #removeFirstOccurrence(Object) removeFirstOccurrence}. - * - * @param o element to be removed from this deque, if present - * @return {@code true} if this deque changed as a result of the call - */ - @Override - public boolean remove(Object o) { - return removeFirstOccurrence(o); - } - - /** - * Returns the number of elements in this deque. - * - * @return the number of elements in this deque - */ - @Override - public int size() { - final ReentrantLock lock = this.lock; - lock.lock(); - try { - return count; - } finally { - lock.unlock(); - } - } - - /** - * Returns {@code true} if this deque contains the specified element. - * More formally, returns {@code true} if and only if this deque contains - * at least one element {@code e} such that {@code o.equals(e)}. - * - * @param o object to be checked for containment in this deque - * @return {@code true} if this deque contains the specified element - */ - @Override - public boolean contains(Object o) { - if (o == null) return false; - final ReentrantLock lock = this.lock; - lock.lock(); - try { - for (Node p = first; p != null; p = p.next) - if (o.equals(p.item)) - return true; - return false; - } finally { - lock.unlock(); - } - } - - /* - * TODO: Add support for more efficient bulk operations. - * - * We don't want to acquire the lock for every iteration, but we - * also want other threads a chance to interact with the - * collection, especially when count is close to capacity. - */ - -// /** -// * Adds all of the elements in the specified collection to this -// * queue. Attempts to addAll of a queue to itself result in -// * {@code IllegalArgumentException}. Further, the behavior of -// * this operation is undefined if the specified collection is -// * modified while the operation is in progress. -// * -// * @param c collection containing elements to be added to this queue -// * @return {@code true} if this queue changed as a result of the call -// * @throws ClassCastException {@inheritDoc} -// * @throws NullPointerException {@inheritDoc} -// * @throws IllegalArgumentException {@inheritDoc} -// * @throws IllegalStateException {@inheritDoc} -// * @see #add(Object) -// */ -// public boolean addAll(Collection c) { -// if (c == null) -// throw new NullPointerException(); -// if (c == this) -// throw new IllegalArgumentException(); -// final ReentrantLock lock = this.lock; -// lock.lock(); -// try { -// boolean modified = false; -// for (E e : c) -// if (linkLast(e)) -// modified = true; -// return modified; -// } finally { -// lock.unlock(); -// } -// } - - /** - * Returns an array containing all of the elements in this deque, in - * proper sequence (from first to last element). - * - *

The returned array will be "safe" in that no references to it are - * maintained by this deque. (In other words, this method must allocate - * a new array). The caller is thus free to modify the returned array. - * - *

This method acts as bridge between array-based and collection-based - * APIs. - * - * @return an array containing all of the elements in this deque - */ - @Override - @SuppressWarnings("unchecked") - public Object[] toArray() { - final ReentrantLock lock = this.lock; - lock.lock(); - try { - Object[] a = new Object[count]; - int k = 0; - for (Node p = first; p != null; p = p.next) - a[k++] = p.item; - return a; - } finally { - lock.unlock(); - } - } - - /** - * Returns an array containing all of the elements in this deque, in - * proper sequence; the runtime type of the returned array is that of - * the specified array. If the deque fits in the specified array, it - * is returned therein. Otherwise, a new array is allocated with the - * runtime type of the specified array and the size of this deque. - * - *

If this deque fits in the specified array with room to spare - * (i.e., the array has more elements than this deque), the element in - * the array immediately following the end of the deque is set to - * {@code null}. - * - *

Like the {@link #toArray()} method, this method acts as bridge between - * array-based and collection-based APIs. Further, this method allows - * precise control over the runtime type of the output array, and may, - * under certain circumstances, be used to save allocation costs. - * - *

Suppose {@code x} is a deque known to contain only strings. - * The following code can be used to dump the deque into a newly - * allocated array of {@code String}: - * - *

-     *     String[] y = x.toArray(new String[0]);
- * - * Note that {@code toArray(new Object[0])} is identical in function to - * {@code toArray()}. - * - * @param a the array into which the elements of the deque are to - * be stored, if it is big enough; otherwise, a new array of the - * same runtime type is allocated for this purpose - * @return an array containing all of the elements in this deque - * @throws ArrayStoreException if the runtime type of the specified array - * is not a supertype of the runtime type of every element in - * this deque - * @throws NullPointerException if the specified array is null - */ - @Override - @SuppressWarnings("unchecked") - public T[] toArray(T[] a) { - final ReentrantLock lock = this.lock; - lock.lock(); - try { - if (a.length < count) - a = (T[])java.lang.reflect.Array.newInstance - (a.getClass().getComponentType(), count); - - int k = 0; - for (Node p = first; p != null; p = p.next) - a[k++] = (T)p.item; - if (a.length > k) - a[k] = null; - return a; - } finally { - lock.unlock(); - } - } - - @Override - public String toString() { - final ReentrantLock lock = this.lock; - lock.lock(); - try { - Node p = first; - if (p == null) - return "[]"; - - StringBuilder sb = new StringBuilder(); - sb.append('['); - for (;;) { - E e = p.item; - sb.append(e == this ? "(this Collection)" : e); - p = p.next; - if (p == null) - return sb.append(']').toString(); - sb.append(',').append(' '); - } - } finally { - lock.unlock(); - } - } - - /** - * Atomically removes all of the elements from this deque. - * The deque will be empty after this call returns. - */ - @Override - public void clear() { - final ReentrantLock lock = this.lock; - lock.lock(); - try { - for (Node f = first; f != null; ) { - f.item = null; - Node n = f.next; - f.prev = null; - f.next = null; - f = n; - } - first = last = null; - count = 0; - notFull.signalAll(); - } finally { - lock.unlock(); - } - } - - /** - * 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). - * - *

The returned iterator is a "weakly consistent" iterator that - * will never throw {@link java.util.ConcurrentModificationException - * ConcurrentModificationException}, and guarantees to traverse - * elements as they existed upon construction of the iterator, and - * may (but is not guaranteed to) reflect any modifications - * subsequent to construction. - * - * @return an iterator over the elements in this deque in proper sequence - */ - @Override - public Iterator iterator() { - return new Itr(); - } - - /** - * 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). - * - *

The returned iterator is a "weakly consistent" iterator that - * will never throw {@link java.util.ConcurrentModificationException - * ConcurrentModificationException}, and guarantees to traverse - * elements as they existed upon construction of the iterator, and - * may (but is not guaranteed to) reflect any modifications - * subsequent to construction. - * - * @return an iterator over the elements in this deque in reverse order - */ - public Iterator descendingIterator() { - return new DescendingItr(); - } - - /** - * Base class for Iterators for LinkedBlockingDeque - */ - private abstract class AbstractItr implements Iterator { - /** - * The next node to return in next() - */ - Node next; - - /** - * nextItem holds on to item fields because once we claim that - * an element exists in hasNext(), we must return item read - * under lock (in advance()) even if it was in the process of - * being removed when hasNext() was called. - */ - E nextItem; - - /** - * Node returned by most recent call to next. Needed by remove. - * Reset to null if this element is deleted by a call to remove. - */ - private Node lastRet; - - abstract Node firstNode(); - abstract Node nextNode(Node n); - - AbstractItr() { - // set to initial position - final ReentrantLock lock = LinkedBlockingDeque.this.lock; - lock.lock(); - try { - next = firstNode(); - nextItem = (next == null) ? null : next.item; - } finally { - lock.unlock(); - } - } - - /** - * Returns the successor node of the given non-null, but - * possibly previously deleted, node. - */ - private Node succ(Node n) { - // Chains of deleted nodes ending in null or self-links - // are possible if multiple interior nodes are removed. - for (;;) { - Node s = nextNode(n); - if (s == null) - return null; - else if (s.item != null) - return s; - else if (s == n) - return firstNode(); - else - n = s; - } - } - - /** - * Advances next. - */ - void advance() { - final ReentrantLock lock = LinkedBlockingDeque.this.lock; - lock.lock(); - try { - // assert next != null; - next = succ(next); - nextItem = (next == null) ? null : next.item; - } finally { - lock.unlock(); - } - } - - public boolean hasNext() { - return next != null; - } - - public E next() { - if (next == null) - throw new NoSuchElementException(); - lastRet = next; - E x = nextItem; - advance(); - return x; - } - - public void remove() { - Node n = lastRet; - if (n == null) - throw new IllegalStateException(); - lastRet = null; - final ReentrantLock lock = LinkedBlockingDeque.this.lock; - lock.lock(); - try { - if (n.item != null) - unlink(n); - } finally { - lock.unlock(); - } - } - } - - /** Forward iterator */ - private class Itr extends AbstractItr { - @Override - Node firstNode() { return first; } - @Override - Node nextNode(Node n) { return n.next; } - } - - /** Descending iterator */ - private class DescendingItr extends AbstractItr { - @Override - Node firstNode() { return last; } - @Override - Node nextNode(Node n) { return n.prev; } - } - - /** - * Save the state of this deque to a stream (that is, serialize it). - * - * @serialData The capacity (int), followed by elements (each an - * {@code Object}) in the proper order, followed by a null - * @param s the stream - */ - private void writeObject(java.io.ObjectOutputStream s) - throws java.io.IOException { - final ReentrantLock lock = this.lock; - lock.lock(); - try { - // Write out capacity and any hidden stuff - s.defaultWriteObject(); - // Write out all elements in the proper order. - for (Node p = first; p != null; p = p.next) - s.writeObject(p.item); - // Use trailing null as sentinel - s.writeObject(null); - } finally { - lock.unlock(); - } - } - - /** - * Reconstitute this deque from a stream (that is, - * deserialize it). - * @param s the stream - */ - private void readObject(java.io.ObjectInputStream s) - throws java.io.IOException, ClassNotFoundException { - s.defaultReadObject(); - count = 0; - first = null; - last = null; - // Read in all elements and place in queue - for (;;) { - @SuppressWarnings("unchecked") - E item = (E)s.readObject(); - if (item == null) - break; - add(item); - } - } - -}