diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 12f1b2c122..569a2be870 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -223,6 +223,14 @@
+
+
+
+
+
+
+
{
- 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();
- }
- }
- });
+ setOkListener(v -> PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(context));
}
@Override
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/OutdatedBuildReminder.java b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/OutdatedBuildReminder.java
index f738b922f6..28477d003a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/OutdatedBuildReminder.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/OutdatedBuildReminder.java
@@ -1,35 +1,17 @@
package org.thoughtcrime.securesms.components.reminder;
-import android.content.ActivityNotFoundException;
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.util.PlayStoreUtil;
import org.thoughtcrime.securesms.util.Util;
public class OutdatedBuildReminder extends Reminder {
- private static final String TAG = OutdatedBuildReminder.class.getSimpleName();
-
public OutdatedBuildReminder(final Context context) {
super(context.getString(R.string.reminder_header_outdated_build),
getPluralsText(context));
- setOkListener(v -> {
- 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();
- }
- }
- });
+ setOkListener(v -> PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(context));
}
private static CharSequence getPluralsText(final Context context) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java
index f9d5457c0c..dc1a7a4945 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java
@@ -1388,6 +1388,11 @@ public class ConversationFragment extends LoggingFragment {
public void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord) {
listener.onMessageWithErrorClicked(messageRecord);
}
+
+ @Override
+ public boolean onUrlClicked(@NonNull String url) {
+ return CommunicationActions.handlePotentialGroupLinkUrl(requireActivity(), url);
+ }
}
@Override
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java
index d338509f73..6468a8361e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java
@@ -110,12 +110,13 @@ import org.thoughtcrime.securesms.revealable.ViewOnceUtil;
import org.thoughtcrime.securesms.stickers.StickerUrl;
import org.thoughtcrime.securesms.util.DateUtils;
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.SearchUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.VibrateUtil;
+import org.thoughtcrime.securesms.util.UrlClickHandler;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.Stub;
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 LinkPreviewClickListener linkPreviewClickListener = new LinkPreviewClickListener();
private final ViewOnceMessageClickListener revealableClickListener = new ViewOnceMessageClickListener();
+ private final UrlClickListener urlClickListener = new UrlClickListener();
private final Context context;
@@ -580,7 +582,9 @@ public class ConversationItem extends LinearLayout implements BindableConversati
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.setFocusable(false);
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);
}
- 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;
boolean hasLinks = Linkify.addLinks(messageBody, shouldLinkifyAllLinks ? linkPattern : 0);
@@ -928,9 +934,10 @@ public class ConversationItem extends LinearLayout implements BindableConversati
URLSpan[] urlSpans = messageBody.getSpans(0, messageBody.length(), URLSpan.class);
for (URLSpan urlSpan : urlSpans) {
- int start = messageBody.getSpanStart(urlSpan);
- int end = messageBody.getSpanEnd(urlSpan);
- messageBody.setSpan(new LongClickCopySpan(urlSpan.getURL()), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ int start = messageBody.getSpanStart(urlSpan);
+ int end = messageBody.getSpanEnd(urlSpan);
+ 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 final RecipientId mentionedRecipientId;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/joining/GroupJoinUpdateRequiredBottomSheetDialogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/joining/GroupJoinUpdateRequiredBottomSheetDialogFragment.java
new file mode 100644
index 0000000000..583a007f22
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/joining/GroupJoinUpdateRequiredBottomSheetDialogFragment.java
@@ -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);
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/GroupInviteLinkUrl.java b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/GroupInviteLinkUrl.java
new file mode 100644
index 0000000000..d4f273530b
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/GroupInviteLinkUrl.java
@@ -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);
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/GroupLinkPassword.java b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/GroupLinkPassword.java
new file mode 100644
index 0000000000..a835daa1c5
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/GroupLinkPassword.java
@@ -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 {
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Base64UrlSafe.java b/app/src/main/java/org/thoughtcrime/securesms/util/Base64UrlSafe.java
new file mode 100644
index 0000000000..d2eac3be23
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/Base64UrlSafe.java
@@ -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("=", "");
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java b/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java
index 507194f4cf..937e76d7e6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java
@@ -24,12 +24,17 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.WebRtcCallActivity;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
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.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.WebRtcCallService;
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;
public class CommunicationActions {
@@ -162,6 +167,47 @@ public class CommunicationActions {
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) {
try {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Hex.java b/app/src/main/java/org/thoughtcrime/securesms/util/Hex.java
index a2142fbbf5..a1f3af4cad 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/Hex.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/Hex.java
@@ -75,6 +75,14 @@ public class Hex {
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) {
return dump(bytes, 0, bytes.length);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/InterceptableLongClickCopyLinkSpan.java b/app/src/main/java/org/thoughtcrime/securesms/util/InterceptableLongClickCopyLinkSpan.java
new file mode 100644
index 0000000000..04df7eb3a1
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/InterceptableLongClickCopyLinkSpan.java
@@ -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);
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/PlayStoreUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/PlayStoreUtil.java
new file mode 100644
index 0000000000..80df712ea3
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/PlayStoreUtil.java
@@ -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);
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/UrlClickHandler.java b/app/src/main/java/org/thoughtcrime/securesms/util/UrlClickHandler.java
new file mode 100644
index 0000000000..b21a0936c2
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/UrlClickHandler.java
@@ -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);
+}
diff --git a/app/src/main/res/layout/group_join_update_needed_bottom_sheet.xml b/app/src/main/res/layout/group_join_update_needed_bottom_sheet.xml
new file mode 100644
index 0000000000..2298d37097
--- /dev/null
+++ b/app/src/main/res/layout/group_join_update_needed_bottom_sheet.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 083c8c47c7..a7995b5f71 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -644,6 +644,14 @@
- Do you want to revoke %2$d invites sent by %1$s?
+
+ You are already a member
+
+
+ Update Signal to use group links
+ The version of Signal you’re using does not support sharable group links. Update to the latest version to join this group via link.
+ Update Signal
+
Group avatar
Avatar
@@ -1312,9 +1320,6 @@
Mute notifications
-
- No web browser installed!
-
Import in progress
Importing text messages
diff --git a/app/src/test/java/org/thoughtcrime/securesms/groups/v2/GroupInviteLinkUrlTest.java b/app/src/test/java/org/thoughtcrime/securesms/groups/v2/GroupInviteLinkUrlTest.java
new file mode 100644
index 0000000000..9fa3a7fc91
--- /dev/null
+++ b/app/src/test/java/org/thoughtcrime/securesms/groups/v2/GroupInviteLinkUrlTest.java
@@ -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