Started use of encryption APIs

This commit is contained in:
Sylvain Berfini 2018-10-26 17:10:48 +02:00
parent c975235cb1
commit d3fc819cf6
18 changed files with 183 additions and 58 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -192,11 +192,12 @@
<ImageView
android:id="@+id/room_security_level"
android:layout_width="33dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/security_alert_button"
android:src="@drawable/security_alert_indicator"
android:layout_alignParentRight="true"
android:layout_below="@id/top"
android:layout_marginRight="12dp"/>
android:layout_marginTop="6dp"
android:layout_marginRight="6dp"/>
</RelativeLayout>

View file

@ -42,6 +42,7 @@ enable_basic_to_client_group_chat_room_migration=0
enable_simple_group_chat_message_state=0
aggregate_imdn=1
version_check_url_root=https://www.linphone.org/releases
xmlrpc_server_url=https://subscribe.linphone.org:444/wizard.php
[app]
activation_code_length=4
@ -60,3 +61,11 @@ username_max_length=64
username_min_length=1
username_regex=^[a-z0-9_.\-]*$
xmlrpc_url=https://subscribe.linphone.org:444/wizard.php
[lime]
lime_v2=1
x3dh_server_url=http://54.37.202.229:80/flexisip-account-manager/x3dh-25519.php
lime_update_threshold=86400
max_nb_device_per_participant=255
allow_message_in_unsafe_chatroom=0
unsafe_if_sas_refused=1

View file

@ -682,7 +682,8 @@ public class LinphoneActivity extends LinphoneGenericActivity implements OnClick
}
}
public void goToChatCreator(String address, ArrayList<ContactAddress> selectedContacts, String subject, boolean isGoBack, Bundle shareInfos, boolean createGroupChat) {
public void goToChatCreator(String address, ArrayList<ContactAddress> selectedContacts, String subject, boolean isGoBack, Bundle shareInfos,
boolean createGroupChat, boolean isChatRoomEncrypted) {
if (currentFragment == FragmentsAvailable.INFO_GROUP_CHAT && isGoBack) {
getFragmentManager().popBackStackImmediate();
getFragmentManager().popBackStackImmediate();
@ -692,6 +693,7 @@ public class LinphoneActivity extends LinphoneGenericActivity implements OnClick
extras.putString("subject", subject);
extras.putString("groupChatRoomAddress", address);
extras.putBoolean("createGroupChatRoom", createGroupChat);
extras.putBoolean("encrypted", isChatRoomEncrypted);
if (shareInfos != null) {
if (shareInfos.getString("fileSharedUri") != null)
@ -731,7 +733,8 @@ public class LinphoneActivity extends LinphoneGenericActivity implements OnClick
displayMissedChats(LinphoneManager.getInstance().getUnreadMessageCount());
}
public void goToChatGroupInfos(String address, ArrayList<ContactAddress> contacts, String subject, boolean isEditionEnabled, boolean isGoBack, Bundle shareInfos) {
public void goToChatGroupInfos(String address, ArrayList<ContactAddress> contacts, String subject,
boolean isEditionEnabled, boolean isGoBack, Bundle shareInfos, boolean enableEncryption) {
if (currentFragment == FragmentsAvailable.CREATE_CHAT && isGoBack) {
getFragmentManager().popBackStackImmediate();
getFragmentManager().popBackStackImmediate();
@ -741,6 +744,7 @@ public class LinphoneActivity extends LinphoneGenericActivity implements OnClick
extras.putBoolean("isEditionEnabled", isEditionEnabled);
extras.putSerializable("ContactAddress", contacts);
extras.putString("subject", subject);
extras.putBoolean("encryptionEnabled", enableEncryption);
if (shareInfos != null) {
if (shareInfos.getString("fileSharedUri") != null)

View file

@ -54,8 +54,7 @@ import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class CreateAccountFragment extends Fragment implements CompoundButton.OnCheckedChangeListener
, OnClickListener, AccountCreatorListener {
public class CreateAccountFragment extends Fragment implements CompoundButton.OnCheckedChangeListener, OnClickListener, AccountCreatorListener {
private EditText phoneNumberEdit, usernameEdit, passwordEdit, passwordConfirmEdit, emailEdit, dialCode;
private TextView phoneNumberError, passwordError, passwordConfirmError, emailError, assisstantTitle, sipUri, skip, instruction;
private ImageView phoneNumberInfo;

View file

@ -79,6 +79,7 @@ public class ChatCreationFragment extends Fragment implements View.OnClickListen
private ImageView mSecurityToggleOff, mSecurityToggleOn;
private Switch mSecurityToggle;
private boolean mCreateGroupChatRoom;
private boolean mChatRoomEncrypted;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@ -97,6 +98,7 @@ public class ChatCreationFragment extends Fragment implements View.OnClickListen
mChatRoomSubject = getArguments().getString("subject");
mChatRoomAddress = getArguments().getString("groupChatRoomAddress");
mCreateGroupChatRoom = getArguments().getBoolean("createGroupChatRoom", false);
mChatRoomEncrypted = getArguments().getBoolean("encrypted", false);
}
mWaitLayout = view.findViewById(R.id.waitScreen);
@ -170,6 +172,13 @@ public class ChatCreationFragment extends Fragment implements View.OnClickListen
}
});
mSecurityToggle.setChecked(mChatRoomEncrypted);
if (mChatRoomSubject != null && mChatRoomAddress != null) {
mSecurityToggle.setEnabled(false);
mSecurityToggleOn.setOnClickListener(null);
mSecurityToggleOff.setOnClickListener(null);
}
LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity().getApplicationContext());
mContactsList.setAdapter(mSearchAdapter);
@ -389,7 +398,7 @@ public class ChatCreationFragment extends Fragment implements View.OnClickListen
}
private void removeContactFromSelection(ContactAddress ca) {
private void addOrRemoveContactFromSelection(ContactAddress ca) {
updateContactsClick(ca, mSearchAdapter.getContactsSelectedList());
mSearchAdapter.notifyDataSetChanged();
updateListSelected();
@ -447,7 +456,7 @@ public class ChatCreationFragment extends Fragment implements View.OnClickListen
if (chatRoom == null) {
ProxyConfig lpc = lc.getDefaultProxyConfig();
if (lpc != null && lpc.getConferenceFactoryUri() != null && !LinphonePreferences.instance().useBasicChatRoomFor1To1()) {
mChatRoom = lc.createClientGroupChatRoom(getString(R.string.dummy_group_chat_subject), true);
mChatRoom = lc.createClientGroupChatRoom(getString(R.string.dummy_group_chat_subject), true, mSecurityToggle.isChecked());
mChatRoom.addListener(mChatRoomCreationListener);
mChatRoom.addParticipant(participant);
} else {
@ -459,17 +468,17 @@ public class ChatCreationFragment extends Fragment implements View.OnClickListen
}
} else {
mContactsSelectedLayout.removeAllViews();
LinphoneActivity.instance().goToChatGroupInfos(null, mContactsSelected, null, true, false, mShareInfos);
LinphoneActivity.instance().goToChatGroupInfos(null, mContactsSelected, null, true, false, mShareInfos, mSecurityToggle.isChecked());
}
} else {
LinphoneActivity.instance().goToChatGroupInfos(mChatRoomAddress, mContactsSelected, mChatRoomSubject, true, true, mShareInfos);
LinphoneActivity.instance().goToChatGroupInfos(mChatRoomAddress, mContactsSelected, mChatRoomSubject, true, true, mShareInfos, mSecurityToggle.isChecked());
}
} else if (id == R.id.clearSearchField) {
mSearchField.setText("");
mSearchAdapter.searchContacts("", mContactsList);
} else if (id == R.id.contactChatDelete) {
ContactAddress ca = (ContactAddress) view.getTag();
removeContactFromSelection(ca);
addOrRemoveContactFromSelection(ca);
}
}
@ -479,10 +488,16 @@ public class ChatCreationFragment extends Fragment implements View.OnClickListen
Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull();
ProxyConfig lpc = lc.getDefaultProxyConfig();
if (lpc == null || lpc.getConferenceFactoryUri() == null || mCreateGroupChatRoom == false) {
if (mSecurityToggle.isChecked() && lpc != null && lpc.getConferenceFactoryUri() != null) {
mChatRoom = lc.createClientGroupChatRoom(getString(R.string.dummy_group_chat_subject), true, mSecurityToggle.isChecked());
mChatRoom.addListener(mChatRoomCreationListener);
mChatRoom.addParticipant(ca.getAddress());
} else {
ChatRoom chatRoom = lc.getChatRoom(ca.getAddress());
LinphoneActivity.instance().goToChat(chatRoom.getPeerAddress().asStringUriOnly(), mShareInfos, chatRoom.getLocalAddress().asString());
}
} else {
removeContactFromSelection(ca);
addOrRemoveContactFromSelection(ca);
}
}

View file

@ -101,14 +101,14 @@ public class ChatListFragment extends Fragment implements ContactsUpdatedListene
mNewDiscussionButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LinphoneActivity.instance().goToChatCreator(null, null, null, false, null, false);
LinphoneActivity.instance().goToChatCreator(null, null, null, false, null, false, false);
}
});
mNewGroupDiscussionButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LinphoneActivity.instance().goToChatCreator(null, null, null, false, null, true);
LinphoneActivity.instance().goToChatCreator(null, null, null, false, null, true, false);
}
});

View file

@ -154,17 +154,28 @@ public class ChatRoomViewHolder extends RecyclerView.ViewHolder implements View.
if (mRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) {
LinphoneContact contact = ContactsManager.getInstance().findContactFromAddress(mRoom.getPeerAddress());
if (contact != null) {
if (mRoom.hasCapability(ChatRoomCapabilities.Encrypted.toInt())) {
ContactAvatar.displayAvatar(contact, mRoom.getSecurityLevel(), avatarLayout);
} else {
ContactAvatar.displayAvatar(contact, avatarLayout);
}
} else {
String username = mRoom.getPeerAddress().getDisplayName();
if (username == null) {
username = mRoom.getPeerAddress().getUsername();
}
if (mRoom.hasCapability(ChatRoomCapabilities.Encrypted.toInt())) {
ContactAvatar.displayAvatar(username, mRoom.getSecurityLevel(), avatarLayout);
} else {
ContactAvatar.displayAvatar(username, avatarLayout);
}
}
} else {
((ImageView)avatarLayout.findViewById(R.id.contact_picture)).setImageBitmap(mDefaultGroupBitmap);
avatarLayout.findViewById(R.id.generated_avatar).setVisibility(View.GONE);
if (mRoom.hasCapability(ChatRoomCapabilities.Encrypted.toInt())) {
ContactAvatar.displayGroupChatAvatar(mRoom.getSecurityLevel(), avatarLayout);
} else {
ContactAvatar.displayGroupChatAvatar(avatarLayout);
}
}
}

View file

@ -30,6 +30,7 @@ import org.linphone.R;
import org.linphone.contacts.ContactsManager;
import org.linphone.contacts.LinphoneContact;
import org.linphone.core.Address;
import org.linphone.core.ChatRoomSecurityLevel;
import org.linphone.core.Participant;
import org.linphone.core.ParticipantDevice;
import org.linphone.ui.ContactAvatar;
@ -76,6 +77,21 @@ public class DeviceAdapter extends BaseExpandableListAdapter {
Address deviceAddress = device.getAddress();
holder.deviceName.setText(deviceAddress.getUriParam("gr")); //TODO
ChatRoomSecurityLevel level = device.getSecurityLevel();
switch (level) {
case Safe:
holder.securityLevel.setImageResource(R.drawable.security_2_indicator);
break;
case Encrypted:
holder.securityLevel.setImageResource(R.drawable.security_1_indicator);
break;
case ClearText:
case Unsafe:
default:
holder.securityLevel.setImageResource(R.drawable.security_alert_indicator);
break;
}
} else {
Participant participant = (Participant) getGroup(groupPosition);
@ -97,11 +113,11 @@ public class DeviceAdapter extends BaseExpandableListAdapter {
Address participantAddress = participant.getAddress();
LinphoneContact contact = ContactsManager.getInstance().findContactFromAddress(participantAddress);
if (contact != null) {
ContactAvatar.displayAvatar(contact, holder.avatarLayout);
ContactAvatar.displayAvatar(contact, participant.getSecurityLevel(), holder.avatarLayout);
holder.participantName.setText(contact.getFullName());
} else {
String displayName = LinphoneUtils.getAddressDisplayName(participantAddress);
ContactAvatar.displayAvatar(displayName, holder.avatarLayout);
ContactAvatar.displayAvatar(displayName, participant.getSecurityLevel(), holder.avatarLayout);
holder.participantName.setText(displayName);
}
@ -132,7 +148,21 @@ public class DeviceAdapter extends BaseExpandableListAdapter {
Address deviceAddress = device.getAddress();
holder.deviceName.setText(deviceAddress.getUriParam("gr")); //TODO
//holder.securityLevel.setImageResource();
ChatRoomSecurityLevel level = device.getSecurityLevel();
switch (level) {
case Safe:
holder.securityLevel.setImageResource(R.drawable.security_2_indicator);
break;
case Encrypted:
holder.securityLevel.setImageResource(R.drawable.security_1_indicator);
break;
case ClearText:
case Unsafe:
default:
holder.securityLevel.setImageResource(R.drawable.security_alert_indicator);
break;
}
return view;
}

View file

@ -146,7 +146,6 @@ public class DevicesFragment extends Fragment {
boolean onlyDisplayChilds = mRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt());
//TODO get list of participants and devices
if (mRoom != null && mRoom.getNbParticipants() > 0) {
mAdapter.updateListItems(Arrays.asList(mRoom.getParticipants()), onlyDisplayChilds);
}

View file

@ -69,6 +69,7 @@ import org.linphone.core.ChatMessage;
import org.linphone.core.ChatRoom;
import org.linphone.core.ChatRoomCapabilities;
import org.linphone.core.ChatRoomListener;
import org.linphone.core.ChatRoomSecurityLevel;
import org.linphone.core.Content;
import org.linphone.core.Core;
import org.linphone.core.EventLog;
@ -186,7 +187,9 @@ public class GroupChatFragment extends Fragment implements ChatRoomListener, Con
ContactAddress ca = new ContactAddress(c, a.asString(), "", c.isFriend(), p.isAdmin());
participants.add(ca);
}
LinphoneActivity.instance().goToChatGroupInfos(mRemoteSipAddress.asString(), participants, mChatRoom.getSubject(), mChatRoom.getMe() != null ? mChatRoom.getMe().isAdmin() : false, false, null);
LinphoneActivity.instance().goToChatGroupInfos(mRemoteSipAddress.asString(), participants, mChatRoom.getSubject(),
mChatRoom.getMe() != null ? mChatRoom.getMe().isAdmin() : false, false, null,
mChatRoom.hasCapability(ChatRoomCapabilities.Encrypted.toInt()));
}
});
@ -634,11 +637,23 @@ public class GroupChatFragment extends Fragment implements ChatRoomListener, Con
setReadOnly();
}
if (mChatRoom.hasCapability(ChatRoomCapabilities.Basic.toInt())) {
if (!mChatRoom.hasCapability(ChatRoomCapabilities.Encrypted.toInt())) {
mChatRoomSecurityLevel.setVisibility(View.GONE);
} else {
//TODO
//mChatRoomSecurityLevel.setImageResource();
ChatRoomSecurityLevel level = mChatRoom.getSecurityLevel();
switch (level) {
case Safe:
mChatRoomSecurityLevel.setImageResource(R.drawable.security_2_indicator);
break;
case Encrypted:
mChatRoomSecurityLevel.setImageResource(R.drawable.security_1_indicator);
break;
case ClearText:
case Unsafe:
mChatRoomSecurityLevel.setImageResource(R.drawable.security_alert_indicator);
break;
}
}
}
@ -1032,6 +1047,12 @@ public class GroupChatFragment extends Fragment implements ChatRoomListener, Con
}
@Override
public void onSecurityEvent(ChatRoom cr, EventLog eventLog) {
mEventsAdapter.addToHistory(eventLog);
scrollToBottom();
}
@Override
public void onStateChanged(ChatRoom cr, ChatRoom.State newState) {
if (mChatRoom.hasBeenLeft()) {

View file

@ -35,6 +35,7 @@ import org.linphone.activities.LinphoneActivity;
import org.linphone.contacts.ContactAddress;
import org.linphone.contacts.LinphoneContact;
import org.linphone.core.ChatRoom;
import org.linphone.core.ChatRoomCapabilities;
import org.linphone.core.Participant;
import org.linphone.ui.ContactAvatar;

View file

@ -80,6 +80,7 @@ public class GroupInfoFragment extends Fragment implements ChatRoomListener {
private Bundle mShareInfos;
private Context mContext;
private LinearLayoutManager layoutManager;
private boolean mIsEncryptionEnabled;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@ -113,6 +114,8 @@ public class GroupInfoFragment extends Fragment implements ChatRoomListener {
mIsEditionEnabled = false;
}
mIsEncryptionEnabled = getArguments().getBoolean("encryptionEnabled", false);
mParticipantsList = view.findViewById(R.id.chat_room_participants);
mAdapter = new GroupInfoAdapter(mParticipants, !mIsEditionEnabled, !mIsAlreadyCreatedGroup);
mAdapter.setOnDeleteClickListener(new View.OnClickListener() {
@ -160,7 +163,7 @@ public class GroupInfoFragment extends Fragment implements ChatRoomListener {
getFragmentManager().popBackStack();
}
} else {
LinphoneActivity.instance().goToChatCreator(null, mParticipants, null, true, mShareInfos, true);
LinphoneActivity.instance().goToChatCreator(null, mParticipants, null, true, mShareInfos, true, mIsEncryptionEnabled);
}
}
});
@ -205,7 +208,7 @@ public class GroupInfoFragment extends Fragment implements ChatRoomListener {
@Override
public void onClick(View view) {
if (mIsEditionEnabled) {
LinphoneActivity.instance().goToChatCreator(mGroupChatRoomAddress != null ? mGroupChatRoomAddress.asString() : null, mParticipants, mSubject, !mIsAlreadyCreatedGroup, null, true);
LinphoneActivity.instance().goToChatCreator(mGroupChatRoomAddress != null ? mGroupChatRoomAddress.asString() : null, mParticipants, mSubject, !mIsAlreadyCreatedGroup, null, true, mIsEncryptionEnabled);
}
}
});
@ -213,7 +216,7 @@ public class GroupInfoFragment extends Fragment implements ChatRoomListener {
mAddParticipantsButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
LinphoneActivity.instance().goToChatCreator(mGroupChatRoomAddress != null ? mGroupChatRoomAddress.asString() : null, mParticipants, mSubject, !mIsAlreadyCreatedGroup, null, true);
LinphoneActivity.instance().goToChatCreator(mGroupChatRoomAddress != null ? mGroupChatRoomAddress.asString() : null, mParticipants, mSubject, !mIsAlreadyCreatedGroup, null, true, mIsEncryptionEnabled);
}
});
@ -258,7 +261,7 @@ public class GroupInfoFragment extends Fragment implements ChatRoomListener {
public void onClick(View view) {
if (!mIsAlreadyCreatedGroup) {
mWaitLayout.setVisibility(View.VISIBLE);
mTempChatRoom = LinphoneManager.getLc().createClientGroupChatRoom(mSubjectField.getText().toString(), mParticipants.size() == 1);
mTempChatRoom = LinphoneManager.getLc().createClientGroupChatRoom(mSubjectField.getText().toString(), mParticipants.size() == 1, mIsEncryptionEnabled);
mTempChatRoom.addListener(mChatRoomCreationListener);
Address addresses[] = new Address[mParticipants.size()];
@ -492,6 +495,11 @@ public class GroupInfoFragment extends Fragment implements ChatRoomListener {
}
@Override
public void onSecurityEvent(ChatRoom cr, EventLog eventLog) {
refreshParticipantsList();
}
@Override
public void onUndecryptableMessageReceived(ChatRoom cr, ChatMessage msg) {

View file

@ -146,6 +146,7 @@ public class SearchContactsListAdapter extends RecyclerView.Adapter<SearchContac
if (c != null) {
ContactAvatar.displayAvatar(c, holder.avatarLayout);
//TODO get if contact has security capabilities
} else {
ContactAvatar.displayAvatar(holder.name.getText().toString(), holder.avatarLayout);
}

View file

@ -29,6 +29,7 @@ import org.linphone.LinphoneService;
import org.linphone.LinphoneUtils;
import org.linphone.R;
import org.linphone.contacts.LinphoneContact;
import org.linphone.core.ChatRoomSecurityLevel;
import org.linphone.mediastream.Log;
import java.io.IOException;
@ -63,6 +64,28 @@ public class ContactAvatar {
return generatedAvatarText.toUpperCase();
}
private static void setSecurityLevel(ChatRoomSecurityLevel level, View v) {
ContactAvatarHolder holder = new ContactAvatarHolder(v);
if (holder.securityLevel != null) {
holder.securityLevel.setVisibility(View.VISIBLE);
switch (level) {
case Safe:
holder.securityLevel.setImageResource(R.drawable.security_2_indicator);
break;
case Encrypted:
holder.securityLevel.setImageResource(R.drawable.security_1_indicator);
break;
case ClearText:
case Unsafe:
default:
holder.securityLevel.setImageResource(R.drawable.security_alert_indicator);
break;
}
} else {
holder.securityLevel.setVisibility(View.GONE);
}
}
public static void setAvatarMask(View v, int resourceId) {
ContactAvatarHolder holder = new ContactAvatarHolder(v);
holder.avatarMask.setImageResource(resourceId);
@ -77,12 +100,17 @@ public class ContactAvatar {
if (displayName.startsWith("+")) {
// If display name is a phone number, use default avatar because generated one will be +...
holder.generatedAvatar.setVisibility(View.GONE);
return;
}
} else {
holder.generatedAvatar.setText(generateAvatar(displayName));
holder.generatedAvatar.setVisibility(View.VISIBLE);
}
holder.securityLevel.setVisibility(View.GONE);
}
public static void displayAvatar(String displayName, ChatRoomSecurityLevel securityLevel, View v) {
displayAvatar(displayName, v);
setSecurityLevel(securityLevel, v);
}
public static void displayAvatar(LinphoneContact contact, View v) {
if (contact == null || v == null) return;
@ -112,25 +140,23 @@ public class ContactAvatar {
contact.getFirstName() + " " + contact.getLastName() : contact.getFullName()));
holder.generatedAvatar.setVisibility(View.VISIBLE);
}
if (holder.securityLevel != null) {
//TODO when security level will be available
/*if (contact.hasSecurity()) {
holder.securityLevel.setVisibility(View.VISIBLE);
switch(contact.getSecurityLevel()) {
case 0:
holder.securityLevel.setImageResource(R.drawable.security_alert_indicator);
break;
case 1:
holder.securityLevel.setImageResource(R.drawable.security_1_indicator.png);
break;
case 2:
holder.securityLevel.setImageResource(R.drawable.security_2_indicator);
break;
}
} else {
holder.securityLevel.setVisibility(View.GONE);
}*/
}
public static void displayAvatar(LinphoneContact contact, ChatRoomSecurityLevel securityLevel, View v) {
displayAvatar(contact, v);
setSecurityLevel(securityLevel, v);
}
public static void displayGroupChatAvatar(View v) {
ContactAvatarHolder holder = new ContactAvatarHolder(v);
holder.contactPicture.setImageResource(R.drawable.chat_group_avatar);
holder.generatedAvatar.setVisibility(View.GONE);
holder.securityLevel.setVisibility(View.GONE);
}
public static void displayGroupChatAvatar(ChatRoomSecurityLevel level, View v) {
displayGroupChatAvatar(v);
setSecurityLevel(level, v);
}
}

@ -1 +1 @@
Subproject commit 7515af934e5fa96c7e59f94f49c8bd25dd3ce071
Subproject commit 2dea113f7f1596af94d024008f401899b102b2ab

@ -1 +1 @@
Subproject commit 876682e9a6a07290bf867e3fc363c476c68a3da2
Subproject commit 9b9dcbbc5ec45c637dbbade15fdd64cf56bbd758

@ -1 +1 @@
Subproject commit 5de9bb4086b036bfe84e088c87211cdf48f37cd6
Subproject commit c08cd438a0583ea80ae3bc0449ed5817509cf681