Merge pull request #60 from loki-project/private-group-chat

Private group chat
This commit is contained in:
gmbnt 2020-02-04 13:23:47 +11:00 committed by GitHub
commit 75cd2d6d28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 1063 additions and 212 deletions

View File

@ -158,6 +158,9 @@
<activity <activity
android:name="org.thoughtcrime.securesms.loki.redesign.activities.CreatePrivateChatActivity" android:name="org.thoughtcrime.securesms.loki.redesign.activities.CreatePrivateChatActivity"
android:screenOrientation="portrait" /> android:screenOrientation="portrait" />
<activity
android:name="org.thoughtcrime.securesms.loki.redesign.activities.CreateClosedGroupActivity"
android:screenOrientation="portrait" />
<activity <activity
android:name="org.thoughtcrime.securesms.loki.redesign.activities.JoinPublicChatActivity" android:name="org.thoughtcrime.securesms.loki.redesign.activities.JoinPublicChatActivity"
android:screenOrientation="portrait" android:screenOrientation="portrait"

View File

@ -0,0 +1,6 @@
<vector android:height="24dp" android:viewportHeight="15"
android:viewportWidth="15" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000"
android:pathData="M7.5,7.5m-6.5,0a6.5,6.5 0,1 1,13 0a6.5,6.5 0,1 1,-13 0"
android:strokeColor="#FFFFFF" android:strokeWidth="1"/>
</vector>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/default_session_background"
android:orientation="vertical">
<EditText
style="@style/SmallSessionEditText"
android:id="@+id/nameEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/large_spacing"
android:layout_marginTop="@dimen/medium_spacing"
android:layout_marginRight="@dimen/large_spacing"
android:layout_marginBottom="@dimen/large_spacing"
android:hint="Enter a group name" />
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@color/separator" />
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@ -40,13 +40,27 @@
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:layout_marginLeft="64dp" /> android:layout_marginLeft="64dp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_alignParentRight="true"
android:layout_centerVertical="true">
<ImageView
android:id="@+id/createClosedGroupButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_group_white_24dp" />
<ImageView <ImageView
android:id="@+id/joinPublicChatButton" android:id="@+id/joinPublicChatButton"
android:layout_width="24dp" android:layout_width="24dp"
android:layout_height="24dp" android:layout_height="24dp"
android:src="@drawable/ic_globe" android:layout_marginLeft="@dimen/medium_spacing"
android:layout_alignParentRight="true" android:src="@drawable/ic_globe" />
android:layout_centerVertical="true" />
</LinearLayout>
</RelativeLayout> </RelativeLayout>

53
res/layout/view_user.xml Normal file
View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/conversation_view_background"
android:orientation="vertical">
<LinearLayout
android:id="@+id/contentView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="@dimen/medium_spacing">
<org.thoughtcrime.securesms.loki.redesign.views.ProfilePictureView
android:id="@+id/profilePictureView"
android:layout_width="@dimen/medium_profile_picture_size"
android:layout_height="@dimen/medium_profile_picture_size" />
<TextView
android:id="@+id/nameTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/medium_spacing"
android:maxLines="1"
android:ellipsize="end"
android:textSize="@dimen/medium_font_size"
android:textStyle="bold"
android:textColor="@color/text"
android:text="Spiderman" />
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<ImageView
android:id="@+id/tickImageView"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginLeft="@dimen/medium_spacing"
android:src="@drawable/ic_circle" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@color/separator" />
</LinearLayout>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:title="Done"
android:id="@+id/createClosedGroupButton"
app:showAsAction="always" />
</menu>

View File

@ -1073,7 +1073,12 @@
<item quantity="one">%1$s joined the group.</item> <item quantity="one">%1$s joined the group.</item>
<item quantity="other">%1$s joined the group.</item> <item quantity="other">%1$s joined the group.</item>
</plurals> </plurals>
<plurals name="GroupUtil_removed_from_the_group">
<item quantity="one">%1$s was removed from the group.</item>
<item quantity="other">%1$s were removed from the group.</item>
</plurals>
<string name="GroupUtil_group_name_is_now">Group name is now \'%1$s\'.</string> <string name="GroupUtil_group_name_is_now">Group name is now \'%1$s\'.</string>
<string name="GroupUtil_you_were_removed_from_group">You were removed from the group.</string>
<!-- profile_group_share_view --> <!-- profile_group_share_view -->
<string name="profile_group_share_view__make_your_profile_name_and_photo_visible_to_this_group">Make your profile name and photo visible to this group?</string> <string name="profile_group_share_view__make_your_profile_name_and_photo_visible_to_this_group">Make your profile name and photo visible to this group?</string>

View File

@ -4,7 +4,7 @@
<domain includeSubdomains="true">imaginary.stream</domain> <domain includeSubdomains="true">imaginary.stream</domain>
<domain includeSubdomains="true">storage.seed1.loki.network</domain> <domain includeSubdomains="true">storage.seed1.loki.network</domain>
<domain includeSubdomains="true">storage.seed2.loki.network</domain> <domain includeSubdomains="true">storage.seed2.loki.network</domain>
<domain includeSubdomains="true">http://public.loki.foundation</domain> <domain includeSubdomains="true">public.loki.foundation:22023</domain>
<domain includeSubdomains="true">127.0.0.1</domain> <domain includeSubdomains="true">127.0.0.1</domain>
</domain-config> </domain-config>
</network-security-config> </network-security-config>

View File

@ -521,18 +521,18 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
public void createDefaultPublicChatsIfNeeded() { public void createDefaultPublicChatsIfNeeded() {
List<LokiPublicChat> defaultPublicChats = LokiPublicChatAPI.Companion.getDefaultChats(BuildConfig.DEBUG); List<LokiPublicChat> defaultPublicChats = LokiPublicChatAPI.Companion.getDefaultChats(BuildConfig.DEBUG);
for (LokiPublicChat publiChat : defaultPublicChats) { for (LokiPublicChat publicChat : defaultPublicChats) {
long threadID = GroupManager.getThreadId(publiChat.getId(), this); long threadID = GroupManager.getPublicChatThreadId(publicChat.getId(), this);
String migrationKey = publiChat.getId() + "_migrated"; String migrationKey = publicChat.getId() + "_migrated";
boolean isChatMigrated = TextSecurePreferences.getBooleanPreference(this, migrationKey, false); boolean isChatMigrated = TextSecurePreferences.getBooleanPreference(this, migrationKey, false);
boolean isChatSetUp = TextSecurePreferences.isChatSetUp(this, publiChat.getId()); boolean isChatSetUp = TextSecurePreferences.isChatSetUp(this, publicChat.getId());
if (!isChatSetUp || !publiChat.isDeletable()) { if (!isChatSetUp || !publicChat.isDeletable()) {
lokiPublicChatManager.addChat(publiChat.getServer(), publiChat.getChannel(), publiChat.getDisplayName()); lokiPublicChatManager.addChat(publicChat.getServer(), publicChat.getChannel(), publicChat.getDisplayName());
TextSecurePreferences.markChatSetUp(this, publiChat.getId()); TextSecurePreferences.markChatSetUp(this, publicChat.getId());
TextSecurePreferences.setBooleanPreference(this, migrationKey, true); TextSecurePreferences.setBooleanPreference(this, migrationKey, true);
} else if (threadID > -1 && !isChatMigrated) { } else if (threadID > -1 && !isChatMigrated) {
// Migrate the old public chats // Migrate the old public chats
DatabaseFactory.getLokiThreadDatabase(this).setPublicChat(publiChat, threadID); DatabaseFactory.getLokiThreadDatabase(this).setPublicChat(publicChat, threadID);
TextSecurePreferences.setBooleanPreference(this, migrationKey, true); TextSecurePreferences.setBooleanPreference(this, migrationKey, true);
} }
} }
@ -545,7 +545,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
for (LokiRSSFeed feed : feeds) { for (LokiRSSFeed feed : feeds) {
boolean isFeedSetUp = TextSecurePreferences.isChatSetUp(this, feed.getId()); boolean isFeedSetUp = TextSecurePreferences.isChatSetUp(this, feed.getId());
if (!isFeedSetUp || !feed.isDeletable()) { 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()); TextSecurePreferences.markChatSetUp(this, feed.getId());
} }
} }
@ -554,7 +554,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
private void createRSSFeedPollersIfNeeded() { private void createRSSFeedPollersIfNeeded() {
// Only create the RSS feed pollers if their threads aren't deleted // Only create the RSS feed pollers if their threads aren't deleted
LokiRSSFeed lokiNewsFeed = lokiNewsFeed(); LokiRSSFeed lokiNewsFeed = lokiNewsFeed();
long lokiNewsFeedThreadID = GroupManager.getThreadId(lokiNewsFeed.getId(), this); long lokiNewsFeedThreadID = GroupManager.getRSSFeedThreadId(lokiNewsFeed.getId(), this);
if (lokiNewsFeedThreadID >= 0 && lokiNewsFeedPoller == null) { if (lokiNewsFeedThreadID >= 0 && lokiNewsFeedPoller == null) {
lokiNewsFeedPoller = new LokiRSSFeedPoller(this, lokiNewsFeed); lokiNewsFeedPoller = new LokiRSSFeedPoller(this, lokiNewsFeed);
// Set up deletion listeners if needed // Set up deletion listeners if needed

View File

@ -72,6 +72,7 @@ import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.io.File; import java.io.File;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -246,7 +247,8 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
return; return;
} }
if (isSignalGroup()) { 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 { } else {
new CreateMmsGroupTask(this, getAdapter().getRecipients()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); new CreateMmsGroupTask(this, getAdapter().getRecipients()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} }
@ -254,7 +256,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
private void handleGroupUpdate() { private void handleGroupUpdate() {
new UpdateSignalGroupTask(this, groupToUpdate.get().id, avatarBmp, 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) { private void handleOpenConversation(long threadId, Recipient recipient) {
@ -344,9 +346,10 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
for (Recipient recipient : members) { for (Recipient recipient : members) {
memberAddresses.add(recipient.getAddress()); 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); Recipient groupRecipient = Recipient.from(activity, Address.fromSerialized(groupId), true);
long threadId = DatabaseFactory.getThreadDatabase(activity).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.DEFAULT); long threadId = DatabaseFactory.getThreadDatabase(activity).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.DEFAULT);
@ -370,16 +373,19 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
protected Bitmap avatar; protected Bitmap avatar;
protected Set<Recipient> members; protected Set<Recipient> members;
protected String name; protected String name;
protected Set<Recipient> admins;
public SignalGroupTask(GroupCreateActivity activity, public SignalGroupTask(GroupCreateActivity activity,
Bitmap avatar, Bitmap avatar,
String name, String name,
Set<Recipient> members) Set<Recipient> members,
Set<Recipient> admins)
{ {
this.activity = activity; this.activity = activity;
this.avatar = avatar; this.avatar = avatar;
this.name = name; this.name = name;
this.members = members; this.members = members;
this.admins = admins;
} }
@Override @Override
@ -403,13 +409,13 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
} }
private static class CreateSignalGroupTask extends SignalGroupTask { private static class CreateSignalGroupTask extends SignalGroupTask {
public CreateSignalGroupTask(GroupCreateActivity activity, Bitmap avatar, String name, Set<Recipient> members) { public CreateSignalGroupTask(GroupCreateActivity activity, Bitmap avatar, String name, Set<Recipient> members, Set<Recipient> admins) {
super(activity, avatar, name, members); super(activity, avatar, name, members, admins);
} }
@Override @Override
protected Optional<GroupActionResult> doInBackground(Void... aVoid) { protected Optional<GroupActionResult> 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 @Override
@ -430,16 +436,16 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
private String groupId; private String groupId;
public UpdateSignalGroupTask(GroupCreateActivity activity, String groupId, public UpdateSignalGroupTask(GroupCreateActivity activity, String groupId,
Bitmap avatar, String name, Set<Recipient> members) Bitmap avatar, String name, Set<Recipient> members, Set<Recipient> admins)
{ {
super(activity, avatar, name, members); super(activity, avatar, name, members, admins);
this.groupId = groupId; this.groupId = groupId;
} }
@Override @Override
protected Optional<GroupActionResult> doInBackground(Void... aVoid) { protected Optional<GroupActionResult> doInBackground(Void... aVoid) {
try { 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) { } catch (InvalidNumberException e) {
return Optional.absent(); return Optional.absent();
} }
@ -491,7 +497,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
for (Recipient recipient : recipients) { for (Recipient recipient : recipients) {
boolean isPush = isActiveInDirectory(recipient); 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, results.add(new Result(null, false, activity.getString(R.string.GroupCreateActivity_cannot_add_non_push_to_existing_group,
recipient.toShortString()))); recipient.toShortString())));
} else if (TextUtils.equals(TextSecurePreferences.getLocalNumber(activity), recipient.getAddress().serialize())) { } else if (TextUtils.equals(TextSecurePreferences.getLocalNumber(activity), recipient.getAddress().serialize())) {
@ -537,11 +543,17 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
existingContacts.addAll(recipients); existingContacts.addAll(recipients);
if (group.isPresent()) { if (group.isPresent()) {
List<Address> adminList = group.get().getAdmins();
final Set<Recipient> admins = new HashSet<>(adminList.size());
for (Address admin : adminList) {
admins.add(Recipient.from(getContext(), admin, false));
}
return Optional.of(new GroupData(groupIds[0], return Optional.of(new GroupData(groupIds[0],
existingContacts, existingContacts,
BitmapUtil.fromByteArray(group.get().getAvatar()), BitmapUtil.fromByteArray(group.get().getAvatar()),
group.get().getAvatar(), group.get().getAvatar(),
group.get().getTitle())); group.get().getTitle(),
admins));
} else { } else {
return Optional.absent(); return Optional.absent();
} }
@ -582,13 +594,15 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
Bitmap avatarBmp; Bitmap avatarBmp;
byte[] avatarBytes; byte[] avatarBytes;
String name; String name;
Set<Recipient> admins;
public GroupData(String id, Set<Recipient> recipients, Bitmap avatarBmp, byte[] avatarBytes, String name) { public GroupData(String id, Set<Recipient> recipients, Bitmap avatarBmp, byte[] avatarBytes, String name, Set<Recipient> admins) {
this.id = id; this.id = id;
this.recipients = recipients; this.recipients = recipients;
this.avatarBmp = avatarBmp; this.avatarBmp = avatarBmp;
this.avatarBytes = avatarBytes; this.avatarBytes = avatarBytes;
this.name = name; this.name = name;
this.admins = admins;
} }
} }
} }

View File

@ -740,9 +740,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
MenuInflater inflater = this.getMenuInflater(); MenuInflater inflater = this.getMenuInflater();
menu.clear(); 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) { if (recipient.getExpireMessages() > 0) {
inflater.inflate(R.menu.conversation_expiring_on, menu); 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); if (isSecureText) inflater.inflate(R.menu.conversation_callable_secure, menu);
else inflater.inflate(R.menu.conversation_callable_insecure, 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); inflater.inflate(R.menu.conversation_group_options, menu);
if (!isPushGroupConversation()) { if (!isPushGroupConversation()) {
@ -1152,10 +1152,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (threadId != -1 && leaveMessage.isPresent()) { if (threadId != -1 && leaveMessage.isPresent()) {
MessageSender.send(this, leaveMessage.get(), threadId, false, null); 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); GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(this);
String groupId = groupRecipient.getAddress().toGroupString(); String groupId = groupRecipient.getAddress().toGroupString();
groupDatabase.setActive(groupId, false); groupDatabase.setActive(groupId, false);
groupDatabase.remove(groupId, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this))); groupDatabase.remove(groupId, Address.fromSerialized(localNumber));
initializeEnabledCheck(); initializeEnabledCheck();
} else { } else {
@ -2077,7 +2081,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} }
private void setBlockedUserState(Recipient recipient, boolean isSecureText, boolean isDefaultSms) { 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); unblockButton.setVisibility(View.GONE);
composePanel.setVisibility(View.GONE); composePanel.setVisibility(View.GONE);
makeDefaultSmsButton.setVisibility(View.GONE); makeDefaultSmsButton.setVisibility(View.GONE);
@ -2106,7 +2110,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} }
private void setGroupShareProfileReminder(@NonNull Recipient recipient) { 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().setRecipient(recipient);
groupShareProfileView.get().setVisibility(View.VISIBLE); groupShareProfileView.get().setVisibility(View.VISIBLE);
} else if (groupShareProfileView.resolved()) { } 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 // Loki - Send a friend request if we're not yet friends with the user in question
LokiThreadFriendRequestStatus friendRequestStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadId); 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) Permissions.with(this)
.request(Manifest.permission.SEND_SMS, Manifest.permission.READ_SMS) .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 // Loki - Send a friend request if we're not yet friends with the user in question
LokiThreadFriendRequestStatus friendRequestStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadId); 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) Permissions.with(this)
.request(Manifest.permission.SEND_SMS) .request(Manifest.permission.SEND_SMS)

View File

@ -52,17 +52,9 @@ public class Address implements Parcelable, Comparable<Address> {
private final String address; 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) { private Address(@NonNull String address) {
this(address, false);
}
private Address(@NonNull String address, Boolean isPublicChat) {
if (address == null) throw new AssertionError(address); if (address == null) throw new AssertionError(address);
this.address = address.toLowerCase(); this.address = address.toLowerCase();
this.isPublicChat = isPublicChat;
} }
public Address(Parcel in) { public Address(Parcel in) {
@ -77,10 +69,6 @@ public class Address implements Parcelable, Comparable<Address> {
return Address.fromSerialized(external); return Address.fromSerialized(external);
} }
public static @NonNull Address fromPublicChatGroupID(@NonNull String serialized) {
return new Address(serialized, true);
}
public static @NonNull List<Address> fromSerializedList(@NonNull String serialized, char delimiter) { public static @NonNull List<Address> fromSerializedList(@NonNull String serialized, char delimiter) {
String[] escapedAddresses = DelimiterUtil.split(serialized, delimiter); String[] escapedAddresses = DelimiterUtil.split(serialized, delimiter);
List<Address> addresses = new LinkedList<>(); List<Address> addresses = new LinkedList<>();
@ -121,13 +109,15 @@ public class Address implements Parcelable, Comparable<Address> {
} }
} }
public boolean isGroup() { public boolean isGroup() { return GroupUtil.isEncodedGroup(address); }
return GroupUtil.isEncodedGroup(address);
}
public boolean isMmsGroup() { public boolean isSignalGroup() { return !isPublicChat() && !isRSSFeed(); }
return GroupUtil.isMmsGroup(address);
} public boolean isPublicChat() { return GroupUtil.isPublicChat(address); }
public boolean isRSSFeed() { return GroupUtil.isRssFeed(address); }
public boolean isMmsGroup() { return GroupUtil.isMmsGroup(address); }
public boolean isEmail() { public boolean isEmail() {
return NumberUtil.isValidEmail(address); return NumberUtil.isValidEmail(address);
@ -143,7 +133,7 @@ public class Address implements Parcelable, Comparable<Address> {
} }
public @NonNull String toPhoneString() { public @NonNull String toPhoneString() {
if (!isPhone() && !isPublicChat) { if (!isPhone() && !isPublicChat()) {
if (isEmail()) throw new AssertionError("Not e164, is email"); if (isEmail()) throw new AssertionError("Not e164, is email");
if (isGroup()) throw new AssertionError("Not e164, is group"); if (isGroup()) throw new AssertionError("Not e164, is group");
throw new AssertionError("Not e164, unknown"); throw new AssertionError("Not e164, unknown");

View File

@ -24,7 +24,6 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPoin
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
@ -52,6 +51,7 @@ public class GroupDatabase extends Database {
// Loki // Loki
private static final String AVATAR_URL = "avatar_url"; private static final String AVATAR_URL = "avatar_url";
private static final String ADMINS = "admins";
public static final String CREATE_TABLE = public static final String CREATE_TABLE =
"CREATE TABLE " + TABLE_NAME + "CREATE TABLE " + TABLE_NAME +
@ -68,6 +68,7 @@ public class GroupDatabase extends Database {
ACTIVE + " INTEGER DEFAULT 1, " + ACTIVE + " INTEGER DEFAULT 1, " +
AVATAR_DIGEST + " BLOB, " + AVATAR_DIGEST + " BLOB, " +
AVATAR_URL + " TEXT, " + AVATAR_URL + " TEXT, " +
ADMINS + " TEXT, " +
MMS + " INTEGER DEFAULT 0);"; MMS + " INTEGER DEFAULT 0);";
public static final String[] CREATE_INDEXS = { public static final String[] CREATE_INDEXS = {
@ -76,7 +77,7 @@ public class GroupDatabase extends Database {
private static final String[] GROUP_PROJECTION = { private static final String[] GROUP_PROJECTION = {
GROUP_ID, TITLE, MEMBERS, AVATAR, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST, 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<String> TYPED_GROUP_PROJECTION = Stream.of(GROUP_PROJECTION).map(columnName -> TABLE_NAME + "." + columnName).toList(); static final List<String> 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); return new Reader(cursor);
} }
public String getOrCreateGroupForMembers(List<Address> members, boolean mms) { public String getOrCreateGroupForMembers(List<Address> members, boolean mms, List<Address> admins) {
Collections.sort(members); Collections.sort(members);
Collections.sort(admins);
Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[] {GROUP_ID}, Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[] {GROUP_ID},
MEMBERS + " = ? AND " + MMS + " = ?", MEMBERS + " = ? AND " + MMS + " = ?",
@ -128,7 +130,7 @@ public class GroupDatabase extends Database {
return cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID)); return cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID));
} else { } else {
String groupId = GroupUtil.getEncodedId(allocateGroupId(), mms); String groupId = GroupUtil.getEncodedId(allocateGroupId(), mms);
create(groupId, null, members, null, null); create(groupId, null, members, null, null, admins);
return groupId; return groupId;
} }
} finally { } finally {
@ -150,14 +152,33 @@ public class GroupDatabase extends Database {
if (!includeSelf && Util.isOwnNumber(context, member)) if (!includeSelf && Util.isOwnNumber(context, member))
continue; continue;
if (member.isPhone()) {
recipients.add(Recipient.from(context, member, false)); recipients.add(Recipient.from(context, member, false));
} }
}
return recipients; 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<Address> members, public void create(@NonNull String groupId, @Nullable String title, @NonNull List<Address> members,
@Nullable SignalServiceAttachmentPointer avatar, @Nullable String relay) @Nullable SignalServiceAttachmentPointer avatar, @Nullable String relay, @Nullable List<Address> admins)
{ {
Collections.sort(members); Collections.sort(members);
@ -179,6 +200,10 @@ public class GroupDatabase extends Database {
contentValues.put(ACTIVE, 1); contentValues.put(ACTIVE, 1);
contentValues.put(MMS, GroupUtil.isMmsGroup(groupId)); contentValues.put(MMS, GroupUtil.isMmsGroup(groupId));
if (admins != null) {
contentValues.put(ADMINS, Address.toSerializedList(admins, ','));
}
databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues); databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues);
Recipient.applyCached(Address.fromSerialized(groupId), recipient -> { Recipient.applyCached(Address.fromSerialized(groupId), recipient -> {
@ -260,6 +285,17 @@ public class GroupDatabase extends Database {
}); });
} }
public void updateAdmins(String groupId, List<Address> 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) { public void remove(String groupId, Address source) {
List<Address> currentMembers = getCurrentMembers(groupId); List<Address> currentMembers = getCurrentMembers(groupId);
currentMembers.remove(source); currentMembers.remove(source);
@ -351,7 +387,8 @@ public class GroupDatabase extends Database {
cursor.getInt(cursor.getColumnIndexOrThrow(ACTIVE)) == 1, cursor.getInt(cursor.getColumnIndexOrThrow(ACTIVE)) == 1,
cursor.getBlob(cursor.getColumnIndexOrThrow(AVATAR_DIGEST)), cursor.getBlob(cursor.getColumnIndexOrThrow(AVATAR_DIGEST)),
cursor.getInt(cursor.getColumnIndexOrThrow(MMS)) == 1, cursor.getInt(cursor.getColumnIndexOrThrow(MMS)) == 1,
cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_URL))); cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_URL)),
cursor.getString(cursor.getColumnIndexOrThrow(ADMINS)));
} }
@Override @Override
@ -375,10 +412,11 @@ public class GroupDatabase extends Database {
private final boolean active; private final boolean active;
private final boolean mms; private final boolean mms;
private final String url; private final String url;
private final List<Address> admins;
public GroupRecord(String id, String title, String members, byte[] avatar, public GroupRecord(String id, String title, String members, byte[] avatar,
long avatarId, byte[] avatarKey, String avatarContentType, 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.id = id;
this.title = title; this.title = title;
@ -394,6 +432,9 @@ public class GroupDatabase extends Database {
if (!TextUtils.isEmpty(members)) this.members = Address.fromSerializedList(members, ','); if (!TextUtils.isEmpty(members)) this.members = Address.fromSerializedList(members, ',');
else this.members = new LinkedList<>(); else this.members = new LinkedList<>();
if (!TextUtils.isEmpty(admins)) this.admins = Address.fromSerializedList(admins, ',');
else this.admins = new LinkedList<>();
} }
public byte[] getId() { public byte[] getId() {
@ -448,6 +489,14 @@ public class GroupDatabase extends Database {
return mms; 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 String getUrl() { return url; }
public List<Address> getAdmins() { return admins; }
} }
} }

View File

@ -220,7 +220,7 @@ public class SmsMigrator {
memberAddresses.add(recipient.getAddress()); 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); Recipient ourGroupRecipient = Recipient.from(context, Address.fromSerialized(ourGroupId), true);
long ourThreadId = threadDatabase.getThreadIdFor(ourGroupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION); long ourThreadId = threadDatabase.getThreadIdFor(ourGroupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION);

View File

@ -35,14 +35,17 @@ import org.thoughtcrime.securesms.database.StickerDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob; import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
import org.thoughtcrime.securesms.logging.Log; 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.LokiAPIDatabase;
import org.thoughtcrime.securesms.loki.redesign.messaging.LokiPreKeyBundleDatabase; import org.thoughtcrime.securesms.loki.redesign.messaging.LokiPreKeyBundleDatabase;
import org.thoughtcrime.securesms.loki.redesign.messaging.LokiPreKeyRecordDatabase; import org.thoughtcrime.securesms.loki.redesign.messaging.LokiPreKeyRecordDatabase;
import org.thoughtcrime.securesms.loki.redesign.messaging.LokiUserDatabase; import org.thoughtcrime.securesms.loki.redesign.messaging.LokiUserDatabase;
import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.loki.api.LokiPublicChat;
import java.io.File; import java.io.File;
@ -76,8 +79,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int lokiV3 = 24; private static final int lokiV3 = 24;
private static final int lokiV4 = 25; private static final int lokiV4 = 25;
private static final int lokiV5 = 26; 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 static final String DATABASE_NAME = "signal.db";
private final Context context; private final Context context;
@ -529,6 +533,43 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(LokiAPIDatabase.getCreateUserCountTableCommand()); 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(); db.setTransactionSuccessful();
} finally { } finally {
db.endTransaction(); db.endTransaction();

View File

@ -36,8 +36,13 @@ import java.util.Set;
public class GroupManager { public class GroupManager {
public static long getThreadId(String id, @NonNull Context context) { public static long getPublicChatThreadId(String id, @NonNull Context context) {
final String groupId = GroupUtil.getEncodedId(id.getBytes(), false); 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); return getThreadIdFromGroupId(groupId, context);
} }
@ -50,11 +55,12 @@ public class GroupManager {
@NonNull Set<Recipient> members, @NonNull Set<Recipient> members,
@Nullable Bitmap avatar, @Nullable Bitmap avatar,
@Nullable String name, @Nullable String name,
boolean mms) boolean mms,
@NonNull Set<Recipient> admins)
{ {
GroupDatabase database = DatabaseFactory.getGroupDatabase(context); GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
String id = GroupUtil.getEncodedId(database.allocateGroupId(), mms); 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, public static @NonNull GroupActionResult createGroup(@NonNull String id,
@ -62,56 +68,90 @@ public class GroupManager {
@NonNull Set<Recipient> members, @NonNull Set<Recipient> members,
@Nullable Bitmap avatar, @Nullable Bitmap avatar,
@Nullable String name, @Nullable String name,
boolean mms) boolean mms,
@NonNull Set<Recipient> admins)
{ {
final byte[] avatarBytes = BitmapUtil.toByteArray(avatar); final byte[] avatarBytes = BitmapUtil.toByteArray(avatar);
final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
final String groupId = GroupUtil.getEncodedId(id.getBytes(), mms); final String groupId = GroupUtil.getEncodedId(id.getBytes(), mms);
final Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(groupId), false); final Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(groupId), false);
final Set<Address> memberAddresses = getMemberAddresses(members); final Set<Address> memberAddresses = getMemberAddresses(members);
final Set<Address> adminAddresses = getMemberAddresses(admins);
memberAddresses.add(Address.fromSerialized(TextSecurePreferences.getLocalNumber(context))); String masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
groupDatabase.create(groupId, name, new LinkedList<>(memberAddresses), null, null); 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) { if (!mms) {
groupDatabase.updateAvatar(groupId, avatarBytes); groupDatabase.updateAvatar(groupId, avatarBytes);
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient, true); DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient, true);
} return sendGroupUpdate(context, groupId, memberAddresses, name, avatarBytes, adminAddresses);
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);
} else { } else {
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION); long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION);
return new GroupActionResult(groupRecipient, threadId); 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<Address> 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, public static GroupActionResult updateGroup(@NonNull Context context,
@NonNull String groupId, @NonNull String groupId,
@NonNull Set<Recipient> members, @NonNull Set<Recipient> members,
@Nullable Bitmap avatar, @Nullable Bitmap avatar,
@Nullable String name) @Nullable String name,
@NonNull Set<Recipient> admins)
throws InvalidNumberException throws InvalidNumberException
{ {
final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
final Set<Address> memberAddresses = getMemberAddresses(members); final Set<Address> memberAddresses = getMemberAddresses(members);
final Set<Address> adminAddresses = getMemberAddresses(admins);
final byte[] avatarBytes = BitmapUtil.toByteArray(avatar); final byte[] avatarBytes = BitmapUtil.toByteArray(avatar);
memberAddresses.add(Address.fromSerialized(TextSecurePreferences.getLocalNumber(context))); memberAddresses.add(Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)));
groupDatabase.updateMembers(groupId, new LinkedList<>(memberAddresses)); groupDatabase.updateMembers(groupId, new LinkedList<>(memberAddresses));
groupDatabase.updateAdmins(groupId, new LinkedList<>(adminAddresses));
groupDatabase.updateTitle(groupId, name); groupDatabase.updateTitle(groupId, name);
groupDatabase.updateAvatar(groupId, avatarBytes); groupDatabase.updateAvatar(groupId, avatarBytes);
if (!GroupUtil.isMmsGroup(groupId)) { if (!GroupUtil.isMmsGroup(groupId)) {
return sendGroupUpdate(context, groupId, memberAddresses, name, avatarBytes); return sendGroupUpdate(context, groupId, memberAddresses, name, avatarBytes, adminAddresses);
} else { } else {
Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(groupId), true); Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(groupId), true);
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient); long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient);
@ -123,7 +163,8 @@ public class GroupManager {
@NonNull String groupId, @NonNull String groupId,
@NonNull Set<Address> members, @NonNull Set<Address> members,
@Nullable String groupName, @Nullable String groupName,
@Nullable byte[] avatar) @Nullable byte[] avatar,
@NonNull Set<Address> admins)
{ {
try { try {
Attachment avatarAttachment = null; Attachment avatarAttachment = null;
@ -131,15 +172,20 @@ public class GroupManager {
Recipient groupRecipient = Recipient.from(context, groupAddress, false); Recipient groupRecipient = Recipient.from(context, groupAddress, false);
List<String> numbers = new LinkedList<>(); List<String> numbers = new LinkedList<>();
for (Address member : members) { for (Address member : members) {
numbers.add(member.serialize()); numbers.add(member.serialize());
} }
List<String> adminNumbers = new LinkedList<>();
for (Address admin : admins) {
adminNumbers.add(admin.serialize());
}
GroupContext.Builder groupContextBuilder = GroupContext.newBuilder() GroupContext.Builder groupContextBuilder = GroupContext.newBuilder()
.setId(ByteString.copyFrom(GroupUtil.getDecodedId(groupId))) .setId(ByteString.copyFrom(GroupUtil.getDecodedId(groupId)))
.setType(GroupContext.Type.UPDATE) .setType(GroupContext.Type.UPDATE)
.addAllMembers(numbers); .addAllMembers(numbers)
.addAllAdmins(adminNumbers);
if (groupName != null) groupContextBuilder.setName(groupName); if (groupName != null) groupContextBuilder.setName(groupName);
GroupContext groupContext = groupContextBuilder.build(); GroupContext groupContext = groupContextBuilder.build();

View File

@ -8,6 +8,7 @@ import android.support.annotation.Nullable;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase; 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.recipients.Recipient;
import org.thoughtcrime.securesms.sms.IncomingGroupMessage; import org.thoughtcrime.securesms.sms.IncomingGroupMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage; import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.GroupUtil; 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.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup.Type; 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.Collections;
import java.util.HashSet; import java.util.HashSet;
@ -38,6 +45,8 @@ import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import kotlin.Unit;
import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord; 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.AttachmentPointer;
import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext; import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
@ -58,7 +67,7 @@ public class GroupMessageProcessor {
GroupDatabase database = DatabaseFactory.getGroupDatabase(context); GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
SignalServiceGroup group = message.getGroupInfo().get(); SignalServiceGroup group = message.getGroupInfo().get();
String id = GroupUtil.getEncodedId(group.getGroupId(), false); String id = GroupUtil.getEncodedId(group);
Optional<GroupRecord> record = database.getGroup(id); Optional<GroupRecord> record = database.getGroup(id);
if (record.isPresent() && group.getType() == Type.UPDATE) { if (record.isPresent() && group.getType() == Type.UPDATE) {
@ -81,12 +90,13 @@ public class GroupMessageProcessor {
boolean outgoing) boolean outgoing)
{ {
GroupDatabase database = DatabaseFactory.getGroupDatabase(context); GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
String id = GroupUtil.getEncodedId(group.getGroupId(), false); String id = GroupUtil.getEncodedId(group);
GroupContext.Builder builder = createGroupContext(group); GroupContext.Builder builder = createGroupContext(group);
builder.setType(GroupContext.Type.UPDATE); builder.setType(GroupContext.Type.UPDATE);
SignalServiceAttachment avatar = group.getAvatar().orNull(); SignalServiceAttachment avatar = group.getAvatar().orNull();
List<Address> members = group.getMembers().isPresent() ? new LinkedList<Address>() : null; List<Address> members = group.getMembers().isPresent() ? new LinkedList<Address>() : null;
List<Address> admins = group.getAdmins().isPresent() ? new LinkedList<>() : null;
if (group.getMembers().isPresent()) { if (group.getMembers().isPresent()) {
for (String member : group.getMembers().get()) { 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, 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); return storeMessage(context, content, group, builder.build(), outgoing);
} }
@ -108,7 +135,16 @@ public class GroupMessageProcessor {
{ {
GroupDatabase database = DatabaseFactory.getGroupDatabase(context); 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<Address> recordMembers = new HashSet<>(groupRecord.getMembers()); Set<Address> recordMembers = new HashSet<>(groupRecord.getMembers());
Set<Address> messageMembers = new HashSet<>(); Set<Address> messageMembers = new HashSet<>();
@ -141,7 +177,9 @@ public class GroupMessageProcessor {
} }
if (missingMembers.size() > 0) { 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()) { if (group.getName().isPresent() || group.getAvatar().isPresent()) {
@ -155,6 +193,10 @@ public class GroupMessageProcessor {
if (!groupRecord.isActive()) database.setActive(id, true); if (!groupRecord.isActive()) database.setActive(id, true);
if (group.getMembers().isPresent()) {
establishSessionsWithMembersIfNeeded(context, group.getMembers().get());
}
return storeMessage(context, content, group, builder.build(), outgoing); return storeMessage(context, content, group, builder.build(), outgoing);
} }
@ -163,7 +205,10 @@ public class GroupMessageProcessor {
@NonNull SignalServiceGroup group, @NonNull SignalServiceGroup group,
@NonNull GroupRecord record) @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) ApplicationContext.getInstance(context)
.getJobManager() .getJobManager()
.add(new PushGroupUpdateJob(content.getSender(), group.getGroupId())); .add(new PushGroupUpdateJob(content.getSender(), group.getGroupId()));
@ -179,14 +224,15 @@ public class GroupMessageProcessor {
boolean outgoing) boolean outgoing)
{ {
GroupDatabase database = DatabaseFactory.getGroupDatabase(context); GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
String id = GroupUtil.getEncodedId(group.getGroupId(), false); String id = GroupUtil.getEncodedId(group);
List<Address> members = record.getMembers(); List<Address> members = record.getMembers();
GroupContext.Builder builder = createGroupContext(group); GroupContext.Builder builder = createGroupContext(group);
builder.setType(GroupContext.Type.QUIT); builder.setType(GroupContext.Type.QUIT);
if (members.contains(Address.fromExternal(context, content.getSender()))) { String hexEncodedPublicKey = getMasterHexEncodedPublicKey(context, content.getSender());
database.remove(id, Address.fromExternal(context, content.getSender())); if (members.contains(Address.fromExternal(context, hexEncodedPublicKey))) {
database.remove(id, Address.fromExternal(context, hexEncodedPublicKey));
if (outgoing) database.setActive(id, false); if (outgoing) database.setActive(id, false);
return storeMessage(context, content, group, builder.build(), outgoing); return storeMessage(context, content, group, builder.build(), outgoing);
@ -204,14 +250,14 @@ public class GroupMessageProcessor {
{ {
if (group.getAvatar().isPresent()) { if (group.getAvatar().isPresent()) {
ApplicationContext.getInstance(context).getJobManager() ApplicationContext.getInstance(context).getJobManager()
.add(new AvatarDownloadJob(group.getGroupId())); .add(new AvatarDownloadJob(GroupUtil.getEncodedId(group)));
} }
try { try {
if (outgoing) { if (outgoing) {
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context); MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
Address addres = Address.fromExternal(context, GroupUtil.getEncodedId(group.getGroupId(), false)); Address address = Address.fromExternal(context, GroupUtil.getEncodedId(group));
Recipient recipient = Recipient.from(context, addres, false); Recipient recipient = Recipient.from(context, address, false);
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(recipient, storage, null, content.getTimestamp(), 0, null, Collections.emptyList(), Collections.emptyList()); OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(recipient, storage, null, content.getTimestamp(), 0, null, Collections.emptyList(), Collections.emptyList());
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient); long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
long messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null); long messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null);
@ -260,7 +306,38 @@ public class GroupMessageProcessor {
builder.addAllMembers(group.getMembers().get()); builder.addAllMembers(group.getMembers().get());
} }
if (group.getAdmins().isPresent()) {
builder.addAllAdmins(group.getAdmins().get());
}
return builder; 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<String> 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;
});
}
}
} }

View File

@ -40,9 +40,9 @@ public class AvatarDownloadJob extends BaseJob implements InjectableType {
@Inject SignalServiceMessageReceiver receiver; @Inject SignalServiceMessageReceiver receiver;
private byte[] groupId; private String groupId;
public AvatarDownloadJob(@NonNull byte[] groupId) { public AvatarDownloadJob(@NonNull String groupId) {
this(new Job.Parameters.Builder() this(new Job.Parameters.Builder()
.addConstraint(NetworkConstraint.KEY) .addConstraint(NetworkConstraint.KEY)
.setMaxAttempts(10) .setMaxAttempts(10)
@ -50,14 +50,14 @@ public class AvatarDownloadJob extends BaseJob implements InjectableType {
groupId); groupId);
} }
private AvatarDownloadJob(@NonNull Job.Parameters parameters, @NonNull byte[] groupId) { private AvatarDownloadJob(@NonNull Job.Parameters parameters, @NonNull String groupId) {
super(parameters); super(parameters);
this.groupId = groupId; this.groupId = groupId;
} }
@Override @Override
public @NonNull Data serialize() { 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 @Override
@ -67,9 +67,8 @@ public class AvatarDownloadJob extends BaseJob implements InjectableType {
@Override @Override
public void onRun() throws IOException { public void onRun() throws IOException {
String encodeId = GroupUtil.getEncodedId(groupId, false);
GroupDatabase database = DatabaseFactory.getGroupDatabase(context); GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
Optional<GroupRecord> record = database.getGroup(encodeId); Optional<GroupRecord> record = database.getGroup(groupId);
File attachment = null; File attachment = null;
try { try {
@ -97,7 +96,7 @@ public class AvatarDownloadJob extends BaseJob implements InjectableType {
InputStream inputStream = receiver.retrieveAttachment(pointer, attachment, MAX_AVATAR_SIZE); InputStream inputStream = receiver.retrieveAttachment(pointer, attachment, MAX_AVATAR_SIZE);
Bitmap avatar = BitmapUtil.createScaledBitmap(context, new AttachmentModel(attachment, key, 0, digest), 500, 500); Bitmap avatar = BitmapUtil.createScaledBitmap(context, new AttachmentModel(attachment, key, 0, digest), 500, 500);
database.updateAvatar(encodeId, avatar); database.updateAvatar(groupId, avatar);
inputStream.close(); inputStream.close();
} }
} catch (BitmapDecodingException | NonSuccessfulResponseCodeException | InvalidMessageException e) { } catch (BitmapDecodingException | NonSuccessfulResponseCodeException | InvalidMessageException e) {
@ -120,11 +119,7 @@ public class AvatarDownloadJob extends BaseJob implements InjectableType {
public static final class Factory implements Job.Factory<AvatarDownloadJob> { public static final class Factory implements Job.Factory<AvatarDownloadJob> {
@Override @Override
public @NonNull AvatarDownloadJob create(@NonNull Parameters parameters, @NonNull Data data) { public @NonNull AvatarDownloadJob create(@NonNull Parameters parameters, @NonNull Data data) {
try { return new AvatarDownloadJob(parameters, data.getString(KEY_GROUP_ID));
return new AvatarDownloadJob(parameters, GroupUtil.getDecodedId(data.getString(KEY_GROUP_ID)));
} catch (IOException e) {
throw new AssertionError(e);
}
} }
} }
} }

View File

@ -9,9 +9,7 @@ import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.DirectoryHelper;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import java.io.IOException; import java.io.IOException;
@ -67,11 +65,11 @@ public class DirectoryRefreshJob extends BaseJob {
public void onRun() throws IOException { public void onRun() throws IOException {
Log.i(TAG, "DirectoryRefreshJob.onRun()"); Log.i(TAG, "DirectoryRefreshJob.onRun()");
if (recipient == null) { // if (recipient == null) {
DirectoryHelper.refreshDirectory(context, notifyOfNewUsers); // DirectoryHelper.refreshDirectory(context, notifyOfNewUsers);
} else { // } else {
DirectoryHelper.refreshDirectoryFor(context, recipient); // DirectoryHelper.refreshDirectoryFor(context, recipient);
} // }
} }
@Override @Override

View File

@ -244,7 +244,7 @@ public class MmsDownloadJob extends BaseJob {
} }
if (members.size() > 2) { 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); IncomingMediaMessage message = new IncomingMediaMessage(from, group, body, retrieved.getDate() * 1000L, attachments, subscriptionId, 0, false, false);

View File

@ -85,7 +85,7 @@ public class MultiDeviceGroupUpdateJob extends BaseJob implements InjectableType
reader = DatabaseFactory.getGroupDatabase(context).getGroups(); reader = DatabaseFactory.getGroupDatabase(context).getGroups();
while ((record = reader.getNext()) != null) { while ((record = reader.getNext()) != null) {
if (!record.isMms()) { if (!record.isMms() && !record.isPublicChat() && !record.isRSSFeed()) {
List<String> members = new LinkedList<>(); List<String> members = new LinkedList<>();
for (Address member : record.getMembers()) { for (Address member : record.getMembers()) {

View File

@ -291,6 +291,9 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
// Loki - Handle friend request acceptance if needed // Loki - Handle friend request acceptance if needed
acceptFriendRequestIfNeeded(content); acceptFriendRequestIfNeeded(content);
// Loki - Session requests
handleSessionRequestIfNeeded(content);
// Loki - Store pre key bundle // Loki - Store pre key bundle
// We shouldn't store it if it's a pairing message // We shouldn't store it if it's a pairing message
if (!content.getPairingAuthorisation().isPresent()) { if (!content.getPairingAuthorisation().isPresent()) {
@ -335,7 +338,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
} }
} else { } else {
// Loki - Don't process session restore message any further // 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); if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId);
else if (message.isGroupUpdate()) handleGroupMessage(content, message, smsMessageId); else if (message.isGroupUpdate()) handleGroupMessage(content, message, smsMessageId);
else if (message.isExpirationUpdate()) else if (message.isExpirationUpdate())
@ -345,7 +349,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
else if (message.getBody().isPresent()) else if (message.getBody().isPresent())
handleTextMessage(content, message, smsMessageId, Optional.absent()); 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()); handleUnknownGroupMessage(content, message.getGroupInfo().get());
} }
@ -601,10 +605,12 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
private void handleUnknownGroupMessage(@NonNull SignalServiceContent content, private void handleUnknownGroupMessage(@NonNull SignalServiceContent content,
@NonNull SignalServiceGroup group) @NonNull SignalServiceGroup group)
{ {
if (group.getGroupType() == SignalServiceGroup.GroupType.SIGNAL) {
ApplicationContext.getInstance(context) ApplicationContext.getInstance(context)
.getJobManager() .getJobManager()
.add(new RequestGroupInfoJob(content.getSender(), group.getGroupId())); .add(new RequestGroupInfoJob(content.getSender(), group.getGroupId()));
} }
}
private void handleExpirationUpdate(@NonNull SignalServiceContent content, private void handleExpirationUpdate(@NonNull SignalServiceContent content,
@NonNull SignalServiceDataMessage message, @NonNull SignalServiceDataMessage message,
@ -690,7 +696,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
Log.d("Loki", "Sent friend request to " + pubKey); Log.d("Loki", "Sent friend request to " + pubKey);
} else if (status == LokiThreadFriendRequestStatus.REQUEST_RECEIVED) { } else if (status == LokiThreadFriendRequestStatus.REQUEST_RECEIVED) {
// Accept the incoming friend request // Accept the incoming friend request
becomeFriendsWithContact(pubKey, false); becomeFriendsWithContact(pubKey, false, false);
// Send them an accept message back // Send them an accept message back
MessageSender.sendBackgroundMessage(context, pubKey); MessageSender.sendBackgroundMessage(context, pubKey);
Log.d("Loki", "Became friends with " + deviceContact.getNumber()); Log.d("Loki", "Became friends with " + deviceContact.getNumber());
@ -728,7 +734,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
threadId = handleSynchronizeSentTextMessage(message); 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()); handleUnknownGroupMessage(content, message.getMessage().getGroupInfo().get());
} }
@ -738,7 +744,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
Recipient recipient = null; Recipient recipient = null;
if (message.getDestination().isPresent()) recipient = Recipient.from(context, Address.fromSerialized(message.getDestination().get()), false); 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()) { if (recipient != null && !recipient.isSystemContact() && !recipient.isProfileSharing()) {
@ -845,8 +851,16 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
Optional<List<LinkPreview>> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().or("")); Optional<List<LinkPreview>> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().or(""));
Optional<Attachment> sticker = getStickerAttachment(message.getSticker()); Optional<Attachment> sticker = getStickerAttachment(message.getSticker());
// If message is from group then we need to map it to the correct sender Address sender = primaryDeviceRecipient.getAddress();
Address sender = message.isGroupUpdate() ? Address.fromSerialized(content.getSender()) : 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, IncomingMediaMessage mediaMessage = new IncomingMediaMessage(sender, message.getTimestamp(), -1,
message.getExpiresInSeconds() * 1000L, false, content.isNeedsReceipt(), message.getBody(), message.getGroupInfo(), message.getAttachments(), message.getExpiresInSeconds() * 1000L, false, content.isNeedsReceipt(), message.getBody(), message.getGroupInfo(), message.getAttachments(),
quote, sharedContacts, linkPreviews, sticker); quote, sharedContacts, linkPreviews, sticker);
@ -1030,8 +1044,16 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
} else { } else {
notifyTypingStoppedFromIncomingMessage(primaryDeviceRecipient, content.getSender(), content.getSenderDevice()); notifyTypingStoppedFromIncomingMessage(primaryDeviceRecipient, content.getSender(), content.getSenderDevice());
// If message is from group then we need to map it to the correct sender Address sender = primaryDeviceRecipient.getAddress();
Address sender = message.isGroupUpdate() ? Address.fromSerialized(content.getSender()) : 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, IncomingTextMessage _textMessage = new IncomingTextMessage(sender,
content.getSenderDevice(), content.getSenderDevice(),
message.getTimestamp(), body, message.getTimestamp(), body,
@ -1217,10 +1239,29 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
private void acceptFriendRequestIfNeeded(@NonNull SignalServiceContent content) { 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 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; } 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); LokiThreadDatabase lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context);
Recipient contactID = Recipient.from(context, Address.fromSerialized(pubKey), false); Recipient contactID = Recipient.from(context, Address.fromSerialized(pubKey), false);
if (contactID.isGroupRecipient()) return; if (contactID.isGroupRecipient()) return;
@ -1228,6 +1269,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(contactID); long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(contactID);
LokiThreadFriendRequestStatus threadFriendRequestStatus = lokiThreadDatabase.getFriendRequestStatus(threadID); LokiThreadFriendRequestStatus threadFriendRequestStatus = lokiThreadDatabase.getFriendRequestStatus(threadID);
if (threadFriendRequestStatus == LokiThreadFriendRequestStatus.FRIENDS) { return; } 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, // 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. // it must be a friend request accepted message. Declining a friend request doesn't send a message.
lokiThreadDatabase.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.FRIENDS); 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) { 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 // This handles the case where another user sends us a regular message without authorisation
Promise<Boolean, Exception> promise = PromiseUtil.timeout(MultiDeviceUtilities.shouldAutomaticallyBecomeFriendsWithDevice(content.getSender(), context), 8000); Promise<Boolean, Exception> promise = PromiseUtil.timeout(MultiDeviceUtilities.shouldAutomaticallyBecomeFriendsWithDevice(content.getSender(), context), 8000);
boolean shouldBecomeFriends = PromiseUtil.get(promise, false); boolean shouldBecomeFriends = PromiseUtil.get(promise, false);
if (shouldBecomeFriends) { if (shouldBecomeFriends) {
// Become friends AND update the message they sent // Become friends AND update the message they sent
becomeFriendsWithContact(content.getSender(), true); becomeFriendsWithContact(content.getSender(), true, true);
// Send them an accept message back // Send them an accept message back
MessageSender.sendBackgroundMessage(context, content.getSender()); MessageSender.sendBackgroundMessage(context, content.getSender());
} else { } else {
@ -1561,10 +1607,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
long threadId; long threadId;
if (typingMessage.getGroupId().isPresent()) { 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)); Address groupAddress = Address.fromSerialized(GroupUtil.getEncodedId(typingMessage.getGroupId().get(), false));
Recipient groupRecipient = Recipient.from(context, groupAddress, false); Recipient groupRecipient = Recipient.from(context, groupAddress, false);
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient); threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(groupRecipient);
} else { } else {
// See if we need to redirect the message // See if we need to redirect the message
author = getPrimaryDeviceRecipient(content.getSender()); author = getPrimaryDeviceRecipient(content.getSender());
@ -1712,15 +1759,15 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
} }
private Recipient getSyncMessageDestination(SentTranscriptMessage message) { private Recipient getSyncMessageDestination(SentTranscriptMessage message) {
if (message.getMessage().getGroupInfo().isPresent()) { if (message.getMessage().isGroupMessage()) {
return Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(message.getMessage().getGroupInfo().get().getGroupId(), false)), false); return Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(message.getMessage().getGroupInfo().get())), false);
} else { } else {
return Recipient.from(context, Address.fromSerialized(message.getDestination().get()), false); return Recipient.from(context, Address.fromSerialized(message.getDestination().get()), false);
} }
} }
private Recipient getSyncMessagePrimaryDestination(SentTranscriptMessage message) { private Recipient getSyncMessagePrimaryDestination(SentTranscriptMessage message) {
if (message.getMessage().getGroupInfo().isPresent()) { if (message.getMessage().isGroupMessage()) {
return getSyncMessageDestination(message); return getSyncMessageDestination(message);
} else { } else {
return getPrimaryDeviceRecipient(message.getDestination().get()); return getPrimaryDeviceRecipient(message.getDestination().get());
@ -1728,15 +1775,15 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
} }
private Recipient getMessageDestination(SignalServiceContent content, SignalServiceDataMessage message) { private Recipient getMessageDestination(SignalServiceContent content, SignalServiceDataMessage message) {
if (message.getGroupInfo().isPresent()) { if (message.isGroupMessage()) {
return Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId(), false)), false); return Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(message.getGroupInfo().get())), false);
} else { } else {
return Recipient.from(context, Address.fromSerialized(content.getSender()), false); return Recipient.from(context, Address.fromSerialized(content.getSender()), false);
} }
} }
private Recipient getMessagePrimaryDestination(SignalServiceContent content, SignalServiceDataMessage message) { private Recipient getMessagePrimaryDestination(SignalServiceContent content, SignalServiceDataMessage message) {
if (message.getGroupInfo().isPresent()) { if (message.isGroupMessage()) {
return getMessageDestination(content, message); return getMessageDestination(content, message);
} else { } else {
return getPrimaryDeviceRecipient(content.getSender()); return getPrimaryDeviceRecipient(content.getSender());
@ -1794,7 +1841,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
return true; return true;
} else if (conversation.isGroupRecipient()) { } else if (conversation.isGroupRecipient()) {
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
Optional<String> groupId = message.getGroupInfo().isPresent() ? Optional.of(GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId(), false)) Optional<String> groupId = message.getGroupInfo().isPresent() ? Optional.of(GroupUtil.getEncodedId(message.getGroupInfo().get()))
: Optional.absent(); : Optional.absent();
if (groupId.isPresent() && groupDatabase.isUnknownGroup(groupId.get())) { if (groupId.isPresent() && groupDatabase.isUnknownGroup(groupId.get())) {
@ -1830,8 +1877,12 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
return false; return false;
} }
private boolean isSessionRequest(SignalServiceContent content) {
return content.getDataMessage().isPresent() && content.getDataMessage().get().isSessionRequest();
}
private boolean isGroupChatMessage(SignalServiceContent content) { 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) { private void resetRecipientToPush(@NonNull Recipient recipient) {

View File

@ -12,8 +12,10 @@ import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupReceiptDatabase.GroupReceiptInfo;
import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; 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.JobManager;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException; import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.util.GroupUtil; 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.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; 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.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
import org.whispersystems.signalservice.loki.api.LokiPublicChat; 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.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Set; 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 if (!existingNetworkFailures.isEmpty()) target = Stream.of(existingNetworkFailures).map(NetworkFailure::getAddress).toList();
else target = getGroupMessageRecipients(message.getRecipient().getAddress().toGroupString(), messageId); else target = getGroupMessageRecipients(message.getRecipient().getAddress().toGroupString(), messageId);
List<SendMessageResult> results = deliver(message, target); String localNumber = TextSecurePreferences.getLocalNumber(context);
// Only send messages to the contacts we have sessions with
List<Address> 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<Address> others = Stream.of(target).filter(t -> !validTargets.contains(t)).toList();
for (Address device : others) {
MessageSender.sendBackgroundSessionRequest(context, device.toPhoneString());
}
List<SendMessageResult> results = deliver(message, validTargets);
List<NetworkFailure> networkFailures = Stream.of(results).filter(SendMessageResult::isNetworkFailure).map(result -> new NetworkFailure(Address.fromSerialized(result.getAddress().getNumber()))).toList(); List<NetworkFailure> networkFailures = Stream.of(results).filter(SendMessageResult::isNetworkFailure).map(result -> new NetworkFailure(Address.fromSerialized(result.getAddress().getNumber()))).toList();
List<IdentityKeyMismatch> identityMismatches = Stream.of(results).filter(result -> result.getIdentityFailure() != null).map(result -> new IdentityKeyMismatch(Address.fromSerialized(result.getAddress().getNumber()), result.getIdentityFailure().getIdentityKey())).toList(); List<IdentityKeyMismatch> identityMismatches = Stream.of(results).filter(result -> result.getIdentityFailure() != null).map(result -> new IdentityKeyMismatch(Address.fromSerialized(result.getAddress().getNumber()), result.getIdentityFailure().getIdentityKey())).toList();
Set<Address> successAddresses = Stream.of(results).filter(result -> result.getSuccess() != null).map(result -> Address.fromSerialized(result.getAddress().getNumber())).collect(Collectors.toSet()); Set<Address> 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 { throws IOException, UntrustedIdentityException, UndeliverableMessageException {
// rotateSenderCertificateIfNecessary(); // 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<SendMessageResult> results = new ArrayList<>();
for (Address destination : destinations) results.add(SendMessageResult.networkFailure(new SignalServiceAddress(destination.toPhoneString())));
return results;
}
String groupId = groupAddress.toGroupString();
Optional<byte[]> profileKey = getProfileKey(message.getRecipient()); Optional<byte[]> profileKey = getProfileKey(message.getRecipient());
Optional<Quote> quote = getQuoteFor(message); Optional<Quote> quote = getQuoteFor(message);
Optional<SignalServiceDataMessage.Sticker> sticker = getStickerFor(message); Optional<SignalServiceDataMessage.Sticker> sticker = getStickerFor(message);
@ -247,24 +285,28 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
.map(recipient -> UnidentifiedAccessUtil.getAccessFor(context, recipient)) .map(recipient -> UnidentifiedAccessUtil.getAccessFor(context, recipient))
.toList(); .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; OutgoingGroupMediaMessage groupMessage = (OutgoingGroupMediaMessage) message;
GroupContext groupContext = groupMessage.getGroupContext(); GroupContext groupContext = groupMessage.getGroupContext();
SignalServiceAttachment avatar = attachmentPointers.isEmpty() ? null : attachmentPointers.get(0); SignalServiceAttachment avatar = attachmentPointers.isEmpty() ? null : attachmentPointers.get(0);
SignalServiceGroup.Type type = groupMessage.isGroupQuit() ? SignalServiceGroup.Type.QUIT : SignalServiceGroup.Type.UPDATE; 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() SignalServiceDataMessage groupDataMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(message.getSentTimeMillis()) .withTimestamp(message.getSentTimeMillis())
.withExpiration(message.getRecipient().getExpireMessages()) .withExpiration(message.getRecipient().getExpireMessages())
.withBody(message.getBody())
.asGroupMessage(group) .asGroupMessage(group)
.build(); .build();
// Loki - Disable group updates for now return messageSender.sendMessage(messageId, addresses, unidentifiedAccess, groupDataMessage);
List<SendMessageResult> results = new ArrayList<>();
for (Address destination : destinations) results.add(SendMessageResult.success(new SignalServiceAddress(destination.toPhoneString()), false, false));
return results;
} else { } else {
SignalServiceGroup group = new SignalServiceGroup(GroupUtil.getDecodedId(groupId)); SignalServiceGroup group = new SignalServiceGroup(GroupUtil.getDecodedId(groupId), groupType);
SignalServiceDataMessage groupMessage = SignalServiceDataMessage.newBuilder() SignalServiceDataMessage groupMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(message.getSentTimeMillis()) .withTimestamp(message.getSentTimeMillis())
.asGroupMessage(group) .asGroupMessage(group)
@ -284,26 +326,56 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
} }
private @NonNull List<Address> getGroupMessageRecipients(String groupId, long messageId) { private @NonNull List<Address> getGroupMessageRecipients(String groupId, long messageId) {
ArrayList<Address> result = new ArrayList<>(); if (GroupUtil.isRssFeed(groupId)) { return new ArrayList<>(); }
// Loki - All group messages should be directed to their respective servers // Loki - All public chat group messages should be directed to their respective servers
if (GroupUtil.isPublicChat(groupId)) {
ArrayList<Address> result = new ArrayList<>();
long threadID = GroupManager.getThreadIdFromGroupId(groupId, context); long threadID = GroupManager.getThreadIdFromGroupId(groupId, context);
LokiPublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID); LokiPublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID);
if (publicChat != null) { if (publicChat != null) {
// We need to somehow maintain information that will allow the sender to map result.add(Address.fromSerialized(groupId));
// a recipient to the correct public chat thread, and so this might be a bit hacky }
result.add(Address.fromPublicChatGroupID(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<GroupReceiptInfo> destinations = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageId);
Set<Address> memberSet = new HashSet<>();
if (destinations.isEmpty()) {
List<Recipient> 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());
} }
return result; // 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
List<GroupReceiptInfo> destinations = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageId); for (Address member : memberSet) {
if (!destinations.isEmpty()) return Stream.of(destinations).map(GroupReceiptInfo::getAddress).toList(); if (!member.isPhone() || member.serialize().equalsIgnoreCase(localNumber)) { continue; }
try {
List<String> 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
}
}
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, false); return new LinkedList<>(memberSet);
return Stream.of(members).map(Recipient::getAddress).toList(); }
*/
} }
public static class Factory implements Job.Factory<PushGroupSendJob> { public static class Factory implements Job.Factory<PushGroupSendJob> {

View File

@ -97,15 +97,20 @@ public class PushGroupUpdateJob extends BaseJob implements InjectableType {
} }
List<String> members = new LinkedList<>(); List<String> members = new LinkedList<>();
for (Address member : record.get().getMembers()) { for (Address member : record.get().getMembers()) {
members.add(member.serialize()); members.add(member.serialize());
} }
List<String> admins = new LinkedList<>();
for (Address admin : record.get().getAdmins()) {
admins.add(admin.serialize());
}
SignalServiceGroup groupContext = SignalServiceGroup.newBuilder(Type.UPDATE) SignalServiceGroup groupContext = SignalServiceGroup.newBuilder(Type.UPDATE)
.withAvatar(avatar) .withAvatar(avatar)
.withId(groupId) .withId(groupId, SignalServiceGroup.GroupType.SIGNAL)
.withMembers(members) .withMembers(members)
.withAdmins(admins)
.withName(record.get().getTitle()) .withName(record.get().getTitle())
.build(); .build();

View File

@ -71,7 +71,7 @@ public class RequestGroupInfoJob extends BaseJob implements InjectableType {
@Override @Override
public void onRun() throws IOException, UntrustedIdentityException { public void onRun() throws IOException, UntrustedIdentityException {
SignalServiceGroup group = SignalServiceGroup.newBuilder(Type.REQUEST_INFO) SignalServiceGroup group = SignalServiceGroup.newBuilder(Type.REQUEST_INFO)
.withId(groupId) .withId(groupId, SignalServiceGroup.GroupType.SIGNAL)
.build(); .build();
SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder() SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()

View File

@ -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;
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage.Action; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage.Action;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus;
import org.whispersystems.signalservice.loki.utilities.PromiseUtil; import org.whispersystems.signalservice.loki.utilities.PromiseUtil;
import java.util.Collections; 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 // Loki - Don't send typing indicators in group chats or to ourselves
if (recipient.isGroupRecipient()) { return; } 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); boolean isOurDevice = PromiseUtil.get(MultiDeviceUtilities.isOneOfOurDevices(context, recipient.getAddress()), false);
if (!isOurDevice) { if (!isOurDevice) {
messageSender.sendTyping(0, addresses, unidentifiedAccess, typingMessage); messageSender.sendTyping(0, addresses, unidentifiedAccess, typingMessage);

View File

@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.loki
import android.content.Context import android.content.Context
import nl.komponents.kovenant.ui.successUi import nl.komponents.kovenant.ui.successUi
import org.thoughtcrime.securesms.database.DatabaseFactory 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.LokiMessageFriendRequestStatus
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus
@ -62,7 +63,7 @@ object FriendRequestHandler {
fun updateLastFriendRequestMessage(context: Context, threadId: Long, status: LokiMessageFriendRequestStatus) { fun updateLastFriendRequestMessage(context: Context, threadId: Long, status: LokiMessageFriendRequestStatus) {
if (threadId < 0) { return } if (threadId < 0) { return }
val recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId) ?: 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 messages = DatabaseFactory.getSmsDatabase(context).getAllMessageIDs(threadId)
val lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context) 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 // 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 // This ensures that we don't spam the UI with accept/decline messages
val recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId) ?: 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 }
isFriendsWithAnyLinkedDevice(context, recipient).successUi { isFriends -> isFriendsWithAnyLinkedDevice(context, recipient).successUi { isFriends ->
if (isFriends) { return@successUi } if (isFriends) { return@successUi }

View File

@ -14,7 +14,6 @@ import org.thoughtcrime.securesms.loki.redesign.messaging.LokiPublicChatPoller
import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.Util import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.loki.api.LokiPublicChat import org.whispersystems.signalservice.loki.api.LokiPublicChat
import java.util.*
class LokiPublicChatManager(private val context: Context) { class LokiPublicChatManager(private val context: Context) {
private var chats = mutableMapOf<Long, LokiPublicChat>() private var chats = mutableMapOf<Long, LokiPublicChat>()
@ -50,10 +49,10 @@ class LokiPublicChatManager(private val context: Context) {
public fun addChat(server: String, channel: Long, name: String): LokiPublicChat { public fun addChat(server: String, channel: Long, name: String): LokiPublicChat {
val chat = LokiPublicChat(channel, server, name, true) 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 // Create the group if we don't have one
if (threadID < 0) { 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 threadID = result.threadId
} }
DatabaseFactory.getLokiThreadDatabase(context).setPublicChat(chat, threadID) DatabaseFactory.getLokiThreadDatabase(context).setPublicChat(chat, threadID)
@ -74,7 +73,7 @@ class LokiPublicChatManager(private val context: Context) {
removedChatThreadIds.forEach { pollers.remove(it)?.stop() } removedChatThreadIds.forEach { pollers.remove(it)?.stop() }
// Only append to chats if we have a thread for the chat // 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) { private fun listenToThreadDeletion(threadID: Long) {

View File

@ -45,6 +45,11 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
fun getFriendRequestStatus(threadID: Long): LokiThreadFriendRequestStatus { fun getFriendRequestStatus(threadID: Long): LokiThreadFriendRequestStatus {
if (threadID < 0) { return LokiThreadFriendRequestStatus.NONE } 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 database = databaseHelper.readableDatabase
val result = database.get(friendRequestTableName, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) { cursor -> val result = database.get(friendRequestTableName, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) { cursor ->
cursor.getInt(friendRequestStatus) cursor.getInt(friendRequestStatus)

View File

@ -34,6 +34,9 @@ data class BackgroundMessage private constructor(val data: Map<String, Any>) {
@JvmStatic @JvmStatic
fun createSessionRestore(recipient: String) = BackgroundMessage(mapOf( "recipient" to recipient, "friendRequest" to true, "sessionRestore" to true )) 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 { internal fun parse(serialized: String): BackgroundMessage {
val data = JsonUtil.fromJson(serialized, Map::class.java) as? Map<String, Any> ?: throw AssertionError("JSON parsing failed") val data = JsonUtil.fromJson(serialized, Map::class.java) as? Map<String, Any> ?: throw AssertionError("JSON parsing failed")
return BackgroundMessage(data) return BackgroundMessage(data)
@ -99,6 +102,10 @@ class PushBackgroundMessageSendJob private constructor(
dataMessage.asSessionRestore(true) dataMessage.asSessionRestore(true)
} }
if (message.get("sessionRequest", false)) {
dataMessage.asSessionRequest(true)
}
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender() val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
val address = SignalServiceAddress(recipient) val address = SignalServiceAddress(recipient)
try { try {

View File

@ -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<List<String>> {
private var members = listOf<String>()
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<String>
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<List<String>> {
return CreateClosedGroupLoader(this)
}
override fun onLoadFinished(loader: Loader<List<String>>, members: List<String>) {
update(members)
}
override fun onLoaderReset(loader: Loader<List<String>>) {
update(listOf())
}
private fun update(members: List<String>) {
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<CreateClosedGroupActivity>,
private val avatar: Bitmap?,
private val name: String?,
private val members: Set<Recipient>,
private val admins: Set<Recipient>
) : AsyncTask<Void, Void, Optional<GroupManager.GroupActionResult>>() {
override fun doInBackground(vararg params: Void?): Optional<GroupManager.GroupActionResult> {
val activity = activity.get() ?: return Optional.absent()
return Optional.of(GroupManager.createGroup(activity, members, avatar, name, false, admins))
}
override fun onPostExecute(result: Optional<GroupManager.GroupActionResult>) {
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
}

View File

@ -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<CreateClosedGroupAdapter.ViewHolder>() {
lateinit var glide: GlideRequests
val selectedMembers = mutableSetOf<String>()
var members = listOf<String>()
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)
}

View File

@ -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<List<String>>(context) {
override fun loadInBackground(): List<String> {
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<String>()
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
}
}

View File

@ -83,6 +83,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
profileButton.hexEncodedPublicKey = hexEncodedPublicKey profileButton.hexEncodedPublicKey = hexEncodedPublicKey
profileButton.update() profileButton.update()
profileButton.setOnClickListener { openSettings() } profileButton.setOnClickListener { openSettings() }
createClosedGroupButton.setOnClickListener { createClosedGroup() }
joinPublicChatButton.setOnClickListener { joinPublicChat() } joinPublicChatButton.setOnClickListener { joinPublicChat() }
// Set up seed reminder view // Set up seed reminder view
val isMasterDevice = (TextSecurePreferences.getMasterHexEncodedPublicKey(this) == null) val isMasterDevice = (TextSecurePreferences.getMasterHexEncodedPublicKey(this) == null)
@ -182,6 +183,11 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
show(intent) show(intent)
} }
private fun createClosedGroup() {
val intent = Intent(this, CreateClosedGroupActivity::class.java)
show(intent)
}
private fun joinPublicChat() { private fun joinPublicChat() {
val intent = Intent(this, JoinPublicChatActivity::class.java) val intent = Intent(this, JoinPublicChatActivity::class.java)
show(intent) show(intent)

View File

@ -111,7 +111,7 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
// region Polling // region Polling
private fun getDataMessage(message: LokiPublicChatMessage): SignalServiceDataMessage { private fun getDataMessage(message: LokiPublicChatMessage): SignalServiceDataMessage {
val id = group.id.toByteArray() 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) { val quote = if (message.quote != null) {
SignalServiceDataMessage.Quote(message.quote!!.quotedMessageTimestamp, SignalServiceAddress(message.quote!!.quoteeHexEncodedPublicKey), message.quote!!.quotedMessageBody, listOf()) SignalServiceDataMessage.Quote(message.quote!!.quotedMessageTimestamp, SignalServiceAddress(message.quote!!.quoteeHexEncodedPublicKey), message.quote!!.quotedMessageBody, listOf())
} else { } else {

View File

@ -63,7 +63,7 @@ class LokiRSSFeedPoller(private val context: Context, private val feed: LokiRSSF
bodyAsHTML = matcher.replaceAll("$2 ($1)") bodyAsHTML = matcher.replaceAll("$2 ($1)")
val body = Html.fromHtml(bodyAsHTML).toString().trim() val body = Html.fromHtml(bodyAsHTML).toString().trim()
val id = feed.id.toByteArray() 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 x2 = SignalServiceDataMessage(timestamp, x1, null, body)
val x3 = SignalServiceContent(x2, "Loki", SignalServiceAddress.DEFAULT_DEVICE_ID, timestamp, false, false) val x3 = SignalServiceContent(x2, "Loki", SignalServiceAddress.DEFAULT_DEVICE_ID, timestamp, false, false)
PushDecryptJob(context).handleTextMessage(x3, x2, Optional.absent(), Optional.absent()) PushDecryptJob(context).handleTextMessage(x3, x2, Optional.absent(), Optional.absent())

View File

@ -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
}

View File

@ -79,7 +79,7 @@ public class IncomingMediaMessage {
this.quote = quote.orNull(); this.quote = quote.orNull();
this.unidentified = unidentified; 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; else this.groupId = null;
this.attachments.addAll(PointerAttachment.forPointers(attachments)); this.attachments.addAll(PointerAttachment.forPointers(attachments));

View File

@ -78,7 +78,7 @@ public class IncomingTextMessage implements Parcelable {
this.unidentified = unidentified; this.unidentified = unidentified;
if (group.isPresent()) { if (group.isPresent()) {
this.groupId = Address.fromSerialized(GroupUtil.getEncodedId(group.get().getGroupId(), false)); this.groupId = Address.fromSerialized(GroupUtil.getEncodedId(group.get()));
} else { } else {
this.groupId = null; this.groupId = null;
} }

View File

@ -17,7 +17,6 @@
package org.thoughtcrime.securesms.sms; package org.thoughtcrime.securesms.sms;
import android.content.Context; import android.content.Context;
import android.os.AsyncTask;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.ApplicationContext;
@ -60,11 +59,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager; 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.ContactTokenDetails;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI; import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus; import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus;
import org.whispersystems.signalservice.loki.utilities.PromiseUtil; import org.whispersystems.signalservice.loki.utilities.PromiseUtil;
@ -143,6 +138,10 @@ public class MessageSender {
public static void sendRestoreSessionMessage(Context context, String contactHexEncodedPublicKey) { public static void sendRestoreSessionMessage(Context context, String contactHexEncodedPublicKey) {
ApplicationContext.getInstance(context).getJobManager().add(new PushBackgroundMessageSendJob(BackgroundMessage.createSessionRestore(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 // endregion
public static long send(final Context context, public static long send(final Context context,
@ -202,7 +201,7 @@ public class MessageSender {
if (attachment != null) { message.getAttachments().add(attachment); } if (attachment != null) { message.getAttachments().add(attachment); }
long messageID = database.insertMessageOutbox(message, allocatedThreadId, forceSms, insertListener); long messageID = database.insertMessageOutbox(message, allocatedThreadId, forceSms, insertListener);
// Loki - Set the message's friend request status as soon as it has hit the database // 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); FriendRequestHandler.updateFriendRequestState(context, FriendRequestHandler.ActionType.Sending, messageID, allocatedThreadId);
} }
sendMediaMessage(context, recipient, forceSms, messageID, message.getExpiresIn()); sendMediaMessage(context, recipient, forceSms, messageID, message.getExpiresIn());
@ -215,7 +214,7 @@ public class MessageSender {
try { try {
long messageID = database.insertMessageOutbox(message, allocatedThreadId, forceSms, insertListener); long messageID = database.insertMessageOutbox(message, allocatedThreadId, forceSms, insertListener);
// Loki - Set the message's friend request status as soon as it has hit the database // 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); FriendRequestHandler.updateFriendRequestState(context, FriendRequestHandler.ActionType.Sending, messageID, allocatedThreadId);
} }
sendMediaMessage(context, recipient, forceSms, messageID, message.getExpiresIn()); sendMediaMessage(context, recipient, forceSms, messageID, message.getExpiresIn());

View File

@ -11,11 +11,13 @@ import network.loki.messenger.R;
import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.GroupDatabase.*;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientModifiedListener; import org.thoughtcrime.securesms.recipients.RecipientModifiedListener;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import java.io.IOException; import java.io.IOException;
import java.util.Collections; 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_SIGNAL_GROUP_PREFIX = "__textsecure_group__!";
private static final String ENCODED_MMS_GROUP_PREFIX = "__signal_mms_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(); 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) { public static String getEncodedId(byte[] groupId, boolean mms) {
return (mms ? ENCODED_MMS_GROUP_PREFIX : ENCODED_SIGNAL_GROUP_PREFIX) + Hex.toStringCondensed(groupId); 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 { public static byte[] getDecodedId(String groupId) throws IOException {
if (!isEncodedGroup(groupId)) { if (!isEncodedGroup(groupId)) {
throw new IOException("Invalid encoding"); throw new IOException("Invalid encoding");
@ -48,13 +70,21 @@ public class GroupUtil {
} }
public static boolean isEncodedGroup(@NonNull String groupId) { 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) { public static boolean isMmsGroup(@NonNull String groupId) {
return groupId.startsWith(ENCODED_MMS_GROUP_PREFIX); 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 @WorkerThread
public static Optional<OutgoingGroupMediaMessage> createGroupLeaveMessage(@NonNull Context context, @NonNull Recipient groupRecipient) { public static Optional<OutgoingGroupMediaMessage> createGroupLeaveMessage(@NonNull Context context, @NonNull Recipient groupRecipient) {
String encodedGroupId = groupRecipient.getAddress().toGroupString(); String encodedGroupId = groupRecipient.getAddress().toGroupString();
@ -100,24 +130,49 @@ public class GroupUtil {
@NonNull private final Context context; @NonNull private final Context context;
@Nullable private final GroupContext groupContext; @Nullable private final GroupContext groupContext;
@Nullable private final List<Recipient> members; private final List<Recipient> members;
private final List<Recipient> removedMembers;
private boolean ourDeviceWasRemoved;
public GroupDescription(@NonNull Context context, @Nullable GroupContext groupContext) { public GroupDescription(@NonNull Context context, @Nullable GroupContext groupContext) {
this.context = context.getApplicationContext(); this.context = context.getApplicationContext();
this.groupContext = groupContext; 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()) { if (groupContext != null && !groupContext.getMembersList().isEmpty()) {
this.members.add(Recipient.from(context, Address.fromExternal(context, member), true)); List<String> memberList = groupContext.getMembersList();
List<Address> 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) { 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(); StringBuilder description = new StringBuilder();
description.append(context.getString(R.string.MessageRecord_s_updated_group, sender.toShortString())); description.append(context.getString(R.string.MessageRecord_s_updated_group, sender.toShortString()));
@ -127,14 +182,20 @@ public class GroupUtil {
String title = groupContext.getName(); String title = groupContext.getName();
if (members != null) { if (!members.isEmpty()) {
description.append("\n"); description.append("\n");
description.append(context.getResources().getQuantityString(R.plurals.GroupUtil_joined_the_group, description.append(context.getResources().getQuantityString(R.plurals.GroupUtil_joined_the_group,
members.size(), toString(members))); 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 (title != null && !title.trim().isEmpty()) {
if (members != null) description.append(" "); if (!members.isEmpty()) description.append(" ");
else description.append("\n"); else description.append("\n");
description.append(context.getString(R.string.GroupUtil_group_name_is_now, title)); description.append(context.getString(R.string.GroupUtil_group_name_is_now, title));
} }
@ -143,7 +204,7 @@ public class GroupUtil {
} }
public void addListener(RecipientModifiedListener listener) { public void addListener(RecipientModifiedListener listener) {
if (this.members != null) { if (!this.members.isEmpty()) {
for (Recipient member : this.members) { for (Recipient member : this.members) {
member.addListener(listener); member.addListener(listener);
} }
@ -162,5 +223,23 @@ public class GroupUtil {
return result; return result;
} }
private List<Address> 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;
}
} }
} }

View File

@ -74,8 +74,9 @@ public class IdentityUtil {
GroupDatabase.GroupRecord groupRecord; GroupDatabase.GroupRecord groupRecord;
while ((groupRecord = reader.getNext()) != null) { while ((groupRecord = reader.getNext()) != null) {
if (groupRecord.isRSSFeed() || groupRecord.isPublicChat()) { continue; }
if (groupRecord.getMembers().contains(recipient.getAddress()) && groupRecord.isActive() && !groupRecord.isMms()) { 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) { if (remote) {
IncomingTextMessage incoming = new IncomingTextMessage(recipient.getAddress(), 1, time, null, Optional.of(group), 0, false); IncomingTextMessage incoming = new IncomingTextMessage(recipient.getAddress(), 1, time, null, Optional.of(group), 0, false);
@ -126,8 +127,9 @@ public class IdentityUtil {
GroupDatabase.GroupRecord groupRecord; GroupDatabase.GroupRecord groupRecord;
while ((groupRecord = reader.getNext()) != null) { while ((groupRecord = reader.getNext()) != null) {
if (groupRecord.isRSSFeed() || groupRecord.isPublicChat()) { continue; }
if (groupRecord.getMembers().contains(recipient.getAddress()) && groupRecord.isActive()) { 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); IncomingTextMessage incoming = new IncomingTextMessage(recipient.getAddress(), 1, time, null, Optional.of(group), 0, false);
IncomingIdentityUpdateMessage groupUpdate = new IncomingIdentityUpdateMessage(incoming); IncomingIdentityUpdateMessage groupUpdate = new IncomingIdentityUpdateMessage(incoming);