mirror of
https://github.com/oxen-io/session-android.git
synced 2025-06-08 07:18:33 +00:00
Handle GV2 addresses.
This commit is contained in:
parent
06eadd0c15
commit
e4456bb236
@ -223,6 +223,14 @@
|
|||||||
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
|
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data android:scheme="https"
|
||||||
|
android:host="group.signal.org"/>
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
<meta-data android:name="com.sec.minimode.icon.portrait.normal"
|
<meta-data android:name="com.sec.minimode.icon.portrait.normal"
|
||||||
android:resource="@mipmap/ic_launcher" />
|
android:resource="@mipmap/ic_launcher" />
|
||||||
<meta-data android:name="com.sec.minimode.icon.landscape.normal"
|
<meta-data android:name="com.sec.minimode.icon.landscape.normal"
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
package org.thoughtcrime.securesms;
|
package org.thoughtcrime.securesms;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
|
||||||
import org.thoughtcrime.securesms.groups.GroupId;
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
@ -49,5 +49,8 @@ public interface BindableConversationItem extends Unbindable {
|
|||||||
void onReactionClicked(@NonNull View reactionTarget, long messageId, boolean isMms);
|
void onReactionClicked(@NonNull View reactionTarget, long messageId, boolean isMms);
|
||||||
void onGroupMemberClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
|
void onGroupMemberClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
|
||||||
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
|
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
|
||||||
|
|
||||||
|
/** @return true if handled, false if you want to let the normal url handling continue */
|
||||||
|
boolean onUrlClicked(@NonNull String url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
package org.thoughtcrime.securesms;
|
package org.thoughtcrime.securesms;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||||
|
|
||||||
@ -18,6 +21,14 @@ public class MainActivity extends PassphraseRequiredActivity {
|
|||||||
setContentView(R.layout.main_activity);
|
setContentView(R.layout.main_activity);
|
||||||
|
|
||||||
navigator.onCreate(savedInstanceState);
|
navigator.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
handleGroupLinkInIntent(getIntent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onNewIntent(Intent intent) {
|
||||||
|
super.onNewIntent(intent);
|
||||||
|
handleGroupLinkInIntent(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -42,4 +53,11 @@ public class MainActivity extends PassphraseRequiredActivity {
|
|||||||
public @NonNull MainNavigator getNavigator() {
|
public @NonNull MainNavigator getNavigator() {
|
||||||
return navigator;
|
return navigator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleGroupLinkInIntent(Intent intent) {
|
||||||
|
Uri data = intent.getData();
|
||||||
|
if (data != null) {
|
||||||
|
CommunicationActions.handlePotentialGroupLinkUrl(this, data.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,33 +1,17 @@
|
|||||||
package org.thoughtcrime.securesms.components.reminder;
|
package org.thoughtcrime.securesms.components.reminder;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.util.PlayStoreUtil;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
|
|
||||||
public class ExpiredBuildReminder extends Reminder {
|
public class ExpiredBuildReminder extends Reminder {
|
||||||
@SuppressWarnings("unused")
|
|
||||||
private static final String TAG = ExpiredBuildReminder.class.getSimpleName();
|
|
||||||
|
|
||||||
public ExpiredBuildReminder(final Context context) {
|
public ExpiredBuildReminder(final Context context) {
|
||||||
super(context.getString(R.string.reminder_header_expired_build),
|
super(context.getString(R.string.reminder_header_expired_build),
|
||||||
context.getString(R.string.reminder_header_expired_build_details));
|
context.getString(R.string.reminder_header_expired_build_details));
|
||||||
setOkListener(v -> {
|
setOkListener(v -> PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(context));
|
||||||
try {
|
|
||||||
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + context.getPackageName())));
|
|
||||||
} catch (android.content.ActivityNotFoundException anfe) {
|
|
||||||
try {
|
|
||||||
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=" + context.getPackageName())));
|
|
||||||
} catch (android.content.ActivityNotFoundException anfe2) {
|
|
||||||
Log.w(TAG, anfe2);
|
|
||||||
Toast.makeText(context, R.string.OutdatedBuildReminder_no_web_browser_installed, Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1,35 +1,17 @@
|
|||||||
package org.thoughtcrime.securesms.components.reminder;
|
package org.thoughtcrime.securesms.components.reminder;
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
|
||||||
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.util.PlayStoreUtil;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
|
|
||||||
public class OutdatedBuildReminder extends Reminder {
|
public class OutdatedBuildReminder extends Reminder {
|
||||||
|
|
||||||
private static final String TAG = OutdatedBuildReminder.class.getSimpleName();
|
|
||||||
|
|
||||||
public OutdatedBuildReminder(final Context context) {
|
public OutdatedBuildReminder(final Context context) {
|
||||||
super(context.getString(R.string.reminder_header_outdated_build),
|
super(context.getString(R.string.reminder_header_outdated_build),
|
||||||
getPluralsText(context));
|
getPluralsText(context));
|
||||||
setOkListener(v -> {
|
setOkListener(v -> PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(context));
|
||||||
try {
|
|
||||||
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + context.getPackageName())));
|
|
||||||
} catch (ActivityNotFoundException anfe) {
|
|
||||||
try {
|
|
||||||
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=" + context.getPackageName())));
|
|
||||||
} catch (ActivityNotFoundException anfe2) {
|
|
||||||
Log.w(TAG, anfe2);
|
|
||||||
Toast.makeText(context, R.string.OutdatedBuildReminder_no_web_browser_installed, Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CharSequence getPluralsText(final Context context) {
|
private static CharSequence getPluralsText(final Context context) {
|
||||||
|
@ -1388,6 +1388,11 @@ public class ConversationFragment extends LoggingFragment {
|
|||||||
public void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord) {
|
public void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord) {
|
||||||
listener.onMessageWithErrorClicked(messageRecord);
|
listener.onMessageWithErrorClicked(messageRecord);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onUrlClicked(@NonNull String url) {
|
||||||
|
return CommunicationActions.handlePotentialGroupLinkUrl(requireActivity(), url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -110,12 +110,13 @@ import org.thoughtcrime.securesms.revealable.ViewOnceUtil;
|
|||||||
import org.thoughtcrime.securesms.stickers.StickerUrl;
|
import org.thoughtcrime.securesms.stickers.StickerUrl;
|
||||||
import org.thoughtcrime.securesms.util.DateUtils;
|
import org.thoughtcrime.securesms.util.DateUtils;
|
||||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||||
import org.thoughtcrime.securesms.util.LongClickCopySpan;
|
import org.thoughtcrime.securesms.util.InterceptableLongClickCopyLinkSpan;
|
||||||
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
|
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
|
||||||
import org.thoughtcrime.securesms.util.SearchUtil;
|
import org.thoughtcrime.securesms.util.SearchUtil;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||||
import org.thoughtcrime.securesms.util.VibrateUtil;
|
import org.thoughtcrime.securesms.util.VibrateUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.UrlClickHandler;
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
import org.thoughtcrime.securesms.util.views.Stub;
|
import org.thoughtcrime.securesms.util.views.Stub;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
@ -194,6 +195,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
|||||||
private final SharedContactClickListener sharedContactClickListener = new SharedContactClickListener();
|
private final SharedContactClickListener sharedContactClickListener = new SharedContactClickListener();
|
||||||
private final LinkPreviewClickListener linkPreviewClickListener = new LinkPreviewClickListener();
|
private final LinkPreviewClickListener linkPreviewClickListener = new LinkPreviewClickListener();
|
||||||
private final ViewOnceMessageClickListener revealableClickListener = new ViewOnceMessageClickListener();
|
private final ViewOnceMessageClickListener revealableClickListener = new ViewOnceMessageClickListener();
|
||||||
|
private final UrlClickListener urlClickListener = new UrlClickListener();
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
|
||||||
@ -580,7 +582,9 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
|||||||
return messageRecord.isMms() && ((MmsMessageRecord) messageRecord).isViewOnce();
|
return messageRecord.isMms() && ((MmsMessageRecord) messageRecord).isViewOnce();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setBodyText(MessageRecord messageRecord, @Nullable String searchQuery) {
|
private void setBodyText(@NonNull MessageRecord messageRecord,
|
||||||
|
@Nullable String searchQuery)
|
||||||
|
{
|
||||||
bodyText.setClickable(false);
|
bodyText.setClickable(false);
|
||||||
bodyText.setFocusable(false);
|
bodyText.setFocusable(false);
|
||||||
bodyText.setTextSize(TypedValue.COMPLEX_UNIT_SP, TextSecurePreferences.getMessageBodyTextSize(context));
|
bodyText.setTextSize(TypedValue.COMPLEX_UNIT_SP, TextSecurePreferences.getMessageBodyTextSize(context));
|
||||||
@ -916,7 +920,9 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
|||||||
contactPhoto.setAvatar(glideRequests, recipient, false);
|
contactPhoto.setAvatar(glideRequests, recipient, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private SpannableString linkifyMessageBody(SpannableString messageBody, boolean shouldLinkifyAllLinks) {
|
private SpannableString linkifyMessageBody(@NonNull SpannableString messageBody,
|
||||||
|
boolean shouldLinkifyAllLinks)
|
||||||
|
{
|
||||||
int linkPattern = Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS;
|
int linkPattern = Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS;
|
||||||
boolean hasLinks = Linkify.addLinks(messageBody, shouldLinkifyAllLinks ? linkPattern : 0);
|
boolean hasLinks = Linkify.addLinks(messageBody, shouldLinkifyAllLinks ? linkPattern : 0);
|
||||||
|
|
||||||
@ -930,7 +936,8 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
|||||||
for (URLSpan urlSpan : urlSpans) {
|
for (URLSpan urlSpan : urlSpans) {
|
||||||
int start = messageBody.getSpanStart(urlSpan);
|
int start = messageBody.getSpanStart(urlSpan);
|
||||||
int end = messageBody.getSpanEnd(urlSpan);
|
int end = messageBody.getSpanEnd(urlSpan);
|
||||||
messageBody.setSpan(new LongClickCopySpan(urlSpan.getURL()), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
URLSpan span = new InterceptableLongClickCopyLinkSpan(urlSpan.getURL(), urlClickListener);
|
||||||
|
messageBody.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1473,6 +1480,14 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class UrlClickListener implements UrlClickHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean handleOnClick(@NonNull String url) {
|
||||||
|
return eventListener != null && eventListener.onUrlClicked(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class MentionClickableSpan extends ClickableSpan {
|
private class MentionClickableSpan extends ClickableSpan {
|
||||||
private final RecipientId mentionedRecipientId;
|
private final RecipientId mentionedRecipientId;
|
||||||
|
|
||||||
|
@ -0,0 +1,55 @@
|
|||||||
|
package org.thoughtcrime.securesms.groups.ui.invitesandrequests.joining;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.fragment.app.DialogFragment;
|
||||||
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.util.BottomSheetUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.PlayStoreUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||||
|
|
||||||
|
public final class GroupJoinUpdateRequiredBottomSheetDialogFragment extends BottomSheetDialogFragment {
|
||||||
|
|
||||||
|
public static void show(@NonNull FragmentManager manager) {
|
||||||
|
new GroupJoinUpdateRequiredBottomSheetDialogFragment().show(manager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
setStyle(DialogFragment.STYLE_NORMAL,
|
||||||
|
ThemeUtil.isDarkTheme(requireContext()) ? R.style.Theme_Signal_RoundedBottomSheet
|
||||||
|
: R.style.Theme_Signal_RoundedBottomSheet_Light);
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||||
|
return inflater.inflate(R.layout.group_join_update_needed_bottom_sheet, container, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
|
||||||
|
view.findViewById(R.id.group_join_update_button)
|
||||||
|
.setOnClickListener(v -> {
|
||||||
|
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext());
|
||||||
|
dismiss();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void show(@NonNull FragmentManager manager, @Nullable String tag) {
|
||||||
|
BottomSheetUtil.show(manager, tag, this);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,115 @@
|
|||||||
|
package org.thoughtcrime.securesms.groups.v2;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
|
||||||
|
import org.signal.storageservice.protos.groups.GroupInviteLink;
|
||||||
|
import org.signal.zkgroup.InvalidInputException;
|
||||||
|
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||||
|
import org.thoughtcrime.securesms.util.Base64UrlSafe;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
public final class GroupInviteLinkUrl {
|
||||||
|
|
||||||
|
private static final String GROUP_URL_HOST = "group.signal.org";
|
||||||
|
private static final String GROUP_URL_PREFIX = "https://" + GROUP_URL_HOST + "/#";
|
||||||
|
|
||||||
|
private final GroupMasterKey groupMasterKey;
|
||||||
|
private final GroupLinkPassword password;
|
||||||
|
private final String url;
|
||||||
|
|
||||||
|
public static @Nullable GroupInviteLinkUrl fromUrl(@NonNull String urlString)
|
||||||
|
throws InvalidGroupLinkException, UnknownGroupLinkVersionException
|
||||||
|
{
|
||||||
|
URL url;
|
||||||
|
try {
|
||||||
|
url = new URL(urlString);
|
||||||
|
} catch (MalformedURLException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!GROUP_URL_HOST.equalsIgnoreCase(url.getHost())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!"/".equals(url.getPath()) && url.getPath().length() > 0) {
|
||||||
|
throw new InvalidGroupLinkException("No path was expected in url");
|
||||||
|
}
|
||||||
|
|
||||||
|
String encoding = url.getRef();
|
||||||
|
|
||||||
|
if (encoding == null || encoding.length() == 0) {
|
||||||
|
throw new InvalidGroupLinkException("No reference was in the url");
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] bytes = Base64UrlSafe.decodePaddingAgnostic(encoding);
|
||||||
|
GroupInviteLink groupInviteLink = GroupInviteLink.parseFrom(bytes);
|
||||||
|
|
||||||
|
//noinspection SwitchStatementWithTooFewBranches
|
||||||
|
switch (groupInviteLink.getContentsCase()) {
|
||||||
|
case V1CONTENTS: {
|
||||||
|
GroupInviteLink.GroupInviteLinkContentsV1 groupInviteLinkContentsV1 = groupInviteLink.getV1Contents();
|
||||||
|
GroupMasterKey groupMasterKey = new GroupMasterKey(groupInviteLinkContentsV1.getGroupMasterKey().toByteArray());
|
||||||
|
GroupLinkPassword password = GroupLinkPassword.fromBytes(groupInviteLinkContentsV1.getInviteLinkPassword().toByteArray());
|
||||||
|
|
||||||
|
return new GroupInviteLinkUrl(groupMasterKey, password);
|
||||||
|
}
|
||||||
|
default: throw new UnknownGroupLinkVersionException("Url contains no known group link content");
|
||||||
|
}
|
||||||
|
} catch (GroupLinkPassword.InvalidLengthException | InvalidInputException | IOException e){
|
||||||
|
throw new InvalidGroupLinkException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private GroupInviteLinkUrl(@NonNull GroupMasterKey groupMasterKey, @NonNull GroupLinkPassword password) {
|
||||||
|
this.groupMasterKey = groupMasterKey;
|
||||||
|
this.password = password;
|
||||||
|
this.url = createUrl(groupMasterKey, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static @NonNull String createUrl(@NonNull GroupMasterKey groupMasterKey, @NonNull GroupLinkPassword password) {
|
||||||
|
GroupInviteLink groupInviteLink = GroupInviteLink.newBuilder()
|
||||||
|
.setV1Contents(GroupInviteLink.GroupInviteLinkContentsV1.newBuilder()
|
||||||
|
.setGroupMasterKey(ByteString.copyFrom(groupMasterKey.serialize()))
|
||||||
|
.setInviteLinkPassword(ByteString.copyFrom(password.serialize())))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
String encoding = Base64UrlSafe.encodeBytesWithoutPadding(groupInviteLink.toByteArray());
|
||||||
|
|
||||||
|
return GROUP_URL_PREFIX + encoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull String getUrl() {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull GroupMasterKey getGroupMasterKey() {
|
||||||
|
return groupMasterKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull GroupLinkPassword getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final static class InvalidGroupLinkException extends Exception {
|
||||||
|
public InvalidGroupLinkException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public InvalidGroupLinkException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final static class UnknownGroupLinkVersionException extends Exception {
|
||||||
|
public UnknownGroupLinkVersionException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
package org.thoughtcrime.securesms.groups.v2;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public final class GroupLinkPassword {
|
||||||
|
|
||||||
|
private static final int SIZE = 16;
|
||||||
|
|
||||||
|
private final byte[] bytes;
|
||||||
|
|
||||||
|
public static @NonNull GroupLinkPassword createNew() {
|
||||||
|
return new GroupLinkPassword(Util.getSecretBytes(SIZE));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @NonNull GroupLinkPassword fromBytes(@NonNull byte[] bytes) throws InvalidLengthException {
|
||||||
|
if (bytes.length != SIZE) {
|
||||||
|
throw new InvalidLengthException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GroupLinkPassword(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private GroupLinkPassword(@NonNull byte[] bytes) {
|
||||||
|
this.bytes = bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull byte[] serialize() {
|
||||||
|
return bytes.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object other) {
|
||||||
|
if (!(other instanceof GroupLinkPassword)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Arrays.equals(bytes, ((GroupLinkPassword) other).bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Arrays.hashCode(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class InvalidLengthException extends Exception {
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
package org.thoughtcrime.securesms.util;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.whispersystems.util.Base64;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public final class Base64UrlSafe {
|
||||||
|
|
||||||
|
private Base64UrlSafe() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @NonNull byte[] decode(@NonNull String s) throws IOException {
|
||||||
|
return Base64.decode(s, Base64.URL_SAFE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @NonNull byte[] decodePaddingAgnostic(@NonNull String s) throws IOException {
|
||||||
|
switch (s.length() % 4) {
|
||||||
|
case 1:
|
||||||
|
case 3: s = s + "="; break;
|
||||||
|
case 2: s = s + "=="; break;
|
||||||
|
}
|
||||||
|
return decode(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @NonNull String encodeBytes(@NonNull byte[] source) {
|
||||||
|
try {
|
||||||
|
return Base64.encodeBytes(source, Base64.URL_SAFE);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @NonNull String encodeBytesWithoutPadding(@NonNull byte[] source) {
|
||||||
|
return encodeBytes(source).replace("=", "");
|
||||||
|
}
|
||||||
|
}
|
@ -24,12 +24,17 @@ import org.thoughtcrime.securesms.R;
|
|||||||
import org.thoughtcrime.securesms.WebRtcCallActivity;
|
import org.thoughtcrime.securesms.WebRtcCallActivity;
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
|
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.joining.GroupJoinUpdateRequiredBottomSheetDialogFragment;
|
||||||
|
import org.thoughtcrime.securesms.groups.v2.GroupInviteLinkUrl;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
||||||
import org.thoughtcrime.securesms.service.WebRtcCallService;
|
import org.thoughtcrime.securesms.service.WebRtcCallService;
|
||||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||||
|
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||||
|
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||||
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
|
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
|
||||||
|
|
||||||
public class CommunicationActions {
|
public class CommunicationActions {
|
||||||
@ -162,6 +167,47 @@ public class CommunicationActions {
|
|||||||
context.startActivity(intent);
|
context.startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the url is a group link it will handle it.
|
||||||
|
* If the url is a malformed group link, it will assume Signal needs to update.
|
||||||
|
* Otherwise returns false, indicating was not a group link.
|
||||||
|
*/
|
||||||
|
public static boolean handlePotentialGroupLinkUrl(@NonNull FragmentActivity activity, @NonNull String potentialGroupLinkUrl) {
|
||||||
|
try {
|
||||||
|
GroupInviteLinkUrl groupInviteLinkUrl = GroupInviteLinkUrl.fromUrl(potentialGroupLinkUrl);
|
||||||
|
|
||||||
|
if (groupInviteLinkUrl == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleGroupLinkUrl(activity, groupInviteLinkUrl);
|
||||||
|
return true;
|
||||||
|
} catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
|
||||||
|
Log.w(TAG, "Could not parse group URL", e);
|
||||||
|
GroupJoinUpdateRequiredBottomSheetDialogFragment.show(activity.getSupportFragmentManager());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void handleGroupLinkUrl(@NonNull FragmentActivity activity,
|
||||||
|
@NonNull GroupInviteLinkUrl groupInviteLinkUrl)
|
||||||
|
{
|
||||||
|
GroupId.V2 groupId = GroupId.v2(groupInviteLinkUrl.getGroupMasterKey());
|
||||||
|
|
||||||
|
SimpleTask.run(SignalExecutors.BOUNDED, () ->
|
||||||
|
DatabaseFactory.getGroupDatabase(activity)
|
||||||
|
.getGroup(groupId)
|
||||||
|
.transform(groupRecord -> Recipient.resolved(groupRecord.getRecipientId()))
|
||||||
|
.orNull(),
|
||||||
|
recipient -> {
|
||||||
|
if (recipient != null) {
|
||||||
|
CommunicationActions.startConversation(activity, recipient, null);
|
||||||
|
Toast.makeText(activity, R.string.GroupJoinBottomSheetDialogFragment_you_are_already_a_member, Toast.LENGTH_SHORT).show();
|
||||||
|
} else {
|
||||||
|
GroupJoinUpdateRequiredBottomSheetDialogFragment.show(activity.getSupportFragmentManager());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private static void startInsecureCallInternal(@NonNull Activity activity, @NonNull Recipient recipient) {
|
private static void startInsecureCallInternal(@NonNull Activity activity, @NonNull Recipient recipient) {
|
||||||
try {
|
try {
|
||||||
|
@ -75,6 +75,14 @@ public class Hex {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static byte[] fromStringOrThrow(String encoded) {
|
||||||
|
try {
|
||||||
|
return fromStringCondensed(encoded);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static String dump(byte[] bytes) {
|
public static String dump(byte[] bytes) {
|
||||||
return dump(bytes, 0, bytes.length);
|
return dump(bytes, 0, bytes.length);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
package org.thoughtcrime.securesms.util;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.groups.v2.GroupInviteLinkUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Passes clicked Urls to the supplied {@link UrlClickHandler}.
|
||||||
|
*/
|
||||||
|
public final class InterceptableLongClickCopyLinkSpan extends LongClickCopySpan {
|
||||||
|
|
||||||
|
private final UrlClickHandler onClickListener;
|
||||||
|
|
||||||
|
public InterceptableLongClickCopyLinkSpan(@NonNull String url,
|
||||||
|
@NonNull UrlClickHandler onClickListener)
|
||||||
|
{
|
||||||
|
super(url);
|
||||||
|
this.onClickListener = onClickListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(View widget) {
|
||||||
|
if (!onClickListener.handleOnClick(getURL())) {
|
||||||
|
super.onClick(widget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
package org.thoughtcrime.securesms.util;
|
||||||
|
|
||||||
|
import android.content.ActivityNotFoundException;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.BuildConfig;
|
||||||
|
|
||||||
|
public final class PlayStoreUtil {
|
||||||
|
|
||||||
|
private PlayStoreUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void openPlayStoreOrOurApkDownloadPage(@NonNull Context context) {
|
||||||
|
if (BuildConfig.PLAY_STORE_DISABLED) {
|
||||||
|
CommunicationActions.openBrowserLink(context, "https://signal.org/android/apk");
|
||||||
|
} else {
|
||||||
|
openPlayStore(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void openPlayStore(@NonNull Context context) {
|
||||||
|
String packageName = context.getPackageName();
|
||||||
|
|
||||||
|
try {
|
||||||
|
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + packageName)));
|
||||||
|
} catch (ActivityNotFoundException e) {
|
||||||
|
CommunicationActions.openBrowserLink(context, "https://play.google.com/store/apps/details?id=" + packageName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package org.thoughtcrime.securesms.util;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
public interface UrlClickHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if you have handled it, false if you want to allow the standard Url handling.
|
||||||
|
*/
|
||||||
|
boolean handleOnClick(@NonNull String url);
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:theme="@style/Theme.Signal.RoundedBottomSheet.Light">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/group_join_update_title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/GroupJoinUpdateRequiredBottomSheetDialogFragment_update_signal_to_use_group_links"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Title2"
|
||||||
|
android:textColor="?title_text_color_primary"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/group_join_update_explain"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:text="@string/GroupJoinUpdateRequiredBottomSheetDialogFragment_update_message"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Body1"
|
||||||
|
android:textColor="?title_text_color_secondary"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/group_join_update_title" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/group_join_update_button"
|
||||||
|
style="@style/Button.Primary"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="64dp"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="32dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:text="@string/GroupJoinUpdateRequiredBottomSheetDialogFragment_update_signal"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0.5"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/group_join_update_explain"
|
||||||
|
app:layout_constraintVertical_bias="0" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -644,6 +644,14 @@
|
|||||||
<item quantity="other">Do you want to revoke %2$d invites sent by %1$s?</item>
|
<item quantity="other">Do you want to revoke %2$d invites sent by %1$s?</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
|
<!-- GroupJoinBottomSheetDialogFragment -->
|
||||||
|
<string name="GroupJoinBottomSheetDialogFragment_you_are_already_a_member">You are already a member</string>
|
||||||
|
|
||||||
|
<!-- GroupJoinUpdateRequiredBottomSheetDialogFragment -->
|
||||||
|
<string name="GroupJoinUpdateRequiredBottomSheetDialogFragment_update_signal_to_use_group_links">Update Signal to use group links</string>
|
||||||
|
<string name="GroupJoinUpdateRequiredBottomSheetDialogFragment_update_message">The version of Signal you’re using does not support sharable group links. Update to the latest version to join this group via link.</string>
|
||||||
|
<string name="GroupJoinUpdateRequiredBottomSheetDialogFragment_update_signal">Update Signal</string>
|
||||||
|
|
||||||
<!-- CropImageActivity -->
|
<!-- CropImageActivity -->
|
||||||
<string name="CropImageActivity_group_avatar">Group avatar</string>
|
<string name="CropImageActivity_group_avatar">Group avatar</string>
|
||||||
<string name="CropImageActivity_profile_avatar">Avatar</string>
|
<string name="CropImageActivity_profile_avatar">Avatar</string>
|
||||||
@ -1312,9 +1320,6 @@
|
|||||||
<!-- MuteDialog -->
|
<!-- MuteDialog -->
|
||||||
<string name="MuteDialog_mute_notifications">Mute notifications</string>
|
<string name="MuteDialog_mute_notifications">Mute notifications</string>
|
||||||
|
|
||||||
<!-- OutdatedBuildReminder -->
|
|
||||||
<string name="OutdatedBuildReminder_no_web_browser_installed">No web browser installed!</string>
|
|
||||||
|
|
||||||
<!-- ApplicationMigrationService -->
|
<!-- ApplicationMigrationService -->
|
||||||
<string name="ApplicationMigrationService_import_in_progress">Import in progress</string>
|
<string name="ApplicationMigrationService_import_in_progress">Import in progress</string>
|
||||||
<string name="ApplicationMigrationService_importing_text_messages">Importing text messages</string>
|
<string name="ApplicationMigrationService_importing_text_messages">Importing text messages</string>
|
||||||
|
@ -0,0 +1,101 @@
|
|||||||
|
package org.thoughtcrime.securesms.groups.v2;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.Parameterized;
|
||||||
|
import org.signal.zkgroup.InvalidInputException;
|
||||||
|
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||||
|
import org.thoughtcrime.securesms.util.Hex;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
@RunWith(Parameterized.class)
|
||||||
|
public final class GroupInviteLinkUrlTest {
|
||||||
|
|
||||||
|
private final GroupMasterKey groupMasterKey;
|
||||||
|
private final GroupLinkPassword password;
|
||||||
|
private final String expectedUrl;
|
||||||
|
|
||||||
|
@Parameterized.Parameters
|
||||||
|
public static Collection<Object[]> data() {
|
||||||
|
return Arrays.asList(
|
||||||
|
|
||||||
|
givenGroup().withMasterKey("a501335111fa04e3756c24b6eb87264e2dfb622e8e1d339179765410776c0488")
|
||||||
|
.andPassword("f08b7e22fb938c025e6c158b7d544956")
|
||||||
|
.expectUrl("https://group.signal.org/#CjQKIKUBM1ER-gTjdWwktuuHJk4t-2Iujh0zkXl2VBB3bASIEhDwi34i-5OMAl5sFYt9VElW"),
|
||||||
|
|
||||||
|
givenGroup().withMasterKey("2ca23c04d7cf60fe04039ae76d1912202c2a463d345d9cd48cf27f260dd37f6f")
|
||||||
|
.andPassword("2734457c02ce51da71ad0b62f3c222f7")
|
||||||
|
.expectUrl("https://group.signal.org/#CjQKICyiPATXz2D-BAOa520ZEiAsKkY9NF2c1IzyfyYN039vEhAnNEV8As5R2nGtC2LzwiL3"),
|
||||||
|
|
||||||
|
givenGroup().withMasterKey("00f7e0c2a71ab064cc3ced4c04f08d7b7ef4b84b2c2206f69833be6cfe34df80")
|
||||||
|
.andPassword("9bc324eec437cfda6ae5b8aefbf47ee8")
|
||||||
|
.expectUrl("https://group.signal.org/#CjQKIAD34MKnGrBkzDztTATwjXt-9LhLLCIG9pgzvmz-NN-AEhCbwyTuxDfP2mrluK779H7o")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GroupInviteLinkUrlTest(GroupMasterKey groupMasterKey, GroupLinkPassword password, String expectedUrl) {
|
||||||
|
this.groupMasterKey = groupMasterKey;
|
||||||
|
this.password = password;
|
||||||
|
this.expectedUrl = expectedUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void can_extract_group_master_key_from_url() throws GroupInviteLinkUrl.InvalidGroupLinkException, GroupInviteLinkUrl.UnknownGroupLinkVersionException {
|
||||||
|
assertEquals(groupMasterKey, GroupInviteLinkUrl.fromUrl(expectedUrl).getGroupMasterKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void can_extract_group_master_key_from_url_padded() throws GroupInviteLinkUrl.InvalidGroupLinkException, GroupInviteLinkUrl.UnknownGroupLinkVersionException {
|
||||||
|
assertEquals(groupMasterKey, GroupInviteLinkUrl.fromUrl(expectedUrl + "=").getGroupMasterKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void can_extract_password_from_url() throws GroupInviteLinkUrl.InvalidGroupLinkException, GroupInviteLinkUrl.UnknownGroupLinkVersionException {
|
||||||
|
assertEquals(password, GroupInviteLinkUrl.fromUrl(expectedUrl).getPassword());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void can_extract_password_from_url_padded() throws GroupInviteLinkUrl.InvalidGroupLinkException, GroupInviteLinkUrl.UnknownGroupLinkVersionException {
|
||||||
|
assertEquals(password, GroupInviteLinkUrl.fromUrl(expectedUrl + "=").getPassword());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void can_reconstruct_url() {
|
||||||
|
assertEquals(expectedUrl, GroupInviteLinkUrl.createUrl(groupMasterKey, password));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TestBuilder givenGroup() {
|
||||||
|
return new TestBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class TestBuilder {
|
||||||
|
private GroupMasterKey groupMasterKey;
|
||||||
|
private byte[] passwordBytes;
|
||||||
|
|
||||||
|
public TestBuilder withMasterKey(String groupMasterKeyAsHex) {
|
||||||
|
try {
|
||||||
|
groupMasterKey = new GroupMasterKey(Hex.fromStringOrThrow(groupMasterKeyAsHex));
|
||||||
|
} catch (InvalidInputException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TestBuilder andPassword(String passwordAsHex) {
|
||||||
|
passwordBytes = Hex.fromStringOrThrow(passwordAsHex);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object[] expectUrl(String url) {
|
||||||
|
try {
|
||||||
|
return new Object[]{ groupMasterKey, GroupLinkPassword.fromBytes(passwordBytes), url };
|
||||||
|
} catch (GroupLinkPassword.InvalidLengthException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,120 @@
|
|||||||
|
package org.thoughtcrime.securesms.groups.v2;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
import com.google.protobuf.InvalidProtocolBufferException;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.signal.storageservice.protos.groups.GroupInviteLink;
|
||||||
|
import org.signal.zkgroup.InvalidInputException;
|
||||||
|
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||||
|
import org.thoughtcrime.securesms.util.Base64UrlSafe;
|
||||||
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
|
||||||
|
public final class GroupInviteLinkUrl_InvalidGroupLinkException_Test {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void empty_string() throws GroupInviteLinkUrl.InvalidGroupLinkException, GroupInviteLinkUrl.UnknownGroupLinkVersionException {
|
||||||
|
assertNull(GroupInviteLinkUrl.fromUrl(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void not_a_url_string() throws GroupInviteLinkUrl.InvalidGroupLinkException, GroupInviteLinkUrl.UnknownGroupLinkVersionException {
|
||||||
|
assertNull(GroupInviteLinkUrl.fromUrl("abc"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void wrong_host() throws GroupInviteLinkUrl.InvalidGroupLinkException, GroupInviteLinkUrl.UnknownGroupLinkVersionException {
|
||||||
|
assertNull(GroupInviteLinkUrl.fromUrl("https://x.signal.org/#CAESNAogpQEzURH6BON1bCS264cmTi37Yi6OHTOReXZUEHdsBIgSEPCLfiL7k4wCXmwVi31USVY"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void has_path() {
|
||||||
|
assertThatThrownBy(() -> GroupInviteLinkUrl.fromUrl("https://group.signal.org/not_expected/#CAESNAogpQEzURH6BON1bCS264cmTi37Yi6OHTOReXZUEHdsBIgSEPCLfiL7k4wCXmwVi31USVY"))
|
||||||
|
.isInstanceOf(GroupInviteLinkUrl.InvalidGroupLinkException.class)
|
||||||
|
.hasMessage("No path was expected in url");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void missing_ref() {
|
||||||
|
assertThatThrownBy(() -> GroupInviteLinkUrl.fromUrl("https://group.signal.org/"))
|
||||||
|
.isInstanceOf(GroupInviteLinkUrl.InvalidGroupLinkException.class)
|
||||||
|
.hasMessage("No reference was in the url");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void empty_ref() {
|
||||||
|
assertThatThrownBy(() -> GroupInviteLinkUrl.fromUrl("https://group.signal.org/#"))
|
||||||
|
.isInstanceOf(GroupInviteLinkUrl.InvalidGroupLinkException.class)
|
||||||
|
.hasMessage("No reference was in the url");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void bad_base64() {
|
||||||
|
assertThatThrownBy(() -> GroupInviteLinkUrl.fromUrl("https://group.signal.org/#CAESNAogpQEzURH6BON1bCS264cmTi37Yi6HTOReXZUEHdsBIgSEPCLfiL7k4wCX;mwVi31USVY"))
|
||||||
|
.isInstanceOf(GroupInviteLinkUrl.InvalidGroupLinkException.class)
|
||||||
|
.hasCauseExactlyInstanceOf(IOException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void bad_protobuf() {
|
||||||
|
assertThatThrownBy(() -> GroupInviteLinkUrl.fromUrl("https://group.signal.org/#CAESNAogpQEzURH6BON1bCS264cmTi37Yi6HTOReXZUEHdsBIgSEPCLfiL7k4wCXmwVi31USVY"))
|
||||||
|
.isInstanceOf(GroupInviteLinkUrl.InvalidGroupLinkException.class)
|
||||||
|
.hasCauseExactlyInstanceOf(InvalidProtocolBufferException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void version_999_url() {
|
||||||
|
String url = "https://group.signal.org/#uj4zCiDMSxlNUvF4bQ3z3fYzGyZTFbJ1xEqWbPE3uZSD8bjOrxIP8NxV-0GUz3jpxMLR1rN3";
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> GroupInviteLinkUrl.fromUrl(url))
|
||||||
|
.isInstanceOf(GroupInviteLinkUrl.UnknownGroupLinkVersionException.class)
|
||||||
|
.hasMessage("Url contains no known group link content");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void bad_master_key_length() {
|
||||||
|
byte[] masterKeyBytes = Util.getSecretBytes(33);
|
||||||
|
GroupLinkPassword password = GroupLinkPassword.createNew();
|
||||||
|
|
||||||
|
String encoding = createEncodedProtobuf(masterKeyBytes, password.serialize());
|
||||||
|
|
||||||
|
String url = "https://group.signal.org/#" + encoding;
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> GroupInviteLinkUrl.fromUrl(url))
|
||||||
|
.isInstanceOf(GroupInviteLinkUrl.InvalidGroupLinkException.class)
|
||||||
|
.hasCauseExactlyInstanceOf(InvalidInputException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void bad_password_length() throws InvalidInputException {
|
||||||
|
GroupMasterKey groupMasterKey = new GroupMasterKey(Util.getSecretBytes(32));
|
||||||
|
byte[] passwordBytes = Util.getSecretBytes(15);
|
||||||
|
|
||||||
|
String encoding = createEncodedProtobuf(groupMasterKey.serialize(), passwordBytes);
|
||||||
|
|
||||||
|
String url = "https://group.signal.org/#" + encoding;
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> GroupInviteLinkUrl.fromUrl(url))
|
||||||
|
.isInstanceOf(GroupInviteLinkUrl.InvalidGroupLinkException.class)
|
||||||
|
.hasCauseExactlyInstanceOf(GroupLinkPassword.InvalidLengthException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String createEncodedProtobuf(@NonNull byte[] groupMasterKey,
|
||||||
|
@NonNull byte[] passwordBytes)
|
||||||
|
{
|
||||||
|
return Base64UrlSafe.encodeBytesWithoutPadding(GroupInviteLink.newBuilder()
|
||||||
|
.setV1Contents(GroupInviteLink.GroupInviteLinkContentsV1.newBuilder()
|
||||||
|
.setGroupMasterKey(ByteString.copyFrom(groupMasterKey))
|
||||||
|
.setInviteLinkPassword(ByteString.copyFrom(passwordBytes)))
|
||||||
|
.build()
|
||||||
|
.toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
package org.thoughtcrime.securesms.util;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.Parameterized;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
@RunWith(Parameterized.class)
|
||||||
|
public final class Base64UrlSafeTest {
|
||||||
|
|
||||||
|
private final byte[] data;
|
||||||
|
private final String encoded;
|
||||||
|
private final String encodedWithoutPadding;
|
||||||
|
|
||||||
|
@Parameterized.Parameters
|
||||||
|
public static Collection<Object[]> data() {
|
||||||
|
return Arrays.asList(new Object[][]{
|
||||||
|
{ "", "", "" },
|
||||||
|
{ "01", "AQ==", "AQ" },
|
||||||
|
{ "0102", "AQI=", "AQI" },
|
||||||
|
{ "010203", "AQID", "AQID" },
|
||||||
|
{ "030405", "AwQF", "AwQF" },
|
||||||
|
{ "03040506", "AwQFBg==", "AwQFBg" },
|
||||||
|
{ "0304050708", "AwQFBwg=", "AwQFBwg" },
|
||||||
|
{ "af4d6cff", "r01s_w==", "r01s_w" },
|
||||||
|
{ "ffefde", "_-_e", "_-_e" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Base64UrlSafeTest(String hexData, String encoded, String encodedWithoutPadding) throws IOException {
|
||||||
|
this.data = Hex.fromStringCondensed(hexData);
|
||||||
|
this.encoded = encoded;
|
||||||
|
this.encodedWithoutPadding = encodedWithoutPadding;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void encodes_as_expected() {
|
||||||
|
assertEquals(encoded, Base64UrlSafe.encodeBytes(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void encodes_as_expected_without_padding() {
|
||||||
|
assertEquals(encodedWithoutPadding, Base64UrlSafe.encodeBytesWithoutPadding(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void decodes_as_expected() throws IOException {
|
||||||
|
assertArrayEquals(data, Base64UrlSafe.decode(encoded));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void decodes_padding_agnostic_as_expected() throws IOException {
|
||||||
|
assertArrayEquals(data, Base64UrlSafe.decodePaddingAgnostic(encoded));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void decodes_as_expected_without_padding() throws IOException {
|
||||||
|
assertArrayEquals(data, Base64UrlSafe.decodePaddingAgnostic(encodedWithoutPadding));
|
||||||
|
}
|
||||||
|
}
|
@ -150,3 +150,14 @@ message GroupAttributeBlob {
|
|||||||
uint32 disappearingMessagesDuration = 3;
|
uint32 disappearingMessagesDuration = 3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message GroupInviteLink {
|
||||||
|
message GroupInviteLinkContentsV1 {
|
||||||
|
bytes groupMasterKey = 1;
|
||||||
|
bytes inviteLinkPassword = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
oneof contents {
|
||||||
|
GroupInviteLinkContentsV1 v1Contents = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user