diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index e4693b99ed..0e5a5d0654 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -158,6 +158,9 @@
+
+
+
diff --git a/res/layout/activity_create_closed_group.xml b/res/layout/activity_create_closed_group.xml
new file mode 100644
index 0000000000..fa6585f797
--- /dev/null
+++ b/res/layout/activity_create_closed_group.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/activity_home.xml b/res/layout/activity_home.xml
index d03b9cb65d..28e927d94d 100644
--- a/res/layout/activity_home.xml
+++ b/res/layout/activity_home.xml
@@ -40,13 +40,27 @@
android:layout_centerVertical="true"
android:layout_marginLeft="64dp" />
-
+ android:layout_centerVertical="true">
+
+
+
+
+
+
diff --git a/res/layout/view_user.xml b/res/layout/view_user.xml
new file mode 100644
index 0000000000..6c63b7759c
--- /dev/null
+++ b/res/layout/view_user.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/menu/menu_create_closed_group.xml b/res/menu/menu_create_closed_group.xml
new file mode 100644
index 0000000000..75b41dcd68
--- /dev/null
+++ b/res/menu/menu_create_closed_group.xml
@@ -0,0 +1,10 @@
+
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 71c2c3e4ab..d5d65e2d57 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1073,7 +1073,12 @@
- %1$s joined the group.
- %1$s joined the group.
+
+ - %1$s was removed from the group.
+ - %1$s were removed from the group.
+
Group name is now \'%1$s\'.
+ You were removed from the group.
Make your profile name and photo visible to this group?
diff --git a/res/xml/network_security_configuration.xml b/res/xml/network_security_configuration.xml
index cef100a280..79ba86792a 100644
--- a/res/xml/network_security_configuration.xml
+++ b/res/xml/network_security_configuration.xml
@@ -4,7 +4,7 @@
imaginary.stream
storage.seed1.loki.network
storage.seed2.loki.network
- http://public.loki.foundation
+ public.loki.foundation:22023
127.0.0.1
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/ApplicationContext.java b/src/org/thoughtcrime/securesms/ApplicationContext.java
index 203c84df9f..eed7ba2f38 100644
--- a/src/org/thoughtcrime/securesms/ApplicationContext.java
+++ b/src/org/thoughtcrime/securesms/ApplicationContext.java
@@ -521,18 +521,18 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
public void createDefaultPublicChatsIfNeeded() {
List defaultPublicChats = LokiPublicChatAPI.Companion.getDefaultChats(BuildConfig.DEBUG);
- for (LokiPublicChat publiChat : defaultPublicChats) {
- long threadID = GroupManager.getThreadId(publiChat.getId(), this);
- String migrationKey = publiChat.getId() + "_migrated";
+ for (LokiPublicChat publicChat : defaultPublicChats) {
+ long threadID = GroupManager.getPublicChatThreadId(publicChat.getId(), this);
+ String migrationKey = publicChat.getId() + "_migrated";
boolean isChatMigrated = TextSecurePreferences.getBooleanPreference(this, migrationKey, false);
- boolean isChatSetUp = TextSecurePreferences.isChatSetUp(this, publiChat.getId());
- if (!isChatSetUp || !publiChat.isDeletable()) {
- lokiPublicChatManager.addChat(publiChat.getServer(), publiChat.getChannel(), publiChat.getDisplayName());
- TextSecurePreferences.markChatSetUp(this, publiChat.getId());
+ boolean isChatSetUp = TextSecurePreferences.isChatSetUp(this, publicChat.getId());
+ if (!isChatSetUp || !publicChat.isDeletable()) {
+ lokiPublicChatManager.addChat(publicChat.getServer(), publicChat.getChannel(), publicChat.getDisplayName());
+ TextSecurePreferences.markChatSetUp(this, publicChat.getId());
TextSecurePreferences.setBooleanPreference(this, migrationKey, true);
} else if (threadID > -1 && !isChatMigrated) {
// Migrate the old public chats
- DatabaseFactory.getLokiThreadDatabase(this).setPublicChat(publiChat, threadID);
+ DatabaseFactory.getLokiThreadDatabase(this).setPublicChat(publicChat, threadID);
TextSecurePreferences.setBooleanPreference(this, migrationKey, true);
}
}
@@ -545,7 +545,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
for (LokiRSSFeed feed : feeds) {
boolean isFeedSetUp = TextSecurePreferences.isChatSetUp(this, feed.getId());
if (!isFeedSetUp || !feed.isDeletable()) {
- GroupManager.createGroup(feed.getId(), this, new HashSet<>(), null, feed.getDisplayName(), false);
+ GroupManager.createRSSFeedGroup(feed.getId(), this, null, feed.getDisplayName());
TextSecurePreferences.markChatSetUp(this, feed.getId());
}
}
@@ -554,7 +554,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
private void createRSSFeedPollersIfNeeded() {
// Only create the RSS feed pollers if their threads aren't deleted
LokiRSSFeed lokiNewsFeed = lokiNewsFeed();
- long lokiNewsFeedThreadID = GroupManager.getThreadId(lokiNewsFeed.getId(), this);
+ long lokiNewsFeedThreadID = GroupManager.getRSSFeedThreadId(lokiNewsFeed.getId(), this);
if (lokiNewsFeedThreadID >= 0 && lokiNewsFeedPoller == null) {
lokiNewsFeedPoller = new LokiRSSFeedPoller(this, lokiNewsFeed);
// Set up deletion listeners if needed
diff --git a/src/org/thoughtcrime/securesms/GroupCreateActivity.java b/src/org/thoughtcrime/securesms/GroupCreateActivity.java
index 9408b45bd2..d87946c97d 100644
--- a/src/org/thoughtcrime/securesms/GroupCreateActivity.java
+++ b/src/org/thoughtcrime/securesms/GroupCreateActivity.java
@@ -72,6 +72,7 @@ import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.io.File;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
@@ -246,7 +247,8 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
return;
}
if (isSignalGroup()) {
- new CreateSignalGroupTask(this, avatarBmp, getGroupName(), getAdapter().getRecipients()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ Recipient local = Recipient.from(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)), false);
+ new CreateSignalGroupTask(this, avatarBmp, getGroupName(), getAdapter().getRecipients(), Collections.singleton(local)).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} else {
new CreateMmsGroupTask(this, getAdapter().getRecipients()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
@@ -254,7 +256,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
private void handleGroupUpdate() {
new UpdateSignalGroupTask(this, groupToUpdate.get().id, avatarBmp,
- getGroupName(), getAdapter().getRecipients()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ getGroupName(), getAdapter().getRecipients(), groupToUpdate.get().admins).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void handleOpenConversation(long threadId, Recipient recipient) {
@@ -344,9 +346,10 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
for (Recipient recipient : members) {
memberAddresses.add(recipient.getAddress());
}
- memberAddresses.add(Address.fromSerialized(TextSecurePreferences.getLocalNumber(activity)));
+ Address local = Address.fromSerialized(TextSecurePreferences.getLocalNumber(activity));
+ memberAddresses.add(local);
- String groupId = DatabaseFactory.getGroupDatabase(activity).getOrCreateGroupForMembers(memberAddresses, true);
+ String groupId = DatabaseFactory.getGroupDatabase(activity).getOrCreateGroupForMembers(memberAddresses, true, Collections.singletonList(local));
Recipient groupRecipient = Recipient.from(activity, Address.fromSerialized(groupId), true);
long threadId = DatabaseFactory.getThreadDatabase(activity).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.DEFAULT);
@@ -370,16 +373,19 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
protected Bitmap avatar;
protected Set members;
protected String name;
+ protected Set admins;
public SignalGroupTask(GroupCreateActivity activity,
Bitmap avatar,
String name,
- Set members)
+ Set members,
+ Set admins)
{
this.activity = activity;
this.avatar = avatar;
this.name = name;
this.members = members;
+ this.admins = admins;
}
@Override
@@ -403,13 +409,13 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
}
private static class CreateSignalGroupTask extends SignalGroupTask {
- public CreateSignalGroupTask(GroupCreateActivity activity, Bitmap avatar, String name, Set members) {
- super(activity, avatar, name, members);
+ public CreateSignalGroupTask(GroupCreateActivity activity, Bitmap avatar, String name, Set members, Set admins) {
+ super(activity, avatar, name, members, admins);
}
@Override
protected Optional doInBackground(Void... aVoid) {
- return Optional.of(GroupManager.createGroup(activity, members, avatar, name, false));
+ return Optional.of(GroupManager.createGroup(activity, members, avatar, name, false, admins));
}
@Override
@@ -430,16 +436,16 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
private String groupId;
public UpdateSignalGroupTask(GroupCreateActivity activity, String groupId,
- Bitmap avatar, String name, Set members)
+ Bitmap avatar, String name, Set members, Set admins)
{
- super(activity, avatar, name, members);
+ super(activity, avatar, name, members, admins);
this.groupId = groupId;
}
@Override
protected Optional doInBackground(Void... aVoid) {
try {
- return Optional.of(GroupManager.updateGroup(activity, groupId, members, avatar, name));
+ return Optional.of(GroupManager.updateGroup(activity, groupId, members, avatar, name, admins));
} catch (InvalidNumberException e) {
return Optional.absent();
}
@@ -491,7 +497,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
for (Recipient recipient : recipients) {
boolean isPush = isActiveInDirectory(recipient);
- if (failIfNotPush && !isPush) {
+ if (failIfNotPush && !isPush && !recipient.getAddress().isPhone()) {
results.add(new Result(null, false, activity.getString(R.string.GroupCreateActivity_cannot_add_non_push_to_existing_group,
recipient.toShortString())));
} else if (TextUtils.equals(TextSecurePreferences.getLocalNumber(activity), recipient.getAddress().serialize())) {
@@ -537,11 +543,17 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
existingContacts.addAll(recipients);
if (group.isPresent()) {
+ List adminList = group.get().getAdmins();
+ final Set admins = new HashSet<>(adminList.size());
+ for (Address admin : adminList) {
+ admins.add(Recipient.from(getContext(), admin, false));
+ }
return Optional.of(new GroupData(groupIds[0],
existingContacts,
BitmapUtil.fromByteArray(group.get().getAvatar()),
group.get().getAvatar(),
- group.get().getTitle()));
+ group.get().getTitle(),
+ admins));
} else {
return Optional.absent();
}
@@ -582,13 +594,15 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
Bitmap avatarBmp;
byte[] avatarBytes;
String name;
+ Set admins;
- public GroupData(String id, Set recipients, Bitmap avatarBmp, byte[] avatarBytes, String name) {
+ public GroupData(String id, Set recipients, Bitmap avatarBmp, byte[] avatarBytes, String name, Set admins) {
this.id = id;
this.recipients = recipients;
this.avatarBmp = avatarBmp;
this.avatarBytes = avatarBytes;
this.name = name;
+ this.admins = admins;
}
}
}
diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
index 4bad0eea90..34ee6efe26 100644
--- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
+++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
@@ -740,9 +740,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
MenuInflater inflater = this.getMenuInflater();
menu.clear();
- boolean isLokiPublicChat = isGroupConversation(); // TODO: Figure out a better way of determining this
+ boolean isLokiGroupChat = recipient.getAddress().isPublicChat() || recipient.getAddress().isRSSFeed();
- if (isSecureText && !isLokiPublicChat) { // TODO:
+ if (isSecureText && !isLokiGroupChat) {
if (recipient.getExpireMessages() > 0) {
inflater.inflate(R.menu.conversation_expiring_on, menu);
@@ -762,7 +762,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (isSecureText) inflater.inflate(R.menu.conversation_callable_secure, menu);
else inflater.inflate(R.menu.conversation_callable_insecure, menu);
*/
- } else if (isGroupConversation() && !isLokiPublicChat) {
+ } else if (isGroupConversation() && !isLokiGroupChat) {
inflater.inflate(R.menu.conversation_group_options, menu);
if (!isPushGroupConversation()) {
@@ -1152,10 +1152,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (threadId != -1 && leaveMessage.isPresent()) {
MessageSender.send(this, leaveMessage.get(), threadId, false, null);
+ // We need to remove the master device from the group
+ String masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(this);
+ String localNumber = masterHexEncodedPublicKey != null ? masterHexEncodedPublicKey : TextSecurePreferences.getLocalNumber(this);
+
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(this);
String groupId = groupRecipient.getAddress().toGroupString();
groupDatabase.setActive(groupId, false);
- groupDatabase.remove(groupId, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)));
+ groupDatabase.remove(groupId, Address.fromSerialized(localNumber));
initializeEnabledCheck();
} else {
@@ -2077,7 +2081,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
private void setBlockedUserState(Recipient recipient, boolean isSecureText, boolean isDefaultSms) {
- if (recipient.isGroupRecipient() && (recipient.getName().equals("Loki News") || recipient.getName().equals("Session Updates"))) {
+ if (recipient.isGroupRecipient() && recipient.getAddress().isRSSFeed()) {
unblockButton.setVisibility(View.GONE);
composePanel.setVisibility(View.GONE);
makeDefaultSmsButton.setVisibility(View.GONE);
@@ -2106,7 +2110,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
private void setGroupShareProfileReminder(@NonNull Recipient recipient) {
- if (recipient.isPushGroupRecipient() && !recipient.isProfileSharing()) {
+ if (recipient.isPushGroupRecipient() && !recipient.isProfileSharing() && !recipient.getAddress().isPublicChat() && !recipient.getAddress().isRSSFeed()) {
groupShareProfileView.get().setRecipient(recipient);
groupShareProfileView.get().setVisibility(View.VISIBLE);
} else if (groupShareProfileView.resolved()) {
@@ -2461,7 +2465,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
// Loki - Send a friend request if we're not yet friends with the user in question
LokiThreadFriendRequestStatus friendRequestStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadId);
- outgoingMessage.isFriendRequest = (friendRequestStatus != LokiThreadFriendRequestStatus.FRIENDS); // Needed for stageOutgoingMessage(...)
+ outgoingMessage.isFriendRequest = !isGroupConversation() && friendRequestStatus != LokiThreadFriendRequestStatus.FRIENDS; // Needed for stageOutgoingMessage(...)
Permissions.with(this)
.request(Manifest.permission.SEND_SMS, Manifest.permission.READ_SMS)
@@ -2521,7 +2525,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
// Loki - Send a friend request if we're not yet friends with the user in question
LokiThreadFriendRequestStatus friendRequestStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadId);
- message.isFriendRequest = (friendRequestStatus != LokiThreadFriendRequestStatus.FRIENDS); // Needed for stageOutgoingMessage(...)
+ message.isFriendRequest = !isGroupConversation() && friendRequestStatus != LokiThreadFriendRequestStatus.FRIENDS; // Needed for stageOutgoingMessage(...)
Permissions.with(this)
.request(Manifest.permission.SEND_SMS)
diff --git a/src/org/thoughtcrime/securesms/database/Address.java b/src/org/thoughtcrime/securesms/database/Address.java
index d063025cfa..1286b4b302 100644
--- a/src/org/thoughtcrime/securesms/database/Address.java
+++ b/src/org/thoughtcrime/securesms/database/Address.java
@@ -52,17 +52,9 @@ public class Address implements Parcelable, Comparable {
private final String address;
- // Loki - Special flag to indicate whether this address represents a public chat or not
- private Boolean isPublicChat;
-
private Address(@NonNull String address) {
- this(address, false);
- }
-
- private Address(@NonNull String address, Boolean isPublicChat) {
if (address == null) throw new AssertionError(address);
this.address = address.toLowerCase();
- this.isPublicChat = isPublicChat;
}
public Address(Parcel in) {
@@ -77,10 +69,6 @@ public class Address implements Parcelable, Comparable {
return Address.fromSerialized(external);
}
- public static @NonNull Address fromPublicChatGroupID(@NonNull String serialized) {
- return new Address(serialized, true);
- }
-
public static @NonNull List fromSerializedList(@NonNull String serialized, char delimiter) {
String[] escapedAddresses = DelimiterUtil.split(serialized, delimiter);
List addresses = new LinkedList<>();
@@ -121,13 +109,15 @@ public class Address implements Parcelable, Comparable {
}
}
- public boolean isGroup() {
- return GroupUtil.isEncodedGroup(address);
- }
+ public boolean isGroup() { return GroupUtil.isEncodedGroup(address); }
- public boolean isMmsGroup() {
- return GroupUtil.isMmsGroup(address);
- }
+ public boolean isSignalGroup() { return !isPublicChat() && !isRSSFeed(); }
+
+ public boolean isPublicChat() { return GroupUtil.isPublicChat(address); }
+
+ public boolean isRSSFeed() { return GroupUtil.isRssFeed(address); }
+
+ public boolean isMmsGroup() { return GroupUtil.isMmsGroup(address); }
public boolean isEmail() {
return NumberUtil.isValidEmail(address);
@@ -143,7 +133,7 @@ public class Address implements Parcelable, Comparable {
}
public @NonNull String toPhoneString() {
- if (!isPhone() && !isPublicChat) {
+ if (!isPhone() && !isPublicChat()) {
if (isEmail()) throw new AssertionError("Not e164, is email");
if (isGroup()) throw new AssertionError("Not e164, is group");
throw new AssertionError("Not e164, unknown");
diff --git a/src/org/thoughtcrime/securesms/database/GroupDatabase.java b/src/org/thoughtcrime/securesms/database/GroupDatabase.java
index 747ed36e41..502a566f02 100644
--- a/src/org/thoughtcrime/securesms/database/GroupDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/GroupDatabase.java
@@ -24,7 +24,6 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPoin
import java.io.Closeable;
import java.io.IOException;
-import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Collections;
import java.util.LinkedList;
@@ -52,6 +51,7 @@ public class GroupDatabase extends Database {
// Loki
private static final String AVATAR_URL = "avatar_url";
+ private static final String ADMINS = "admins";
public static final String CREATE_TABLE =
"CREATE TABLE " + TABLE_NAME +
@@ -68,6 +68,7 @@ public class GroupDatabase extends Database {
ACTIVE + " INTEGER DEFAULT 1, " +
AVATAR_DIGEST + " BLOB, " +
AVATAR_URL + " TEXT, " +
+ ADMINS + " TEXT, " +
MMS + " INTEGER DEFAULT 0);";
public static final String[] CREATE_INDEXS = {
@@ -76,7 +77,7 @@ public class GroupDatabase extends Database {
private static final String[] GROUP_PROJECTION = {
GROUP_ID, TITLE, MEMBERS, AVATAR, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST,
- TIMESTAMP, ACTIVE, MMS, AVATAR_URL
+ TIMESTAMP, ACTIVE, MMS, AVATAR_URL, ADMINS
};
static final List TYPED_GROUP_PROJECTION = Stream.of(GROUP_PROJECTION).map(columnName -> TABLE_NAME + "." + columnName).toList();
@@ -116,8 +117,9 @@ public class GroupDatabase extends Database {
return new Reader(cursor);
}
- public String getOrCreateGroupForMembers(List members, boolean mms) {
+ public String getOrCreateGroupForMembers(List members, boolean mms, List admins) {
Collections.sort(members);
+ Collections.sort(admins);
Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[] {GROUP_ID},
MEMBERS + " = ? AND " + MMS + " = ?",
@@ -128,7 +130,7 @@ public class GroupDatabase extends Database {
return cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID));
} else {
String groupId = GroupUtil.getEncodedId(allocateGroupId(), mms);
- create(groupId, null, members, null, null);
+ create(groupId, null, members, null, null, admins);
return groupId;
}
} finally {
@@ -150,14 +152,33 @@ public class GroupDatabase extends Database {
if (!includeSelf && Util.isOwnNumber(context, member))
continue;
- recipients.add(Recipient.from(context, member, false));
+ if (member.isPhone()) {
+ recipients.add(Recipient.from(context, member, false));
+ }
}
return recipients;
}
+ public boolean signalGroupsHaveMember(String hexEncodedPublicKey) {
+ try {
+ Address address = Address.fromSerialized(hexEncodedPublicKey);
+ Reader reader = DatabaseFactory.getGroupDatabase(context).getGroups();
+ GroupRecord record;
+ while ((record = reader.getNext()) != null) {
+ if (record.isSignalGroup() && record.members.contains(address)) {
+ return true;
+ }
+ }
+
+ return false;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
public void create(@NonNull String groupId, @Nullable String title, @NonNull List members,
- @Nullable SignalServiceAttachmentPointer avatar, @Nullable String relay)
+ @Nullable SignalServiceAttachmentPointer avatar, @Nullable String relay, @Nullable List admins)
{
Collections.sort(members);
@@ -179,6 +200,10 @@ public class GroupDatabase extends Database {
contentValues.put(ACTIVE, 1);
contentValues.put(MMS, GroupUtil.isMmsGroup(groupId));
+ if (admins != null) {
+ contentValues.put(ADMINS, Address.toSerializedList(admins, ','));
+ }
+
databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues);
Recipient.applyCached(Address.fromSerialized(groupId), recipient -> {
@@ -260,6 +285,17 @@ public class GroupDatabase extends Database {
});
}
+ public void updateAdmins(String groupId, List admins) {
+ Collections.sort(admins);
+
+ ContentValues contents = new ContentValues();
+ contents.put(ADMINS, Address.toSerializedList(admins, ','));
+ contents.put(ACTIVE, 1);
+
+ databaseHelper.getWritableDatabase().update(TABLE_NAME, contents, GROUP_ID + " = ?",
+ new String[] {groupId});
+ }
+
public void remove(String groupId, Address source) {
List currentMembers = getCurrentMembers(groupId);
currentMembers.remove(source);
@@ -351,7 +387,8 @@ public class GroupDatabase extends Database {
cursor.getInt(cursor.getColumnIndexOrThrow(ACTIVE)) == 1,
cursor.getBlob(cursor.getColumnIndexOrThrow(AVATAR_DIGEST)),
cursor.getInt(cursor.getColumnIndexOrThrow(MMS)) == 1,
- cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_URL)));
+ cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_URL)),
+ cursor.getString(cursor.getColumnIndexOrThrow(ADMINS)));
}
@Override
@@ -375,10 +412,11 @@ public class GroupDatabase extends Database {
private final boolean active;
private final boolean mms;
private final String url;
+ private final List admins;
public GroupRecord(String id, String title, String members, byte[] avatar,
long avatarId, byte[] avatarKey, String avatarContentType,
- String relay, boolean active, byte[] avatarDigest, boolean mms, String url)
+ String relay, boolean active, byte[] avatarDigest, boolean mms, String url, String admins)
{
this.id = id;
this.title = title;
@@ -394,6 +432,9 @@ public class GroupDatabase extends Database {
if (!TextUtils.isEmpty(members)) this.members = Address.fromSerializedList(members, ',');
else this.members = new LinkedList<>();
+
+ if (!TextUtils.isEmpty(admins)) this.admins = Address.fromSerializedList(admins, ',');
+ else this.admins = new LinkedList<>();
}
public byte[] getId() {
@@ -448,6 +489,14 @@ public class GroupDatabase extends Database {
return mms;
}
+ public boolean isPublicChat() { return Address.fromSerialized(id).isPublicChat(); }
+
+ public boolean isRSSFeed() { return Address.fromSerialized(id).isRSSFeed(); }
+
+ public boolean isSignalGroup() { return Address.fromSerialized(id).isSignalGroup(); }
+
public String getUrl() { return url; }
+
+ public List getAdmins() { return admins; }
}
}
diff --git a/src/org/thoughtcrime/securesms/database/SmsMigrator.java b/src/org/thoughtcrime/securesms/database/SmsMigrator.java
index 071714e1f1..f73acd496f 100644
--- a/src/org/thoughtcrime/securesms/database/SmsMigrator.java
+++ b/src/org/thoughtcrime/securesms/database/SmsMigrator.java
@@ -220,7 +220,7 @@ public class SmsMigrator {
memberAddresses.add(recipient.getAddress());
}
- String ourGroupId = DatabaseFactory.getGroupDatabase(context).getOrCreateGroupForMembers(memberAddresses, true);
+ String ourGroupId = DatabaseFactory.getGroupDatabase(context).getOrCreateGroupForMembers(memberAddresses, true, null);
Recipient ourGroupRecipient = Recipient.from(context, Address.fromSerialized(ourGroupId), true);
long ourThreadId = threadDatabase.getThreadIdFor(ourGroupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION);
diff --git a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
index cb489b0971..cfa396d474 100644
--- a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
+++ b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
@@ -35,14 +35,17 @@ import org.thoughtcrime.securesms.database.StickerDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
import org.thoughtcrime.securesms.logging.Log;
-import org.thoughtcrime.securesms.loki.*;
+import org.thoughtcrime.securesms.loki.LokiMessageDatabase;
+import org.thoughtcrime.securesms.loki.LokiThreadDatabase;
import org.thoughtcrime.securesms.loki.redesign.messaging.LokiAPIDatabase;
import org.thoughtcrime.securesms.loki.redesign.messaging.LokiPreKeyBundleDatabase;
import org.thoughtcrime.securesms.loki.redesign.messaging.LokiPreKeyRecordDatabase;
import org.thoughtcrime.securesms.loki.redesign.messaging.LokiUserDatabase;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.service.KeyCachingService;
+import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
+import org.whispersystems.signalservice.loki.api.LokiPublicChat;
import java.io.File;
@@ -76,8 +79,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int lokiV3 = 24;
private static final int lokiV4 = 25;
private static final int lokiV5 = 26;
+ private static final int lokiV6 = 27;
- private static final int DATABASE_VERSION = lokiV5; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
+ private static final int DATABASE_VERSION = lokiV6; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
private static final String DATABASE_NAME = "signal.db";
private final Context context;
@@ -529,6 +533,43 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(LokiAPIDatabase.getCreateUserCountTableCommand());
}
+ if (oldVersion < lokiV6) {
+ // Migrate public chats from __textsecure_group__ to __loki_public_chat_group__
+ try (Cursor lokiPublicChatCursor = db.rawQuery("SELECT public_chat FROM loki_public_chat_database", null)) {
+ while (lokiPublicChatCursor != null && lokiPublicChatCursor.moveToNext()) {
+ String chatString = lokiPublicChatCursor.getString(0);
+ LokiPublicChat publicChat = LokiPublicChat.fromJSON(chatString);
+ if (publicChat != null) {
+ byte[] groupId = publicChat.getId().getBytes();
+ String oldId = GroupUtil.getEncodedId(groupId, false);
+ String newId = GroupUtil.getEncodedPublicChatId(groupId);
+ ContentValues threadUpdate = new ContentValues();
+ threadUpdate.put("recipient_ids", newId);
+ db.update("thread", threadUpdate, "recipient_ids = ?", new String[]{ oldId });
+ ContentValues groupUpdate = new ContentValues();
+ groupUpdate.put("group_id", newId);
+ db.update("groups", groupUpdate,"group_id = ?", new String[] { oldId });
+ }
+ }
+ }
+
+ // Migrate rss feeds from __textsecure_group__ to __loki_rss_feed_group__
+ String[] rssFeedIds = new String[] { "loki.network.feed", "loki.network.messenger-updates.feed" };
+ for (String groupId : rssFeedIds) {
+ String oldId = GroupUtil.getEncodedId(groupId.getBytes(), false);
+ String newId = GroupUtil.getEncodedRSSFeedId(groupId.getBytes());
+ ContentValues threadUpdate = new ContentValues();
+ threadUpdate.put("recipient_ids", newId);
+ db.update("thread", threadUpdate, "recipient_ids = ?", new String[]{ oldId });
+ ContentValues groupUpdate = new ContentValues();
+ groupUpdate.put("group_id", newId);
+ db.update("groups", groupUpdate,"group_id = ?", new String[] { oldId });
+ }
+
+ // Add admin field in groups
+ db.execSQL("ALTER TABLE groups ADD COLUMN admins TEXT");
+ }
+
db.setTransactionSuccessful();
} finally {
db.endTransaction();
diff --git a/src/org/thoughtcrime/securesms/groups/GroupManager.java b/src/org/thoughtcrime/securesms/groups/GroupManager.java
index e17b964d93..bcd4521bb8 100644
--- a/src/org/thoughtcrime/securesms/groups/GroupManager.java
+++ b/src/org/thoughtcrime/securesms/groups/GroupManager.java
@@ -36,8 +36,13 @@ import java.util.Set;
public class GroupManager {
- public static long getThreadId(String id, @NonNull Context context) {
- final String groupId = GroupUtil.getEncodedId(id.getBytes(), false);
+ public static long getPublicChatThreadId(String id, @NonNull Context context) {
+ final String groupId = GroupUtil.getEncodedPublicChatId(id.getBytes());
+ return getThreadIdFromGroupId(groupId, context);
+ }
+
+ public static long getRSSFeedThreadId(String id, @NonNull Context context) {
+ final String groupId = GroupUtil.getEncodedRSSFeedId(id.getBytes());
return getThreadIdFromGroupId(groupId, context);
}
@@ -50,11 +55,12 @@ public class GroupManager {
@NonNull Set members,
@Nullable Bitmap avatar,
@Nullable String name,
- boolean mms)
+ boolean mms,
+ @NonNull Set admins)
{
GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
String id = GroupUtil.getEncodedId(database.allocateGroupId(), mms);
- return createGroup(id, context, members, avatar, name, mms);
+ return createGroup(id, context, members, avatar, name, mms, admins);
}
public static @NonNull GroupActionResult createGroup(@NonNull String id,
@@ -62,56 +68,90 @@ public class GroupManager {
@NonNull Set members,
@Nullable Bitmap avatar,
@Nullable String name,
- boolean mms)
+ boolean mms,
+ @NonNull Set admins)
{
final byte[] avatarBytes = BitmapUtil.toByteArray(avatar);
final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
final String groupId = GroupUtil.getEncodedId(id.getBytes(), mms);
final Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(groupId), false);
final Set memberAddresses = getMemberAddresses(members);
+ final Set adminAddresses = getMemberAddresses(admins);
- memberAddresses.add(Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)));
- groupDatabase.create(groupId, name, new LinkedList<>(memberAddresses), null, null);
+ String masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
+ String ourNumber = masterHexEncodedPublicKey != null ? masterHexEncodedPublicKey : TextSecurePreferences.getLocalNumber(context);
+
+ memberAddresses.add(Address.fromSerialized(ourNumber));
+ groupDatabase.create(groupId, name, new LinkedList<>(memberAddresses), null, null, new LinkedList<>(adminAddresses));
if (!mms) {
groupDatabase.updateAvatar(groupId, avatarBytes);
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient, true);
- }
-
- long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION);
- return new GroupActionResult(groupRecipient, threadId);
-
- /* Loki: Original Code
- ==================
- if (!mms) {
- groupDatabase.updateAvatar(groupId, avatarBytes);
- DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient, true);
- return sendGroupUpdate(context, groupId, memberAddresses, name, avatarBytes);
+ return sendGroupUpdate(context, groupId, memberAddresses, name, avatarBytes, adminAddresses);
} else {
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION);
return new GroupActionResult(groupRecipient, threadId);
}
- */
+ }
+
+ public static @NonNull GroupActionResult createPublicChatGroup(@NonNull String id,
+ @NonNull Context context,
+ @Nullable Bitmap avatar,
+ @Nullable String name)
+ {
+ final String groupId = GroupUtil.getEncodedPublicChatId(id.getBytes());
+ return createLokiGroup(groupId, context, avatar, name);
+ }
+
+ public static @NonNull GroupActionResult createRSSFeedGroup(@NonNull String id,
+ @NonNull Context context,
+ @Nullable Bitmap avatar,
+ @Nullable String name)
+ {
+ final String groupId = GroupUtil.getEncodedRSSFeedId(id.getBytes());
+ return createLokiGroup(groupId, context, avatar, name);
+ }
+
+ private static @NonNull GroupActionResult createLokiGroup(@NonNull String groupId,
+ @NonNull Context context,
+ @Nullable Bitmap avatar,
+ @Nullable String name)
+ {
+ final byte[] avatarBytes = BitmapUtil.toByteArray(avatar);
+ final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
+ final Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(groupId), false);
+ final Set memberAddresses = new HashSet<>();
+
+ memberAddresses.add(Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)));
+ groupDatabase.create(groupId, name, new LinkedList<>(memberAddresses), null, null, new LinkedList<>());
+
+ groupDatabase.updateAvatar(groupId, avatarBytes);
+
+ long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION);
+ return new GroupActionResult(groupRecipient, threadId);
}
public static GroupActionResult updateGroup(@NonNull Context context,
@NonNull String groupId,
@NonNull Set members,
@Nullable Bitmap avatar,
- @Nullable String name)
+ @Nullable String name,
+ @NonNull Set admins)
throws InvalidNumberException
{
final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
final Set memberAddresses = getMemberAddresses(members);
+ final Set adminAddresses = getMemberAddresses(admins);
final byte[] avatarBytes = BitmapUtil.toByteArray(avatar);
memberAddresses.add(Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)));
groupDatabase.updateMembers(groupId, new LinkedList<>(memberAddresses));
+ groupDatabase.updateAdmins(groupId, new LinkedList<>(adminAddresses));
groupDatabase.updateTitle(groupId, name);
groupDatabase.updateAvatar(groupId, avatarBytes);
if (!GroupUtil.isMmsGroup(groupId)) {
- return sendGroupUpdate(context, groupId, memberAddresses, name, avatarBytes);
+ return sendGroupUpdate(context, groupId, memberAddresses, name, avatarBytes, adminAddresses);
} else {
Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(groupId), true);
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient);
@@ -123,7 +163,8 @@ public class GroupManager {
@NonNull String groupId,
@NonNull Set members,
@Nullable String groupName,
- @Nullable byte[] avatar)
+ @Nullable byte[] avatar,
+ @NonNull Set admins)
{
try {
Attachment avatarAttachment = null;
@@ -131,15 +172,20 @@ public class GroupManager {
Recipient groupRecipient = Recipient.from(context, groupAddress, false);
List numbers = new LinkedList<>();
-
for (Address member : members) {
numbers.add(member.serialize());
}
+ List adminNumbers = new LinkedList<>();
+ for (Address admin : admins) {
+ adminNumbers.add(admin.serialize());
+ }
+
GroupContext.Builder groupContextBuilder = GroupContext.newBuilder()
.setId(ByteString.copyFrom(GroupUtil.getDecodedId(groupId)))
.setType(GroupContext.Type.UPDATE)
- .addAllMembers(numbers);
+ .addAllMembers(numbers)
+ .addAllAdmins(adminNumbers);
if (groupName != null) groupContextBuilder.setName(groupName);
GroupContext groupContext = groupContextBuilder.build();
diff --git a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java
index 4033a8823f..9f216e2fad 100644
--- a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java
+++ b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java
@@ -8,6 +8,7 @@ import android.support.annotation.Nullable;
import com.google.protobuf.ByteString;
import org.thoughtcrime.securesms.ApplicationContext;
+import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
@@ -23,14 +24,20 @@ import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.sms.IncomingGroupMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
+import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.GroupUtil;
+import org.thoughtcrime.securesms.util.TextSecurePreferences;
+import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup.Type;
+import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
+import org.whispersystems.signalservice.loki.utilities.PromiseUtil;
import java.util.Collections;
import java.util.HashSet;
@@ -38,6 +45,8 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Set;
+import kotlin.Unit;
+
import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.AttachmentPointer;
import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
@@ -58,7 +67,7 @@ public class GroupMessageProcessor {
GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
SignalServiceGroup group = message.getGroupInfo().get();
- String id = GroupUtil.getEncodedId(group.getGroupId(), false);
+ String id = GroupUtil.getEncodedId(group);
Optional record = database.getGroup(id);
if (record.isPresent() && group.getType() == Type.UPDATE) {
@@ -81,12 +90,13 @@ public class GroupMessageProcessor {
boolean outgoing)
{
GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
- String id = GroupUtil.getEncodedId(group.getGroupId(), false);
+ String id = GroupUtil.getEncodedId(group);
GroupContext.Builder builder = createGroupContext(group);
builder.setType(GroupContext.Type.UPDATE);
SignalServiceAttachment avatar = group.getAvatar().orNull();
List members = group.getMembers().isPresent() ? new LinkedList() : null;
+ List admins = group.getAdmins().isPresent() ? new LinkedList<>() : null;
if (group.getMembers().isPresent()) {
for (String member : group.getMembers().get()) {
@@ -94,8 +104,25 @@ public class GroupMessageProcessor {
}
}
+ // We should only create the group if we are part of the member list
+ String hexEncodedPublicKey = getMasterHexEncodedPublicKey(context, TextSecurePreferences.getLocalNumber(context));
+ if (members == null || !members.contains(Address.fromSerialized(hexEncodedPublicKey))) {
+ Log.d("Loki - Group Message", "Received a group create message which doesn't include us in the member list. Ignoring.");
+ return null;
+ }
+
+ if (group.getAdmins().isPresent()) {
+ for (String admin : group.getAdmins().get()) {
+ admins.add(Address.fromExternal(context, admin));
+ }
+ }
+
database.create(id, group.getName().orNull(), members,
- avatar != null && avatar.isPointer() ? avatar.asPointer() : null, null);
+ avatar != null && avatar.isPointer() ? avatar.asPointer() : null, null, admins);
+
+ if (group.getMembers().isPresent()) {
+ establishSessionsWithMembersIfNeeded(context, group.getMembers().get());
+ }
return storeMessage(context, content, group, builder.build(), outgoing);
}
@@ -108,7 +135,16 @@ public class GroupMessageProcessor {
{
GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
- String id = GroupUtil.getEncodedId(group.getGroupId(), false);
+ String id = GroupUtil.getEncodedId(group);
+
+ // Only update group if admin sent the message
+ if (group.getGroupType() == SignalServiceGroup.GroupType.SIGNAL) {
+ String hexEncodedPublicKey = getMasterHexEncodedPublicKey(context, content.getSender());
+ if (!groupRecord.getAdmins().contains(Address.fromSerialized(hexEncodedPublicKey))) {
+ Log.d("Loki - Group Message", "Received a group update message from a non-admin user for " + id +". Ignoring.");
+ return null;
+ }
+ }
Set recordMembers = new HashSet<>(groupRecord.getMembers());
Set messageMembers = new HashSet<>();
@@ -141,7 +177,9 @@ public class GroupMessageProcessor {
}
if (missingMembers.size() > 0) {
- // TODO We should tell added and missing about each-other.
+ for (Address removedMember : missingMembers) {
+ builder.addMembers(removedMember.serialize());
+ }
}
if (group.getName().isPresent() || group.getAvatar().isPresent()) {
@@ -155,6 +193,10 @@ public class GroupMessageProcessor {
if (!groupRecord.isActive()) database.setActive(id, true);
+ if (group.getMembers().isPresent()) {
+ establishSessionsWithMembersIfNeeded(context, group.getMembers().get());
+ }
+
return storeMessage(context, content, group, builder.build(), outgoing);
}
@@ -163,7 +205,10 @@ public class GroupMessageProcessor {
@NonNull SignalServiceGroup group,
@NonNull GroupRecord record)
{
- if (record.getMembers().contains(Address.fromExternal(context, content.getSender()))) {
+ String hexEncodedPublicKey = getMasterHexEncodedPublicKey(context, content.getSender());
+ String ourPublicKey = getMasterHexEncodedPublicKey(context, TextSecurePreferences.getLocalNumber(context));
+ // If the requester is a group member and we are admin then we should send them the group update
+ if (record.getMembers().contains(Address.fromSerialized(hexEncodedPublicKey)) && record.getAdmins().contains(Address.fromSerialized(ourPublicKey))) {
ApplicationContext.getInstance(context)
.getJobManager()
.add(new PushGroupUpdateJob(content.getSender(), group.getGroupId()));
@@ -179,14 +224,15 @@ public class GroupMessageProcessor {
boolean outgoing)
{
GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
- String id = GroupUtil.getEncodedId(group.getGroupId(), false);
+ String id = GroupUtil.getEncodedId(group);
List members = record.getMembers();
GroupContext.Builder builder = createGroupContext(group);
builder.setType(GroupContext.Type.QUIT);
- if (members.contains(Address.fromExternal(context, content.getSender()))) {
- database.remove(id, Address.fromExternal(context, content.getSender()));
+ String hexEncodedPublicKey = getMasterHexEncodedPublicKey(context, content.getSender());
+ if (members.contains(Address.fromExternal(context, hexEncodedPublicKey))) {
+ database.remove(id, Address.fromExternal(context, hexEncodedPublicKey));
if (outgoing) database.setActive(id, false);
return storeMessage(context, content, group, builder.build(), outgoing);
@@ -204,14 +250,14 @@ public class GroupMessageProcessor {
{
if (group.getAvatar().isPresent()) {
ApplicationContext.getInstance(context).getJobManager()
- .add(new AvatarDownloadJob(group.getGroupId()));
+ .add(new AvatarDownloadJob(GroupUtil.getEncodedId(group)));
}
try {
if (outgoing) {
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
- Address addres = Address.fromExternal(context, GroupUtil.getEncodedId(group.getGroupId(), false));
- Recipient recipient = Recipient.from(context, addres, false);
+ Address address = Address.fromExternal(context, GroupUtil.getEncodedId(group));
+ Recipient recipient = Recipient.from(context, address, false);
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(recipient, storage, null, content.getTimestamp(), 0, null, Collections.emptyList(), Collections.emptyList());
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
long messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null);
@@ -260,7 +306,38 @@ public class GroupMessageProcessor {
builder.addAllMembers(group.getMembers().get());
}
+ if (group.getAdmins().isPresent()) {
+ builder.addAllAdmins(group.getAdmins().get());
+ }
+
return builder;
}
+ private static String getMasterHexEncodedPublicKey(Context context, String hexEncodedPublicKey) {
+ String ourPublicKey = TextSecurePreferences.getLocalNumber(context);
+ try {
+ String masterHexEncodedPublicKey = hexEncodedPublicKey.equalsIgnoreCase(ourPublicKey)
+ ? TextSecurePreferences.getMasterHexEncodedPublicKey(context)
+ : PromiseUtil.timeout(LokiStorageAPI.shared.getPrimaryDevicePublicKey(hexEncodedPublicKey), 5000).get();
+ return masterHexEncodedPublicKey != null ? masterHexEncodedPublicKey : hexEncodedPublicKey;
+ } catch (Exception e) {
+ return hexEncodedPublicKey;
+ }
+ }
+
+ private static void establishSessionsWithMembersIfNeeded(Context context, List members) {
+ String ourNumber = TextSecurePreferences.getLocalNumber(context);
+ for (String member : members) {
+ // Make sure we have session with all of the members secondary devices
+ LokiStorageAPI.shared.getAllDevicePublicKeys(member).success(devices -> {
+ if (devices.contains(ourNumber)) { return Unit.INSTANCE; }
+ for (String device : devices) {
+ SignalProtocolAddress protocolAddress = new SignalProtocolAddress(device, SignalServiceAddress.DEFAULT_DEVICE_ID);
+ boolean haveSession = new TextSecureSessionStore(context).containsSession(protocolAddress);
+ if (!haveSession) { MessageSender.sendBackgroundSessionRequest(context, device); }
+ }
+ return Unit.INSTANCE;
+ });
+ }
+ }
}
diff --git a/src/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java b/src/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java
index 3536051086..77e4e1c883 100644
--- a/src/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java
@@ -40,9 +40,9 @@ public class AvatarDownloadJob extends BaseJob implements InjectableType {
@Inject SignalServiceMessageReceiver receiver;
- private byte[] groupId;
+ private String groupId;
- public AvatarDownloadJob(@NonNull byte[] groupId) {
+ public AvatarDownloadJob(@NonNull String groupId) {
this(new Job.Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setMaxAttempts(10)
@@ -50,14 +50,14 @@ public class AvatarDownloadJob extends BaseJob implements InjectableType {
groupId);
}
- private AvatarDownloadJob(@NonNull Job.Parameters parameters, @NonNull byte[] groupId) {
+ private AvatarDownloadJob(@NonNull Job.Parameters parameters, @NonNull String groupId) {
super(parameters);
this.groupId = groupId;
}
@Override
public @NonNull Data serialize() {
- return new Data.Builder().putString(KEY_GROUP_ID, GroupUtil.getEncodedId(groupId, false)).build();
+ return new Data.Builder().putString(KEY_GROUP_ID, groupId).build();
}
@Override
@@ -67,9 +67,8 @@ public class AvatarDownloadJob extends BaseJob implements InjectableType {
@Override
public void onRun() throws IOException {
- String encodeId = GroupUtil.getEncodedId(groupId, false);
GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
- Optional record = database.getGroup(encodeId);
+ Optional record = database.getGroup(groupId);
File attachment = null;
try {
@@ -97,7 +96,7 @@ public class AvatarDownloadJob extends BaseJob implements InjectableType {
InputStream inputStream = receiver.retrieveAttachment(pointer, attachment, MAX_AVATAR_SIZE);
Bitmap avatar = BitmapUtil.createScaledBitmap(context, new AttachmentModel(attachment, key, 0, digest), 500, 500);
- database.updateAvatar(encodeId, avatar);
+ database.updateAvatar(groupId, avatar);
inputStream.close();
}
} catch (BitmapDecodingException | NonSuccessfulResponseCodeException | InvalidMessageException e) {
@@ -120,11 +119,7 @@ public class AvatarDownloadJob extends BaseJob implements InjectableType {
public static final class Factory implements Job.Factory {
@Override
public @NonNull AvatarDownloadJob create(@NonNull Parameters parameters, @NonNull Data data) {
- try {
- return new AvatarDownloadJob(parameters, GroupUtil.getDecodedId(data.getString(KEY_GROUP_ID)));
- } catch (IOException e) {
- throw new AssertionError(e);
- }
+ return new AvatarDownloadJob(parameters, data.getString(KEY_GROUP_ID));
}
}
}
diff --git a/src/org/thoughtcrime/securesms/jobs/DirectoryRefreshJob.java b/src/org/thoughtcrime/securesms/jobs/DirectoryRefreshJob.java
index 5b367dd335..24d4e7d48b 100644
--- a/src/org/thoughtcrime/securesms/jobs/DirectoryRefreshJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/DirectoryRefreshJob.java
@@ -9,9 +9,7 @@ import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.logging.Log;
-
import org.thoughtcrime.securesms.recipients.Recipient;
-import org.thoughtcrime.securesms.util.DirectoryHelper;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import java.io.IOException;
@@ -67,11 +65,11 @@ public class DirectoryRefreshJob extends BaseJob {
public void onRun() throws IOException {
Log.i(TAG, "DirectoryRefreshJob.onRun()");
- if (recipient == null) {
- DirectoryHelper.refreshDirectory(context, notifyOfNewUsers);
- } else {
- DirectoryHelper.refreshDirectoryFor(context, recipient);
- }
+// if (recipient == null) {
+// DirectoryHelper.refreshDirectory(context, notifyOfNewUsers);
+// } else {
+// DirectoryHelper.refreshDirectoryFor(context, recipient);
+// }
}
@Override
diff --git a/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java b/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java
index ac6e206930..47411c08f0 100644
--- a/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java
@@ -244,7 +244,7 @@ public class MmsDownloadJob extends BaseJob {
}
if (members.size() > 2) {
- group = Optional.of(Address.fromSerialized(DatabaseFactory.getGroupDatabase(context).getOrCreateGroupForMembers(new LinkedList<>(members), true)));
+ group = Optional.of(Address.fromSerialized(DatabaseFactory.getGroupDatabase(context).getOrCreateGroupForMembers(new LinkedList<>(members), true, new LinkedList<>())));
}
IncomingMediaMessage message = new IncomingMediaMessage(from, group, body, retrieved.getDate() * 1000L, attachments, subscriptionId, 0, false, false);
diff --git a/src/org/thoughtcrime/securesms/jobs/MultiDeviceGroupUpdateJob.java b/src/org/thoughtcrime/securesms/jobs/MultiDeviceGroupUpdateJob.java
index be44c92233..52a81d5d57 100644
--- a/src/org/thoughtcrime/securesms/jobs/MultiDeviceGroupUpdateJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/MultiDeviceGroupUpdateJob.java
@@ -85,7 +85,7 @@ public class MultiDeviceGroupUpdateJob extends BaseJob implements InjectableType
reader = DatabaseFactory.getGroupDatabase(context).getGroups();
while ((record = reader.getNext()) != null) {
- if (!record.isMms()) {
+ if (!record.isMms() && !record.isPublicChat() && !record.isRSSFeed()) {
List members = new LinkedList<>();
for (Address member : record.getMembers()) {
diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
index c8a5e426b6..6282120598 100644
--- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
@@ -291,6 +291,9 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
// Loki - Handle friend request acceptance if needed
acceptFriendRequestIfNeeded(content);
+ // Loki - Session requests
+ handleSessionRequestIfNeeded(content);
+
// Loki - Store pre key bundle
// We shouldn't store it if it's a pairing message
if (!content.getPairingAuthorisation().isPresent()) {
@@ -335,7 +338,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
} else {
// Loki - Don't process session restore message any further
- if (message.isSessionRestore()) { return; }
+ if (message.isSessionRestore() || message.isSessionRequest()) { return; }
+
if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId);
else if (message.isGroupUpdate()) handleGroupMessage(content, message, smsMessageId);
else if (message.isExpirationUpdate())
@@ -345,7 +349,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
else if (message.getBody().isPresent())
handleTextMessage(content, message, smsMessageId, Optional.absent());
- if (message.getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId(), false))) {
+ if (message.getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getGroupInfo().get()))) {
handleUnknownGroupMessage(content, message.getGroupInfo().get());
}
@@ -601,9 +605,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
private void handleUnknownGroupMessage(@NonNull SignalServiceContent content,
@NonNull SignalServiceGroup group)
{
- ApplicationContext.getInstance(context)
- .getJobManager()
- .add(new RequestGroupInfoJob(content.getSender(), group.getGroupId()));
+ if (group.getGroupType() == SignalServiceGroup.GroupType.SIGNAL) {
+ ApplicationContext.getInstance(context)
+ .getJobManager()
+ .add(new RequestGroupInfoJob(content.getSender(), group.getGroupId()));
+ }
}
private void handleExpirationUpdate(@NonNull SignalServiceContent content,
@@ -690,7 +696,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
Log.d("Loki", "Sent friend request to " + pubKey);
} else if (status == LokiThreadFriendRequestStatus.REQUEST_RECEIVED) {
// Accept the incoming friend request
- becomeFriendsWithContact(pubKey, false);
+ becomeFriendsWithContact(pubKey, false, false);
// Send them an accept message back
MessageSender.sendBackgroundMessage(context, pubKey);
Log.d("Loki", "Became friends with " + deviceContact.getNumber());
@@ -728,7 +734,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
threadId = handleSynchronizeSentTextMessage(message);
}
- if (message.getMessage().getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getMessage().getGroupInfo().get().getGroupId(), false))) {
+ if (message.getMessage().getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getMessage().getGroupInfo().get()))) {
handleUnknownGroupMessage(content, message.getMessage().getGroupInfo().get());
}
@@ -738,7 +744,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
Recipient recipient = null;
if (message.getDestination().isPresent()) recipient = Recipient.from(context, Address.fromSerialized(message.getDestination().get()), false);
- else if (message.getMessage().getGroupInfo().isPresent()) recipient = Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(message.getMessage().getGroupInfo().get().getGroupId(), false)), false);
+ else if (message.getMessage().getGroupInfo().isPresent()) recipient = Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(message.getMessage().getGroupInfo().get())), false);
if (recipient != null && !recipient.isSystemContact() && !recipient.isProfileSharing()) {
@@ -845,8 +851,16 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
Optional> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().or(""));
Optional sticker = getStickerAttachment(message.getSticker());
- // If message is from group then we need to map it to the correct sender
- Address sender = message.isGroupUpdate() ? Address.fromSerialized(content.getSender()) : primaryDeviceRecipient.getAddress();
+ Address sender = primaryDeviceRecipient.getAddress();
+
+ // If message is from group then we need to map it to get the sender of the message
+ if (message.isGroupMessage()) {
+ sender = getPrimaryDeviceRecipient(content.getSender()).getAddress();
+ }
+
+ // Ignore messages from ourselves
+ if (sender.serialize().equalsIgnoreCase(TextSecurePreferences.getLocalNumber(context))) { return; }
+
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(sender, message.getTimestamp(), -1,
message.getExpiresInSeconds() * 1000L, false, content.isNeedsReceipt(), message.getBody(), message.getGroupInfo(), message.getAttachments(),
quote, sharedContacts, linkPreviews, sticker);
@@ -1030,8 +1044,16 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
} else {
notifyTypingStoppedFromIncomingMessage(primaryDeviceRecipient, content.getSender(), content.getSenderDevice());
- // If message is from group then we need to map it to the correct sender
- Address sender = message.isGroupUpdate() ? Address.fromSerialized(content.getSender()) : primaryDeviceRecipient.getAddress();
+ Address sender = primaryDeviceRecipient.getAddress();
+
+ // If message is from group then we need to map it to get the sender of the message
+ if (message.isGroupMessage()) {
+ sender = getPrimaryDeviceRecipient(content.getSender()).getAddress();
+ }
+
+ // Ignore messages from ourselves
+ if (sender.serialize().equalsIgnoreCase(TextSecurePreferences.getLocalNumber(context))) { return; }
+
IncomingTextMessage _textMessage = new IncomingTextMessage(sender,
content.getSenderDevice(),
message.getTimestamp(), body,
@@ -1217,10 +1239,29 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
private void acceptFriendRequestIfNeeded(@NonNull SignalServiceContent content) {
// If we get anything other than a friend request, we can assume that we have a session with the other user
if (content.isFriendRequest() || isGroupChatMessage(content)) { return; }
- becomeFriendsWithContact(content.getSender(), true);
+ becomeFriendsWithContact(content.getSender(), true, false);
}
- private void becomeFriendsWithContact(String pubKey, boolean syncContact) {
+ private void handleSessionRequestIfNeeded(@NonNull SignalServiceContent content) {
+ if (content.isFriendRequest() && isSessionRequest(content)) {
+ // Check if the session request from a member in one of our groups or our friend
+ LokiStorageAPI.shared.getPrimaryDevicePublicKey(content.getSender()).success(primaryDevicePublicKey -> {
+ String sender = primaryDevicePublicKey != null ? primaryDevicePublicKey : content.getSender();
+ long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(sender), false));
+ LokiThreadFriendRequestStatus threadFriendRequestStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadID);
+ boolean isOurFriend = threadFriendRequestStatus == LokiThreadFriendRequestStatus.FRIENDS;
+ boolean isInOneOfOurGroups = DatabaseFactory.getGroupDatabase(context).signalGroupsHaveMember(sender);
+ boolean shouldAcceptSessionRequest = isOurFriend || isInOneOfOurGroups;
+ if (shouldAcceptSessionRequest) {
+ // Send a background message to acknowledge session request
+ MessageSender.sendBackgroundMessage(context, content.getSender());
+ }
+ return Unit.INSTANCE;
+ });
+ }
+ }
+
+ private void becomeFriendsWithContact(String pubKey, boolean syncContact, boolean force) {
LokiThreadDatabase lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context);
Recipient contactID = Recipient.from(context, Address.fromSerialized(pubKey), false);
if (contactID.isGroupRecipient()) return;
@@ -1228,6 +1269,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(contactID);
LokiThreadFriendRequestStatus threadFriendRequestStatus = lokiThreadDatabase.getFriendRequestStatus(threadID);
if (threadFriendRequestStatus == LokiThreadFriendRequestStatus.FRIENDS) { return; }
+
+ // We shouldn't be able to skip from None -> Friends in normal circumstances.
+ // Multi-device is the exception to this rule because we want to automatically be friends with a secondary device
+ if (!force && threadFriendRequestStatus == LokiThreadFriendRequestStatus.NONE) { return; }
+
// If the thread's friend request status is not `FRIENDS`, but we're receiving a message,
// it must be a friend request accepted message. Declining a friend request doesn't send a message.
lokiThreadDatabase.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.FRIENDS);
@@ -1248,13 +1294,13 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
private void updateFriendRequestStatusIfNeeded(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message) {
- if (!content.isFriendRequest() || message.isGroupUpdate()) { return; }
+ if (!content.isFriendRequest() || message.isGroupMessage() || message.isSessionRequest()) { return; }
// This handles the case where another user sends us a regular message without authorisation
Promise promise = PromiseUtil.timeout(MultiDeviceUtilities.shouldAutomaticallyBecomeFriendsWithDevice(content.getSender(), context), 8000);
boolean shouldBecomeFriends = PromiseUtil.get(promise, false);
if (shouldBecomeFriends) {
// Become friends AND update the message they sent
- becomeFriendsWithContact(content.getSender(), true);
+ becomeFriendsWithContact(content.getSender(), true, true);
// Send them an accept message back
MessageSender.sendBackgroundMessage(context, content.getSender());
} else {
@@ -1561,10 +1607,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
long threadId;
if (typingMessage.getGroupId().isPresent()) {
+ // Typing messages should only apply to signal groups, thus we use `getEncodedId`
Address groupAddress = Address.fromSerialized(GroupUtil.getEncodedId(typingMessage.getGroupId().get(), false));
Recipient groupRecipient = Recipient.from(context, groupAddress, false);
- threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient);
+ threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(groupRecipient);
} else {
// See if we need to redirect the message
author = getPrimaryDeviceRecipient(content.getSender());
@@ -1712,15 +1759,15 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
private Recipient getSyncMessageDestination(SentTranscriptMessage message) {
- if (message.getMessage().getGroupInfo().isPresent()) {
- return Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(message.getMessage().getGroupInfo().get().getGroupId(), false)), false);
+ if (message.getMessage().isGroupMessage()) {
+ return Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(message.getMessage().getGroupInfo().get())), false);
} else {
return Recipient.from(context, Address.fromSerialized(message.getDestination().get()), false);
}
}
private Recipient getSyncMessagePrimaryDestination(SentTranscriptMessage message) {
- if (message.getMessage().getGroupInfo().isPresent()) {
+ if (message.getMessage().isGroupMessage()) {
return getSyncMessageDestination(message);
} else {
return getPrimaryDeviceRecipient(message.getDestination().get());
@@ -1728,15 +1775,15 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
private Recipient getMessageDestination(SignalServiceContent content, SignalServiceDataMessage message) {
- if (message.getGroupInfo().isPresent()) {
- return Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId(), false)), false);
+ if (message.isGroupMessage()) {
+ return Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(message.getGroupInfo().get())), false);
} else {
return Recipient.from(context, Address.fromSerialized(content.getSender()), false);
}
}
private Recipient getMessagePrimaryDestination(SignalServiceContent content, SignalServiceDataMessage message) {
- if (message.getGroupInfo().isPresent()) {
+ if (message.isGroupMessage()) {
return getMessageDestination(content, message);
} else {
return getPrimaryDeviceRecipient(content.getSender());
@@ -1794,7 +1841,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
return true;
} else if (conversation.isGroupRecipient()) {
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
- Optional groupId = message.getGroupInfo().isPresent() ? Optional.of(GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId(), false))
+ Optional groupId = message.getGroupInfo().isPresent() ? Optional.of(GroupUtil.getEncodedId(message.getGroupInfo().get()))
: Optional.absent();
if (groupId.isPresent() && groupDatabase.isUnknownGroup(groupId.get())) {
@@ -1830,8 +1877,12 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
return false;
}
+ private boolean isSessionRequest(SignalServiceContent content) {
+ return content.getDataMessage().isPresent() && content.getDataMessage().get().isSessionRequest();
+ }
+
private boolean isGroupChatMessage(SignalServiceContent content) {
- return content.getDataMessage().isPresent() && content.getDataMessage().get().getGroupInfo().isPresent();
+ return content.getDataMessage().isPresent() && content.getDataMessage().get().isGroupMessage();
}
private void resetRecipientToPush(@NonNull Recipient recipient) {
diff --git a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java
index d5d4fa6c99..2bc5241f58 100644
--- a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java
@@ -12,8 +12,10 @@ import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
+import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory;
+import org.thoughtcrime.securesms.database.GroupReceiptDatabase.GroupReceiptInfo;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
@@ -25,13 +27,17 @@ import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.logging.Log;
+import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.util.GroupUtil;
+import org.thoughtcrime.securesms.util.TextSecurePreferences;
+import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
@@ -46,10 +52,13 @@ import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
import org.whispersystems.signalservice.loki.api.LokiPublicChat;
+import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
+import org.whispersystems.signalservice.loki.utilities.PromiseUtil;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
@@ -157,7 +166,28 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
else if (!existingNetworkFailures.isEmpty()) target = Stream.of(existingNetworkFailures).map(NetworkFailure::getAddress).toList();
else target = getGroupMessageRecipients(message.getRecipient().getAddress().toGroupString(), messageId);
- List results = deliver(message, target);
+ String localNumber = TextSecurePreferences.getLocalNumber(context);
+
+ // Only send messages to the contacts we have sessions with
+ List validTargets = Stream.of(target).filter(member -> {
+ // Our device is always valid
+ if (member.serialize().equalsIgnoreCase(localNumber)) { return true; }
+
+ SignalProtocolAddress protocolAddress = new SignalProtocolAddress(member.toPhoneString(), SignalServiceAddress.DEFAULT_DEVICE_ID);
+ boolean hasSession = new TextSecureSessionStore(context).containsSession(protocolAddress);
+ if (hasSession) { return true; }
+
+ // We should allow sending if we have a prekeybundle for the contact
+ return DatabaseFactory.getLokiPreKeyBundleDatabase(context).hasPreKeyBundle(member.toPhoneString());
+ }).toList();
+
+ // Send a session request to the other devices
+ List others = Stream.of(target).filter(t -> !validTargets.contains(t)).toList();
+ for (Address device : others) {
+ MessageSender.sendBackgroundSessionRequest(context, device.toPhoneString());
+ }
+
+ List results = deliver(message, validTargets);
List networkFailures = Stream.of(results).filter(SendMessageResult::isNetworkFailure).map(result -> new NetworkFailure(Address.fromSerialized(result.getAddress().getNumber()))).toList();
List identityMismatches = Stream.of(results).filter(result -> result.getIdentityFailure() != null).map(result -> new IdentityKeyMismatch(Address.fromSerialized(result.getAddress().getNumber()), result.getIdentityFailure().getIdentityKey())).toList();
Set successAddresses = Stream.of(results).filter(result -> result.getSuccess() != null).map(result -> Address.fromSerialized(result.getAddress().getNumber())).collect(Collectors.toSet());
@@ -231,7 +261,15 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
throws IOException, UntrustedIdentityException, UndeliverableMessageException {
// rotateSenderCertificateIfNecessary();
- String groupId = message.getRecipient().getAddress().toGroupString();
+ // Messages shouldn't be able to be sent to RSS Feeds
+ Address groupAddress = message.getRecipient().getAddress();
+ if (groupAddress.isRSSFeed()) {
+ List results = new ArrayList<>();
+ for (Address destination : destinations) results.add(SendMessageResult.networkFailure(new SignalServiceAddress(destination.toPhoneString())));
+ return results;
+ }
+
+ String groupId = groupAddress.toGroupString();
Optional profileKey = getProfileKey(message.getRecipient());
Optional quote = getQuoteFor(message);
Optional sticker = getStickerFor(message);
@@ -247,24 +285,28 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
.map(recipient -> UnidentifiedAccessUtil.getAccessFor(context, recipient))
.toList();
- if (message.isGroup()) {
+ SignalServiceGroup.GroupType groupType = SignalServiceGroup.GroupType.SIGNAL;
+ if (groupAddress.isPublicChat()) {
+ groupType = SignalServiceGroup.GroupType.PUBLIC_CHAT;
+ }
+
+ if (message.isGroup() && groupAddress.isSignalGroup()) {
+ // Loki - Only send GroupUpdate or GroupQuit to signal groups
OutgoingGroupMediaMessage groupMessage = (OutgoingGroupMediaMessage) message;
GroupContext groupContext = groupMessage.getGroupContext();
SignalServiceAttachment avatar = attachmentPointers.isEmpty() ? null : attachmentPointers.get(0);
SignalServiceGroup.Type type = groupMessage.isGroupQuit() ? SignalServiceGroup.Type.QUIT : SignalServiceGroup.Type.UPDATE;
- SignalServiceGroup group = new SignalServiceGroup(type, GroupUtil.getDecodedId(groupId), groupContext.getName(), groupContext.getMembersList(), avatar);
+ SignalServiceGroup group = new SignalServiceGroup(type, GroupUtil.getDecodedId(groupId), groupType, groupContext.getName(), groupContext.getMembersList(), avatar, groupContext.getAdminsList());
SignalServiceDataMessage groupDataMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(message.getSentTimeMillis())
.withExpiration(message.getRecipient().getExpireMessages())
+ .withBody(message.getBody())
.asGroupMessage(group)
.build();
- // Loki - Disable group updates for now
- List results = new ArrayList<>();
- for (Address destination : destinations) results.add(SendMessageResult.success(new SignalServiceAddress(destination.toPhoneString()), false, false));
- return results;
+ return messageSender.sendMessage(messageId, addresses, unidentifiedAccess, groupDataMessage);
} else {
- SignalServiceGroup group = new SignalServiceGroup(GroupUtil.getDecodedId(groupId));
+ SignalServiceGroup group = new SignalServiceGroup(GroupUtil.getDecodedId(groupId), groupType);
SignalServiceDataMessage groupMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(message.getSentTimeMillis())
.asGroupMessage(group)
@@ -284,26 +326,56 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
}
private @NonNull List getGroupMessageRecipients(String groupId, long messageId) {
- ArrayList result = new ArrayList<>();
+ if (GroupUtil.isRssFeed(groupId)) { return new ArrayList<>(); }
- // Loki - All group messages should be directed to their respective servers
- long threadID = GroupManager.getThreadIdFromGroupId(groupId, context);
- LokiPublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID);
- if (publicChat != null) {
- // We need to somehow maintain information that will allow the sender to map
- // a recipient to the correct public chat thread, and so this might be a bit hacky
- result.add(Address.fromPublicChatGroupID(groupId));
+ // Loki - All public chat group messages should be directed to their respective servers
+ if (GroupUtil.isPublicChat(groupId)) {
+ ArrayList result = new ArrayList<>();
+ long threadID = GroupManager.getThreadIdFromGroupId(groupId, context);
+ LokiPublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID);
+ if (publicChat != null) {
+ result.add(Address.fromSerialized(groupId));
+ }
+ return result;
+ } else {
+ /*
+ Our biggest assumption here is that group members will only consist of primary devices.
+ No secondary device should be able to be added to a group.
+ */
+ List destinations = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageId);
+
+ Set memberSet = new HashSet<>();
+ if (destinations.isEmpty()) {
+ List groupMembers = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, false);
+ memberSet.addAll(Stream.of(groupMembers).map(Recipient::getAddress).toList());
+ } else {
+ memberSet.addAll(Stream.of(destinations).map(GroupReceiptInfo::getAddress).toList());
+ }
+
+ // Replace primary device public key with ours so message syncing works correctly
+ String masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
+ String localNumber = TextSecurePreferences.getLocalNumber(context);
+ if (masterHexEncodedPublicKey != null && memberSet.contains(Address.fromSerialized(masterHexEncodedPublicKey))) {
+ memberSet.remove(Address.fromSerialized(masterHexEncodedPublicKey));
+ memberSet.add(Address.fromSerialized(localNumber));
+ }
+
+ // Add secondary devices to the list. We shouldn't add our secondary devices
+ for (Address member : memberSet) {
+ if (!member.isPhone() || member.serialize().equalsIgnoreCase(localNumber)) { continue; }
+ try {
+ List secondaryDevices = PromiseUtil.timeout(LokiStorageAPI.shared.getSecondaryDevicePublicKeys(member.serialize()), 5000).get();
+ memberSet.addAll(Stream.of(secondaryDevices).map(string -> {
+ // Loki - Calling .map(Address::fromSerialized) is causing errors, thus we use the long method :(
+ return Address.fromSerialized(string);
+ }).toList());
+ } catch (Exception e) {
+ // Timed out, go to the next member
+ }
+ }
+
+ return new LinkedList<>(memberSet);
}
-
- return result;
-
- /*
- List destinations = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageId);
- if (!destinations.isEmpty()) return Stream.of(destinations).map(GroupReceiptInfo::getAddress).toList();
-
- List members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, false);
- return Stream.of(members).map(Recipient::getAddress).toList();
- */
}
public static class Factory implements Job.Factory {
diff --git a/src/org/thoughtcrime/securesms/jobs/PushGroupUpdateJob.java b/src/org/thoughtcrime/securesms/jobs/PushGroupUpdateJob.java
index e12d094c11..460b028932 100644
--- a/src/org/thoughtcrime/securesms/jobs/PushGroupUpdateJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/PushGroupUpdateJob.java
@@ -97,15 +97,20 @@ public class PushGroupUpdateJob extends BaseJob implements InjectableType {
}
List members = new LinkedList<>();
-
for (Address member : record.get().getMembers()) {
members.add(member.serialize());
}
+ List admins = new LinkedList<>();
+ for (Address admin : record.get().getAdmins()) {
+ admins.add(admin.serialize());
+ }
+
SignalServiceGroup groupContext = SignalServiceGroup.newBuilder(Type.UPDATE)
.withAvatar(avatar)
- .withId(groupId)
+ .withId(groupId, SignalServiceGroup.GroupType.SIGNAL)
.withMembers(members)
+ .withAdmins(admins)
.withName(record.get().getTitle())
.build();
diff --git a/src/org/thoughtcrime/securesms/jobs/RequestGroupInfoJob.java b/src/org/thoughtcrime/securesms/jobs/RequestGroupInfoJob.java
index 380aeadc9b..795e1060c0 100644
--- a/src/org/thoughtcrime/securesms/jobs/RequestGroupInfoJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/RequestGroupInfoJob.java
@@ -71,7 +71,7 @@ public class RequestGroupInfoJob extends BaseJob implements InjectableType {
@Override
public void onRun() throws IOException, UntrustedIdentityException {
SignalServiceGroup group = SignalServiceGroup.newBuilder(Type.REQUEST_INFO)
- .withId(groupId)
+ .withId(groupId, SignalServiceGroup.GroupType.SIGNAL)
.build();
SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
diff --git a/src/org/thoughtcrime/securesms/jobs/TypingSendJob.java b/src/org/thoughtcrime/securesms/jobs/TypingSendJob.java
index babe7fe04e..c6ecc30e2f 100644
--- a/src/org/thoughtcrime/securesms/jobs/TypingSendJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/TypingSendJob.java
@@ -20,6 +20,7 @@ import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage.Action;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus;
import org.whispersystems.signalservice.loki.utilities.PromiseUtil;
import java.util.Collections;
@@ -101,6 +102,10 @@ public class TypingSendJob extends BaseJob implements InjectableType {
// Loki - Don't send typing indicators in group chats or to ourselves
if (recipient.isGroupRecipient()) { return; }
+
+ LokiThreadFriendRequestStatus friendRequestStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadId);
+ if (friendRequestStatus != LokiThreadFriendRequestStatus.FRIENDS) { return; }
+
boolean isOurDevice = PromiseUtil.get(MultiDeviceUtilities.isOneOfOurDevices(context, recipient.getAddress()), false);
if (!isOurDevice) {
messageSender.sendTyping(0, addresses, unidentifiedAccess, typingMessage);
diff --git a/src/org/thoughtcrime/securesms/loki/FriendRequestHandler.kt b/src/org/thoughtcrime/securesms/loki/FriendRequestHandler.kt
index a8d9cae2eb..e59a2875a9 100644
--- a/src/org/thoughtcrime/securesms/loki/FriendRequestHandler.kt
+++ b/src/org/thoughtcrime/securesms/loki/FriendRequestHandler.kt
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.loki
import android.content.Context
import nl.komponents.kovenant.ui.successUi
import org.thoughtcrime.securesms.database.DatabaseFactory
+import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.loki.messaging.LokiMessageFriendRequestStatus
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus
@@ -62,7 +63,7 @@ object FriendRequestHandler {
fun updateLastFriendRequestMessage(context: Context, threadId: Long, status: LokiMessageFriendRequestStatus) {
if (threadId < 0) { return }
val recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId) ?: return
- if (!recipient.address.isPhone) { return }
+ if (!recipient.address.isPhone || recipient.address.serialize() == TextSecurePreferences.getLocalNumber(context)) { return }
val messages = DatabaseFactory.getSmsDatabase(context).getAllMessageIDs(threadId)
val lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context)
@@ -81,7 +82,7 @@ object FriendRequestHandler {
// We only want to update the last message status if we're not friends with any of their linked devices
// This ensures that we don't spam the UI with accept/decline messages
val recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId) ?: return
- if (!recipient.address.isPhone) { return }
+ if (!recipient.address.isPhone || recipient.address.serialize() == TextSecurePreferences.getLocalNumber(context)) { return }
isFriendsWithAnyLinkedDevice(context, recipient).successUi { isFriends ->
if (isFriends) { return@successUi }
diff --git a/src/org/thoughtcrime/securesms/loki/LokiPublicChatManager.kt b/src/org/thoughtcrime/securesms/loki/LokiPublicChatManager.kt
index 3552efb63f..0a959521f5 100644
--- a/src/org/thoughtcrime/securesms/loki/LokiPublicChatManager.kt
+++ b/src/org/thoughtcrime/securesms/loki/LokiPublicChatManager.kt
@@ -14,7 +14,6 @@ import org.thoughtcrime.securesms.loki.redesign.messaging.LokiPublicChatPoller
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.loki.api.LokiPublicChat
-import java.util.*
class LokiPublicChatManager(private val context: Context) {
private var chats = mutableMapOf()
@@ -50,10 +49,10 @@ class LokiPublicChatManager(private val context: Context) {
public fun addChat(server: String, channel: Long, name: String): LokiPublicChat {
val chat = LokiPublicChat(channel, server, name, true)
- var threadID = GroupManager.getThreadId(chat.id, context)
+ var threadID = GroupManager.getPublicChatThreadId(chat.id, context)
// Create the group if we don't have one
if (threadID < 0) {
- val result = GroupManager.createGroup(chat.id, context, HashSet(), null, chat.displayName, false)
+ val result = GroupManager.createPublicChatGroup(chat.id, context, null, chat.displayName)
threadID = result.threadId
}
DatabaseFactory.getLokiThreadDatabase(context).setPublicChat(chat, threadID)
@@ -74,7 +73,7 @@ class LokiPublicChatManager(private val context: Context) {
removedChatThreadIds.forEach { pollers.remove(it)?.stop() }
// Only append to chats if we have a thread for the chat
- chats = chatsInDB.filter { GroupManager.getThreadId(it.value.id, context) > -1 }.toMutableMap()
+ chats = chatsInDB.filter { GroupManager.getPublicChatThreadId(it.value.id, context) > -1 }.toMutableMap()
}
private fun listenToThreadDeletion(threadID: Long) {
diff --git a/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt b/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt
index 3348af38c6..b6aefdaa3a 100644
--- a/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt
+++ b/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt
@@ -45,6 +45,11 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
fun getFriendRequestStatus(threadID: Long): LokiThreadFriendRequestStatus {
if (threadID < 0) { return LokiThreadFriendRequestStatus.NONE }
+
+ // Loki - Friend request logic doesn't apply to group chats, always treat them as friends
+ val recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadID)
+ if (recipient != null && recipient.isGroupRecipient) { return LokiThreadFriendRequestStatus.FRIENDS; }
+
val database = databaseHelper.readableDatabase
val result = database.get(friendRequestTableName, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) { cursor ->
cursor.getInt(friendRequestStatus)
diff --git a/src/org/thoughtcrime/securesms/loki/PushBackgroundMessageSendJob.kt b/src/org/thoughtcrime/securesms/loki/PushBackgroundMessageSendJob.kt
index 347711abdc..8aa084eb54 100644
--- a/src/org/thoughtcrime/securesms/loki/PushBackgroundMessageSendJob.kt
+++ b/src/org/thoughtcrime/securesms/loki/PushBackgroundMessageSendJob.kt
@@ -33,6 +33,9 @@ data class BackgroundMessage private constructor(val data: Map) {
@JvmStatic
fun createSessionRestore(recipient: String) = BackgroundMessage(mapOf( "recipient" to recipient, "friendRequest" to true, "sessionRestore" to true ))
+
+ @JvmStatic
+ fun createSessionRequest(recipient: String) = BackgroundMessage(mapOf("recipient" to recipient, "friendRequest" to true, "sessionRequest" to true))
internal fun parse(serialized: String): BackgroundMessage {
val data = JsonUtil.fromJson(serialized, Map::class.java) as? Map ?: throw AssertionError("JSON parsing failed")
@@ -99,6 +102,10 @@ class PushBackgroundMessageSendJob private constructor(
dataMessage.asSessionRestore(true)
}
+ if (message.get("sessionRequest", false)) {
+ dataMessage.asSessionRequest(true)
+ }
+
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
val address = SignalServiceAddress(recipient)
try {
diff --git a/src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupActivity.kt b/src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupActivity.kt
new file mode 100644
index 0000000000..034176745a
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupActivity.kt
@@ -0,0 +1,152 @@
+package org.thoughtcrime.securesms.loki.redesign.activities
+
+import android.content.Intent
+import android.graphics.Bitmap
+import android.os.AsyncTask
+import android.os.Bundle
+import android.support.v4.app.LoaderManager
+import android.support.v4.content.Loader
+import android.support.v7.widget.LinearLayoutManager
+import android.view.Menu
+import android.view.MenuItem
+import android.widget.Toast
+import kotlinx.android.synthetic.main.activity_create_closed_group.*
+import kotlinx.android.synthetic.main.activity_linked_devices.recyclerView
+import network.loki.messenger.R
+import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
+import org.thoughtcrime.securesms.conversation.ConversationActivity
+import org.thoughtcrime.securesms.database.Address
+import org.thoughtcrime.securesms.database.ThreadDatabase
+import org.thoughtcrime.securesms.groups.GroupManager
+import org.thoughtcrime.securesms.mms.GlideApp
+import org.thoughtcrime.securesms.recipients.Recipient
+import org.thoughtcrime.securesms.util.TextSecurePreferences
+import org.whispersystems.libsignal.util.guava.Optional
+import java.lang.ref.WeakReference
+
+class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), MemberClickListener, LoaderManager.LoaderCallbacks> {
+ private var members = listOf()
+ set(value) { field = value; createClosedGroupAdapter.members = value }
+
+ private val createClosedGroupAdapter by lazy {
+ val result = CreateClosedGroupAdapter(this)
+ result.glide = GlideApp.with(this)
+ result.memberClickListener = this
+ result
+ }
+
+ private val selectedMembers: Set
+ get() { return createClosedGroupAdapter.selectedMembers }
+
+ // region Lifecycle
+ override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
+ super.onCreate(savedInstanceState, isReady)
+ setContentView(R.layout.activity_create_closed_group)
+ supportActionBar!!.title = "New Closed Group"
+ recyclerView.adapter = createClosedGroupAdapter
+ recyclerView.layoutManager = LinearLayoutManager(this)
+ LoaderManager.getInstance(this).initLoader(0, null, this)
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ menuInflater.inflate(R.menu.menu_create_closed_group, menu)
+ return true
+ }
+ // endregion
+
+ // region Updating
+ override fun onCreateLoader(id: Int, bundle: Bundle?): Loader> {
+ return CreateClosedGroupLoader(this)
+ }
+
+ override fun onLoadFinished(loader: Loader>, members: List) {
+ update(members)
+ }
+
+ override fun onLoaderReset(loader: Loader>) {
+ update(listOf())
+ }
+
+ private fun update(members: List) {
+ this.members = members
+ }
+ // endregion
+
+ // region Interaction
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ val id = item.itemId
+ when(id) {
+ R.id.createClosedGroupButton -> createClosedGroup()
+ else -> { /* Do nothing */ }
+ }
+ return super.onOptionsItemSelected(item)
+ }
+
+ override fun onMemberClick(member: String) {
+ createClosedGroupAdapter.onMemberClick(member)
+ }
+
+ private fun createClosedGroup() {
+ val name = nameEditText.text.trim()
+ if (name.isEmpty()) {
+ return Toast.makeText(this, "Please enter a group name", Toast.LENGTH_LONG).show()
+ }
+ if (name.length >= 64) {
+ return Toast.makeText(this, "Please enter a shorter group name", Toast.LENGTH_LONG).show()
+ }
+ val selectedMembers = this.selectedMembers
+ if (selectedMembers.count() < 1) {
+ return Toast.makeText(this, "Please pick at least 1 group member", Toast.LENGTH_LONG).show()
+ }
+ val recipients = selectedMembers.map {
+ Recipient.from(this, Address.fromSerialized(it), false)
+ }.toSet()
+ val ourNumber = TextSecurePreferences.getMasterHexEncodedPublicKey(this) ?: TextSecurePreferences.getLocalNumber(this)
+ val local = Recipient.from(this, Address.fromSerialized(ourNumber), false)
+ CreateClosedGroupTask(WeakReference(this), null, name.toString(), recipients, setOf(local)).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
+ }
+
+ private fun handleOpenConversation(threadId: Long, recipient: Recipient) {
+ val intent = Intent(this, ConversationActivity::class.java)
+ intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId)
+ intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT)
+ intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.address)
+ startActivity(intent)
+ finish()
+ }
+ // endregion
+
+ // region Tasks
+ internal class CreateClosedGroupTask(
+ private val activity: WeakReference,
+ private val avatar: Bitmap?,
+ private val name: String?,
+ private val members: Set,
+ private val admins: Set
+ ) : AsyncTask>() {
+
+ override fun doInBackground(vararg params: Void?): Optional {
+ val activity = activity.get() ?: return Optional.absent()
+ return Optional.of(GroupManager.createGroup(activity, members, avatar, name, false, admins))
+ }
+
+ override fun onPostExecute(result: Optional) {
+ val activity = activity.get()
+ if (activity == null) {
+ super.onPostExecute(result)
+ return
+ }
+
+ if (result.isPresent && result.get().threadId > -1) {
+ if (!activity.isFinishing) {
+ activity.handleOpenConversation(result.get().threadId, result.get().groupRecipient)
+ }
+ } else {
+ super.onPostExecute(result)
+ Toast.makeText(activity.applicationContext,
+ R.string.GroupCreateActivity_contacts_invalid_number, Toast.LENGTH_LONG).show()
+ }
+ }
+ }
+ // endregion
+}
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupAdapter.kt b/src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupAdapter.kt
new file mode 100644
index 0000000000..bd8c6eff02
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupAdapter.kt
@@ -0,0 +1,48 @@
+package org.thoughtcrime.securesms.loki.redesign.activities
+
+import android.content.Context
+import android.support.v7.widget.RecyclerView
+import android.view.ViewGroup
+import org.thoughtcrime.securesms.loki.redesign.views.UserView
+import org.thoughtcrime.securesms.mms.GlideRequests
+
+class CreateClosedGroupAdapter(private val context: Context) : RecyclerView.Adapter() {
+ lateinit var glide: GlideRequests
+ val selectedMembers = mutableSetOf()
+ var members = listOf()
+ set(value) { field = value; notifyDataSetChanged() }
+ var memberClickListener: MemberClickListener? = null
+
+ class ViewHolder(val view: UserView) : RecyclerView.ViewHolder(view)
+
+ override fun getItemCount(): Int {
+ return members.size
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val view = UserView(context)
+ return ViewHolder(view)
+ }
+
+ override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
+ val member = members[position]
+ viewHolder.view.setOnClickListener { memberClickListener?.onMemberClick(member) }
+ val isSelected = selectedMembers.contains(member)
+ viewHolder.view.bind(member, isSelected, glide)
+ }
+
+ fun onMemberClick(member: String) {
+ if (selectedMembers.contains(member)) {
+ selectedMembers.remove(member)
+ } else {
+ selectedMembers.add(member)
+ }
+ val index = members.indexOf(member)
+ notifyItemChanged(index)
+ }
+}
+
+interface MemberClickListener {
+
+ fun onMemberClick(member: String)
+}
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupLoader.kt b/src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupLoader.kt
new file mode 100644
index 0000000000..620d47b5c6
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/loki/redesign/activities/CreateClosedGroupLoader.kt
@@ -0,0 +1,33 @@
+package org.thoughtcrime.securesms.loki.redesign.activities
+
+import android.content.Context
+import org.thoughtcrime.securesms.database.DatabaseFactory
+import org.thoughtcrime.securesms.util.AsyncLoader
+import org.thoughtcrime.securesms.util.TextSecurePreferences
+import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus
+
+class CreateClosedGroupLoader(context: Context) : AsyncLoader>(context) {
+
+ override fun loadInBackground(): List {
+ val threadDatabase = DatabaseFactory.getThreadDatabase(context)
+ val lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context)
+ val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
+ val deviceLinks = DatabaseFactory.getLokiAPIDatabase(context).getPairingAuthorisations(userHexEncodedPublicKey)
+ val userLinkedDeviceHexEncodedPublicKeys = deviceLinks.flatMap {
+ listOf( it.primaryDevicePublicKey.toLowerCase(), it.secondaryDevicePublicKey.toLowerCase() )
+ }.toMutableSet()
+ userLinkedDeviceHexEncodedPublicKeys.add(userHexEncodedPublicKey.toLowerCase())
+ val cursor = threadDatabase.conversationList
+ val reader = threadDatabase.readerFor(cursor)
+ val result = mutableListOf()
+ while (reader.next != null) {
+ val thread = reader.current
+ if (thread.recipient.isGroupRecipient) { continue }
+ if (lokiThreadDatabase.getFriendRequestStatus(thread.threadId) != LokiThreadFriendRequestStatus.FRIENDS) { continue }
+ val hexEncodedPublicKey = thread.recipient.address.toString().toLowerCase()
+ if (userLinkedDeviceHexEncodedPublicKeys.contains(hexEncodedPublicKey)) { continue }
+ result.add(hexEncodedPublicKey)
+ }
+ return result
+ }
+}
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/loki/redesign/activities/HomeActivity.kt b/src/org/thoughtcrime/securesms/loki/redesign/activities/HomeActivity.kt
index 798528dbbe..bb24b04dac 100644
--- a/src/org/thoughtcrime/securesms/loki/redesign/activities/HomeActivity.kt
+++ b/src/org/thoughtcrime/securesms/loki/redesign/activities/HomeActivity.kt
@@ -83,6 +83,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
profileButton.hexEncodedPublicKey = hexEncodedPublicKey
profileButton.update()
profileButton.setOnClickListener { openSettings() }
+ createClosedGroupButton.setOnClickListener { createClosedGroup() }
joinPublicChatButton.setOnClickListener { joinPublicChat() }
// Set up seed reminder view
val isMasterDevice = (TextSecurePreferences.getMasterHexEncodedPublicKey(this) == null)
@@ -182,6 +183,11 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
show(intent)
}
+ private fun createClosedGroup() {
+ val intent = Intent(this, CreateClosedGroupActivity::class.java)
+ show(intent)
+ }
+
private fun joinPublicChat() {
val intent = Intent(this, JoinPublicChatActivity::class.java)
show(intent)
diff --git a/src/org/thoughtcrime/securesms/loki/redesign/messaging/LokiPublicChatPoller.kt b/src/org/thoughtcrime/securesms/loki/redesign/messaging/LokiPublicChatPoller.kt
index e1359d9198..1a600e4293 100644
--- a/src/org/thoughtcrime/securesms/loki/redesign/messaging/LokiPublicChatPoller.kt
+++ b/src/org/thoughtcrime/securesms/loki/redesign/messaging/LokiPublicChatPoller.kt
@@ -111,7 +111,7 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
// region Polling
private fun getDataMessage(message: LokiPublicChatMessage): SignalServiceDataMessage {
val id = group.id.toByteArray()
- val serviceGroup = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, null, null, null)
+ val serviceGroup = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, SignalServiceGroup.GroupType.PUBLIC_CHAT, null, null, null, null)
val quote = if (message.quote != null) {
SignalServiceDataMessage.Quote(message.quote!!.quotedMessageTimestamp, SignalServiceAddress(message.quote!!.quoteeHexEncodedPublicKey), message.quote!!.quotedMessageBody, listOf())
} else {
diff --git a/src/org/thoughtcrime/securesms/loki/redesign/messaging/LokiRSSFeedPoller.kt b/src/org/thoughtcrime/securesms/loki/redesign/messaging/LokiRSSFeedPoller.kt
index fa252d4531..d7a6c3398b 100644
--- a/src/org/thoughtcrime/securesms/loki/redesign/messaging/LokiRSSFeedPoller.kt
+++ b/src/org/thoughtcrime/securesms/loki/redesign/messaging/LokiRSSFeedPoller.kt
@@ -63,7 +63,7 @@ class LokiRSSFeedPoller(private val context: Context, private val feed: LokiRSSF
bodyAsHTML = matcher.replaceAll("$2 ($1)")
val body = Html.fromHtml(bodyAsHTML).toString().trim()
val id = feed.id.toByteArray()
- val x1 = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, null, null, null)
+ val x1 = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, SignalServiceGroup.GroupType.RSS_FEED, null, null, null, null)
val x2 = SignalServiceDataMessage(timestamp, x1, null, body)
val x3 = SignalServiceContent(x2, "Loki", SignalServiceAddress.DEFAULT_DEVICE_ID, timestamp, false, false)
PushDecryptJob(context).handleTextMessage(x3, x2, Optional.absent(), Optional.absent())
diff --git a/src/org/thoughtcrime/securesms/loki/redesign/views/UserView.kt b/src/org/thoughtcrime/securesms/loki/redesign/views/UserView.kt
new file mode 100644
index 0000000000..2b6e6536f7
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/loki/redesign/views/UserView.kt
@@ -0,0 +1,52 @@
+package org.thoughtcrime.securesms.loki.redesign.views
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.widget.LinearLayout
+import kotlinx.android.synthetic.main.view_conversation.view.profilePictureView
+import kotlinx.android.synthetic.main.view_user.view.*
+import network.loki.messenger.R
+import org.thoughtcrime.securesms.database.Address
+import org.thoughtcrime.securesms.mms.GlideRequests
+import org.thoughtcrime.securesms.recipients.Recipient
+
+class UserView : LinearLayout {
+ var user: String? = null
+
+ // region Lifecycle
+ constructor(context: Context) : super(context) {
+ setUpViewHierarchy()
+ }
+
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
+ setUpViewHierarchy()
+ }
+
+ constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
+ setUpViewHierarchy()
+ }
+
+ constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
+ setUpViewHierarchy()
+ }
+
+ private fun setUpViewHierarchy() {
+ val inflater = context.applicationContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
+ val contentView = inflater.inflate(R.layout.view_user, null)
+ addView(contentView)
+ }
+ // endregion
+
+ // region Updating
+ fun bind(user: String, isSelected: Boolean, glide: GlideRequests) {
+ profilePictureView.hexEncodedPublicKey = user
+ profilePictureView.additionalHexEncodedPublicKey = null
+ profilePictureView.isRSSFeed = false
+ profilePictureView.glide = glide
+ profilePictureView.update()
+ nameTextView.text = Recipient.from(context, Address.fromSerialized(user), false).name ?: "Unknown Contact"
+ tickImageView.setImageResource(if (isSelected) R.drawable.ic_circle_check else R.drawable.ic_circle)
+ }
+ // endregion
+}
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java b/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java
index 8d0c19962c..f4d0a4889e 100644
--- a/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java
+++ b/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java
@@ -79,7 +79,7 @@ public class IncomingMediaMessage {
this.quote = quote.orNull();
this.unidentified = unidentified;
- if (group.isPresent()) this.groupId = Address.fromSerialized(GroupUtil.getEncodedId(group.get().getGroupId(), false));
+ if (group.isPresent()) this.groupId = Address.fromSerialized(GroupUtil.getEncodedId(group.get()));
else this.groupId = null;
this.attachments.addAll(PointerAttachment.forPointers(attachments));
diff --git a/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java b/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java
index cfafcb7132..1c787355c3 100644
--- a/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java
+++ b/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java
@@ -78,7 +78,7 @@ public class IncomingTextMessage implements Parcelable {
this.unidentified = unidentified;
if (group.isPresent()) {
- this.groupId = Address.fromSerialized(GroupUtil.getEncodedId(group.get().getGroupId(), false));
+ this.groupId = Address.fromSerialized(GroupUtil.getEncodedId(group.get()));
} else {
this.groupId = null;
}
diff --git a/src/org/thoughtcrime/securesms/sms/MessageSender.java b/src/org/thoughtcrime/securesms/sms/MessageSender.java
index 9bb30f7dc6..df26a6ec03 100644
--- a/src/org/thoughtcrime/securesms/sms/MessageSender.java
+++ b/src/org/thoughtcrime/securesms/sms/MessageSender.java
@@ -17,7 +17,6 @@
package org.thoughtcrime.securesms.sms;
import android.content.Context;
-import android.os.AsyncTask;
import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.ApplicationContext;
@@ -60,11 +59,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
-import org.whispersystems.signalservice.api.SignalServiceMessageSender;
-import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
-import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
-import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus;
import org.whispersystems.signalservice.loki.utilities.PromiseUtil;
@@ -143,6 +138,10 @@ public class MessageSender {
public static void sendRestoreSessionMessage(Context context, String contactHexEncodedPublicKey) {
ApplicationContext.getInstance(context).getJobManager().add(new PushBackgroundMessageSendJob(BackgroundMessage.createSessionRestore(contactHexEncodedPublicKey)));
}
+
+ public static void sendBackgroundSessionRequest(Context context, String contactHexEncodedPublicKey) {
+ ApplicationContext.getInstance(context).getJobManager().add(new PushBackgroundMessageSendJob(BackgroundMessage.createSessionRequest(contactHexEncodedPublicKey)));
+ }
// endregion
public static long send(final Context context,
@@ -202,7 +201,7 @@ public class MessageSender {
if (attachment != null) { message.getAttachments().add(attachment); }
long messageID = database.insertMessageOutbox(message, allocatedThreadId, forceSms, insertListener);
// Loki - Set the message's friend request status as soon as it has hit the database
- if (message.isFriendRequest) {
+ if (message.isFriendRequest && !recipient.getAddress().isGroup() && !message.isGroup()) {
FriendRequestHandler.updateFriendRequestState(context, FriendRequestHandler.ActionType.Sending, messageID, allocatedThreadId);
}
sendMediaMessage(context, recipient, forceSms, messageID, message.getExpiresIn());
@@ -215,7 +214,7 @@ public class MessageSender {
try {
long messageID = database.insertMessageOutbox(message, allocatedThreadId, forceSms, insertListener);
// Loki - Set the message's friend request status as soon as it has hit the database
- if (message.isFriendRequest) {
+ if (message.isFriendRequest && !recipient.getAddress().isGroup() && !message.isGroup()) {
FriendRequestHandler.updateFriendRequestState(context, FriendRequestHandler.ActionType.Sending, messageID, allocatedThreadId);
}
sendMediaMessage(context, recipient, forceSms, messageID, message.getExpiresIn());
diff --git a/src/org/thoughtcrime/securesms/util/GroupUtil.java b/src/org/thoughtcrime/securesms/util/GroupUtil.java
index 224887fb0a..9268e78264 100644
--- a/src/org/thoughtcrime/securesms/util/GroupUtil.java
+++ b/src/org/thoughtcrime/securesms/util/GroupUtil.java
@@ -11,11 +11,13 @@ import network.loki.messenger.R;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
+import org.thoughtcrime.securesms.database.GroupDatabase.*;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientModifiedListener;
import org.whispersystems.libsignal.util.guava.Optional;
+import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import java.io.IOException;
import java.util.Collections;
@@ -28,12 +30,32 @@ public class GroupUtil {
private static final String ENCODED_SIGNAL_GROUP_PREFIX = "__textsecure_group__!";
private static final String ENCODED_MMS_GROUP_PREFIX = "__signal_mms_group__!";
+ private static final String ENCODED_PUBLIC_CHAT_GROUP_PREFIX = "__loki_public_chat_group__!";
+ private static final String ENCODED_RSS_FEED_GROUP_PREFIX = "__loki_rss_feed_group__!";
private static final String TAG = GroupUtil.class.getSimpleName();
+ public static String getEncodedId(SignalServiceGroup group) {
+ byte[] groupId = group.getGroupId();
+ if (group.getGroupType() == SignalServiceGroup.GroupType.PUBLIC_CHAT) {
+ return getEncodedPublicChatId(groupId);
+ } else if (group.getGroupType() == SignalServiceGroup.GroupType.RSS_FEED) {
+ return getEncodedRSSFeedId(groupId);
+ }
+ return getEncodedId(groupId, false);
+ }
+
public static String getEncodedId(byte[] groupId, boolean mms) {
return (mms ? ENCODED_MMS_GROUP_PREFIX : ENCODED_SIGNAL_GROUP_PREFIX) + Hex.toStringCondensed(groupId);
}
+ public static String getEncodedPublicChatId(byte[] groupId) {
+ return ENCODED_PUBLIC_CHAT_GROUP_PREFIX + Hex.toStringCondensed(groupId);
+ }
+
+ public static String getEncodedRSSFeedId(byte[] groupId) {
+ return ENCODED_RSS_FEED_GROUP_PREFIX + Hex.toStringCondensed(groupId);
+ }
+
public static byte[] getDecodedId(String groupId) throws IOException {
if (!isEncodedGroup(groupId)) {
throw new IOException("Invalid encoding");
@@ -48,13 +70,21 @@ public class GroupUtil {
}
public static boolean isEncodedGroup(@NonNull String groupId) {
- return groupId.startsWith(ENCODED_SIGNAL_GROUP_PREFIX) || groupId.startsWith(ENCODED_MMS_GROUP_PREFIX);
+ return groupId.startsWith(ENCODED_SIGNAL_GROUP_PREFIX) || groupId.startsWith(ENCODED_MMS_GROUP_PREFIX) || groupId.startsWith(ENCODED_PUBLIC_CHAT_GROUP_PREFIX) || groupId.startsWith(ENCODED_RSS_FEED_GROUP_PREFIX);
}
public static boolean isMmsGroup(@NonNull String groupId) {
return groupId.startsWith(ENCODED_MMS_GROUP_PREFIX);
}
+ public static boolean isPublicChat(@NonNull String groupId) {
+ return groupId.startsWith(ENCODED_PUBLIC_CHAT_GROUP_PREFIX);
+ }
+
+ public static boolean isRssFeed(@NonNull String groupId) {
+ return groupId.startsWith(ENCODED_RSS_FEED_GROUP_PREFIX);
+ }
+
@WorkerThread
public static Optional createGroupLeaveMessage(@NonNull Context context, @NonNull Recipient groupRecipient) {
String encodedGroupId = groupRecipient.getAddress().toGroupString();
@@ -88,7 +118,7 @@ public class GroupUtil {
}
try {
- GroupContext groupContext = GroupContext.parseFrom(Base64.decode(encodedGroup));
+ GroupContext groupContext = GroupContext.parseFrom(Base64.decode(encodedGroup));
return new GroupDescription(context, groupContext);
} catch (IOException e) {
Log.w(TAG, e);
@@ -100,24 +130,49 @@ public class GroupUtil {
@NonNull private final Context context;
@Nullable private final GroupContext groupContext;
- @Nullable private final List members;
+ private final List members;
+ private final List removedMembers;
+ private boolean ourDeviceWasRemoved;
public GroupDescription(@NonNull Context context, @Nullable GroupContext groupContext) {
this.context = context.getApplicationContext();
this.groupContext = groupContext;
- if (groupContext == null || groupContext.getMembersList().isEmpty()) {
- this.members = null;
- } else {
- this.members = new LinkedList<>();
+ this.members = new LinkedList<>();
+ this.removedMembers = new LinkedList<>();
+ this.ourDeviceWasRemoved = false;
- for (String member : groupContext.getMembersList()) {
- this.members.add(Recipient.from(context, Address.fromExternal(context, member), true));
+ if (groupContext != null && !groupContext.getMembersList().isEmpty()) {
+ List memberList = groupContext.getMembersList();
+ List currentMembers = getCurrentGroupMembers();
+
+ // Add them to the member or removed members lists
+ for (String member : memberList) {
+ Address address = Address.fromSerialized(member);
+ Recipient recipient = Recipient.from(context, address, true);
+ if (currentMembers == null || currentMembers.contains(address)) {
+ this.members.add(recipient);
+ } else {
+ this.removedMembers.add(recipient);
+ }
+ }
+
+ // Check if our device was removed
+ if (!removedMembers.isEmpty()) {
+ String masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
+ String hexEncodedPublicKey = masterHexEncodedPublicKey != null ? masterHexEncodedPublicKey : TextSecurePreferences.getLocalNumber(context);
+ Recipient self = Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false);
+ ourDeviceWasRemoved = removedMembers.contains(self);
}
}
}
public String toString(Recipient sender) {
+ // Show the local removed message
+ if (ourDeviceWasRemoved) {
+ return context.getString(R.string.GroupUtil_you_were_removed_from_group);
+ }
+
StringBuilder description = new StringBuilder();
description.append(context.getString(R.string.MessageRecord_s_updated_group, sender.toShortString()));
@@ -127,14 +182,20 @@ public class GroupUtil {
String title = groupContext.getName();
- if (members != null) {
+ if (!members.isEmpty()) {
description.append("\n");
description.append(context.getResources().getQuantityString(R.plurals.GroupUtil_joined_the_group,
members.size(), toString(members)));
}
+ if (!removedMembers.isEmpty()) {
+ description.append("\n");
+ description.append(context.getResources().getQuantityString(R.plurals.GroupUtil_removed_from_the_group,
+ removedMembers.size(), toString(removedMembers)));
+ }
+
if (title != null && !title.trim().isEmpty()) {
- if (members != null) description.append(" ");
+ if (!members.isEmpty()) description.append(" ");
else description.append("\n");
description.append(context.getString(R.string.GroupUtil_group_name_is_now, title));
}
@@ -143,7 +204,7 @@ public class GroupUtil {
}
public void addListener(RecipientModifiedListener listener) {
- if (this.members != null) {
+ if (!this.members.isEmpty()) {
for (Recipient member : this.members) {
member.addListener(listener);
}
@@ -162,5 +223,23 @@ public class GroupUtil {
return result;
}
+
+ private List getCurrentGroupMembers() {
+ if (groupContext == null) { return null; }
+ GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
+ byte[] decodedGroupId = groupContext.getId().toByteArray();
+ String signalGroupId = getEncodedId(decodedGroupId, false);
+ String publicChatId = getEncodedPublicChatId(decodedGroupId);
+ String rssFeedId = getEncodedRSSFeedId(decodedGroupId);
+ GroupRecord groupRecord = null;
+ if (!groupDatabase.isUnknownGroup(signalGroupId)) {
+ groupRecord = groupDatabase.getGroup(signalGroupId).orNull();
+ } else if (!groupDatabase.isUnknownGroup(publicChatId)) {
+ groupRecord = groupDatabase.getGroup(publicChatId).orNull();
+ } else if (!groupDatabase.isUnknownGroup(rssFeedId)) {
+ groupRecord = groupDatabase.getGroup(rssFeedId).orNull();
+ }
+ return (groupRecord != null) ? groupRecord.getMembers() : null;
+ }
}
}
diff --git a/src/org/thoughtcrime/securesms/util/IdentityUtil.java b/src/org/thoughtcrime/securesms/util/IdentityUtil.java
index 07336f0629..56dff28e6a 100644
--- a/src/org/thoughtcrime/securesms/util/IdentityUtil.java
+++ b/src/org/thoughtcrime/securesms/util/IdentityUtil.java
@@ -74,8 +74,9 @@ public class IdentityUtil {
GroupDatabase.GroupRecord groupRecord;
while ((groupRecord = reader.getNext()) != null) {
+ if (groupRecord.isRSSFeed() || groupRecord.isPublicChat()) { continue; }
if (groupRecord.getMembers().contains(recipient.getAddress()) && groupRecord.isActive() && !groupRecord.isMms()) {
- SignalServiceGroup group = new SignalServiceGroup(groupRecord.getId());
+ SignalServiceGroup group = new SignalServiceGroup(groupRecord.getId(), SignalServiceGroup.GroupType.SIGNAL);
if (remote) {
IncomingTextMessage incoming = new IncomingTextMessage(recipient.getAddress(), 1, time, null, Optional.of(group), 0, false);
@@ -126,8 +127,9 @@ public class IdentityUtil {
GroupDatabase.GroupRecord groupRecord;
while ((groupRecord = reader.getNext()) != null) {
+ if (groupRecord.isRSSFeed() || groupRecord.isPublicChat()) { continue; }
if (groupRecord.getMembers().contains(recipient.getAddress()) && groupRecord.isActive()) {
- SignalServiceGroup group = new SignalServiceGroup(groupRecord.getId());
+ SignalServiceGroup group = new SignalServiceGroup(groupRecord.getId(), SignalServiceGroup.GroupType.SIGNAL);
IncomingTextMessage incoming = new IncomingTextMessage(recipient.getAddress(), 1, time, null, Optional.of(group), 0, false);
IncomingIdentityUpdateMessage groupUpdate = new IncomingIdentityUpdateMessage(incoming);