From 7a5846a6d441dc5329655acc670773f74e0a361e Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Sun, 26 Nov 2017 10:45:39 -0800 Subject: [PATCH] Move more system contact info into recipient database 1) Move contact URI, contact photo URI, and custom label into recipient database, so there are no longer any contact DB queries during Recipient object loading. 2) Use a SoftHashMap so that any referenced Recipient objects can't get kicked out of the cache. 3) Don't load Recipient objects through the provider during sync. This was a super expensive thing to do, and blew up the cache. 4) Only apply changes to Recipient objects during sync if they are in the cache. Otherwise, there should be no outstanding references, and the changes are fine going exclusively to the DB. --- .../securesms/contacts/ContactAccessor.java | 2 +- .../securesms/database/DatabaseFactory.java | 43 +- .../securesms/database/GroupDatabase.java | 24 +- .../securesms/database/RecipientDatabase.java | 136 +- .../jobs/RetrieveProfileAvatarJob.java | 5 - .../securesms/recipients/Recipient.java | 98 +- .../recipients/RecipientProvider.java | 160 +-- .../securesms/util/DirectoryHelper.java | 32 +- .../util/LinkedBlockingLifoQueue.java | 3 +- .../securesms/util/SoftHashMap.java | 319 +++++ src/org/thoughtcrime/securesms/util/Util.java | 6 + .../securesms/util/deque/BlockingDeque.java | 616 --------- .../securesms/util/deque/Deque.java | 555 -------- .../util/deque/LinkedBlockingDeque.java | 1190 ----------------- 14 files changed, 618 insertions(+), 2571 deletions(-) create mode 100644 src/org/thoughtcrime/securesms/util/SoftHashMap.java delete mode 100644 src/org/thoughtcrime/securesms/util/deque/BlockingDeque.java delete mode 100644 src/org/thoughtcrime/securesms/util/deque/Deque.java delete mode 100644 src/org/thoughtcrime/securesms/util/deque/LinkedBlockingDeque.java diff --git a/src/org/thoughtcrime/securesms/contacts/ContactAccessor.java b/src/org/thoughtcrime/securesms/contacts/ContactAccessor.java index f5ac5e0d1..1551cd3e0 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 01512adac..dbb882623 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 99576a1fc..3ef8ca55c 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 c349c35e4..a8f8cb5ef 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 f4f12e706..dbc25d2b9 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 5e9b673e3..aa97f1201 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 9f3cee64d..e34ecd8d1 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 9f393c80e..b66a6964b 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 c7cd36b85..1b07afee6 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 000000000..64553f596 --- /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 9656aa2ef..c20b707f3 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 a486fbd7c..000000000 --- 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 c260941c9..000000000 --- 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 de006ea58..000000000 --- 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); - } - } - -}