From e4456bb2365a036b73a74b60257d944e9d5bdcad Mon Sep 17 00:00:00 2001 From: Alan Evans Date: Thu, 13 Aug 2020 16:39:43 -0300 Subject: [PATCH] Handle GV2 addresses. --- app/src/main/AndroidManifest.xml | 8 ++ .../securesms/BindableConversationItem.java | 7 +- .../thoughtcrime/securesms/MainActivity.java | 18 +++ .../reminder/ExpiredBuildReminder.java | 20 +-- .../reminder/OutdatedBuildReminder.java | 22 +--- .../conversation/ConversationFragment.java | 5 + .../conversation/ConversationItem.java | 27 +++- ...dateRequiredBottomSheetDialogFragment.java | 55 ++++++++ .../groups/v2/GroupInviteLinkUrl.java | 115 +++++++++++++++++ .../groups/v2/GroupLinkPassword.java | 51 ++++++++ .../securesms/util/Base64UrlSafe.java | 38 ++++++ .../securesms/util/CommunicationActions.java | 46 +++++++ .../org/thoughtcrime/securesms/util/Hex.java | 8 ++ .../InterceptableLongClickCopyLinkSpan.java | 29 +++++ .../securesms/util/PlayStoreUtil.java | 34 +++++ .../securesms/util/UrlClickHandler.java | 11 ++ .../group_join_update_needed_bottom_sheet.xml | 52 ++++++++ app/src/main/res/values/strings.xml | 11 +- .../groups/v2/GroupInviteLinkUrlTest.java | 101 +++++++++++++++ ...inkUrl_InvalidGroupLinkException_Test.java | 120 ++++++++++++++++++ .../securesms/util/Base64UrlSafeTest.java | 66 ++++++++++ libsignal/service/src/main/proto/Groups.proto | 11 ++ 22 files changed, 806 insertions(+), 49 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/joining/GroupJoinUpdateRequiredBottomSheetDialogFragment.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/groups/v2/GroupInviteLinkUrl.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/groups/v2/GroupLinkPassword.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/util/Base64UrlSafe.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/util/InterceptableLongClickCopyLinkSpan.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/util/PlayStoreUtil.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/util/UrlClickHandler.java create mode 100644 app/src/main/res/layout/group_join_update_needed_bottom_sheet.xml create mode 100644 app/src/test/java/org/thoughtcrime/securesms/groups/v2/GroupInviteLinkUrlTest.java create mode 100644 app/src/test/java/org/thoughtcrime/securesms/groups/v2/GroupInviteLinkUrl_InvalidGroupLinkException_Test.java create mode 100644 app/src/test/java/org/thoughtcrime/securesms/util/Base64UrlSafeTest.java 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 @@ + + + + + + + +