From e9e2846532ed09b616edd23bbc7a57469d873329 Mon Sep 17 00:00:00 2001
From: Greyson Parrelli
Date: Sat, 30 Jan 2021 11:43:12 -0500
Subject: [PATCH 01/17] Force custom emojis for about views.
---
.../securesms/components/emoji/EmojiEditText.java | 8 +++++++-
app/src/main/res/layout/contact_selection_list_item.xml | 2 ++
app/src/main/res/layout/conversation_banner_view.xml | 3 ++-
app/src/main/res/layout/edit_about_fragment.xml | 3 ++-
app/src/main/res/layout/group_recipient_list_item.xml | 1 +
app/src/main/res/layout/manage_profile_fragment.xml | 1 +
app/src/main/res/layout/recipient_bottom_sheet.xml | 1 +
app/src/main/res/layout/recipient_manage_fragment.xml | 1 +
8 files changed, 17 insertions(+), 3 deletions(-)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiEditText.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiEditText.java
index ebed6ed1404..617c830e5f6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiEditText.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiEditText.java
@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
+import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.text.InputFilter;
import android.util.AttributeSet;
@@ -27,7 +28,12 @@ public EmojiEditText(Context context, AttributeSet attrs) {
public EmojiEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
- if (!TextSecurePreferences.isSystemEmojiPreferred(getContext())) {
+
+ TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiTextView, 0, 0);
+ boolean forceCustom = a.getBoolean(R.styleable.EmojiTextView_emoji_forceCustom, false);
+ a.recycle();
+
+ if (forceCustom || !TextSecurePreferences.isSystemEmojiPreferred(getContext())) {
setFilters(appendEmojiFilter(this.getFilters()));
}
}
diff --git a/app/src/main/res/layout/contact_selection_list_item.xml b/app/src/main/res/layout/contact_selection_list_item.xml
index 741e9db935d..38ba67b76b9 100644
--- a/app/src/main/res/layout/contact_selection_list_item.xml
+++ b/app/src/main/res/layout/contact_selection_list_item.xml
@@ -1,6 +1,7 @@
+ app:layout_constraintTop_toBottomOf="@id/message_request_title"
+ app:emoji_forceCustom="true"/>
+ app:layout_constraintEnd_toStartOf="@id/edit_about_clear"
+ app:emoji_forceCustom="true"/>
Date: Mon, 1 Feb 2021 10:23:22 -0500
Subject: [PATCH 02/17] Fix issue where reaction shade is offset in chat
bubbles.
Fixes #10843
---
.../securesms/conversation/ConversationFragment.java | 5 ++++-
.../conversation/ConversationReactionOverlay.java | 2 +-
.../org/thoughtcrime/securesms/util/WindowUtil.java | 11 +++++++++++
3 files changed, 16 insertions(+), 2 deletions(-)
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 4df61b7a0ce..57bf93c9a9b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java
@@ -23,6 +23,7 @@
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
+import android.graphics.Rect;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
@@ -309,7 +310,9 @@ private void setListVerticalTranslation() {
list.setTranslationY(Math.min(0, -chTop));
list.setOverScrollMode(RecyclerView.OVER_SCROLL_NEVER);
}
- listener.onListVerticalTranslationChanged(list.getTranslationY());
+
+ int offset = WindowUtil.isStatusBarPresent(requireActivity().getWindow()) ? ViewUtil.getStatusBarHeight(list) : 0;
+ listener.onListVerticalTranslationChanged(list.getTranslationY() - offset);
}
@Override
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationReactionOverlay.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationReactionOverlay.java
index 5ec83cf0926..1ab1d546db0 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationReactionOverlay.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationReactionOverlay.java
@@ -141,7 +141,7 @@ protected void onFinishInflate() {
}
public void setListVerticalTranslation(float translationY) {
- maskView.setTargetParentTranslationY(translationY - ViewUtil.getStatusBarHeight(maskView));
+ maskView.setTargetParentTranslationY(translationY);
}
public void show(@NonNull Activity activity,
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/WindowUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/WindowUtil.java
index cec0e26cfe1..2c737ebff88 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/WindowUtil.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/WindowUtil.java
@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.util;
import android.app.Activity;
+import android.graphics.Rect;
import android.os.Build;
import android.view.View;
import android.view.Window;
@@ -62,6 +63,16 @@ public static void setStatusBarColor(@NonNull Window window, @ColorInt int color
window.setStatusBarColor(color);
}
+ /**
+ * A sort of roundabout way of determining if the status bar is present by seeing if there's a
+ * vertical window offset.
+ */
+ public static boolean isStatusBarPresent(@NonNull Window window) {
+ Rect rectangle = new Rect();
+ window.getDecorView().getWindowVisibleDisplayFrame(rectangle);
+ return rectangle.top > 0;
+ }
+
private static void clearSystemUiFlags(@NonNull Window window, int flags) {
View view = window.getDecorView();
int uiFlags = view.getSystemUiVisibility();
From 589f3458257a3b0de1af5dea56f6822a8fce43a8 Mon Sep 17 00:00:00 2001
From: Alan Evans
Date: Mon, 1 Feb 2021 12:47:31 -0400
Subject: [PATCH 03/17] Fix unnecessary zeros padding.
---
.../registration/fragments/WelcomeFragment.java | 12 +-----------
1 file changed, 1 insertion(+), 11 deletions(-)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/WelcomeFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/WelcomeFragment.java
index 073319b1800..8b8ca00ba73 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/WelcomeFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/WelcomeFragment.java
@@ -34,9 +34,6 @@
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
-import java.util.Arrays;
-import java.util.Locale;
-
public final class WelcomeFragment extends BaseRegistrationFragment {
private static final String TAG = Log.tag(WelcomeFragment.class);
@@ -196,14 +193,7 @@ private void initializeNumber() {
if (localNumber.isPresent()) {
Log.i(TAG, "Phone number detected");
Phonenumber.PhoneNumber phoneNumber = localNumber.get();
- String nationalNumber = String.valueOf(phoneNumber.getNationalNumber());
-
- if (phoneNumber.getNumberOfLeadingZeros() != 0) {
- char[] value = new char[phoneNumber.getNumberOfLeadingZeros()];
- Arrays.fill(value, '0');
- nationalNumber = new String(value) + nationalNumber;
- Log.i(TAG, String.format(Locale.US, "Padded national number with %d zeros", phoneNumber.getNumberOfLeadingZeros()));
- }
+ String nationalNumber = PhoneNumberUtil.getInstance().format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.NATIONAL);
getModel().onNumberDetected(phoneNumber.getCountryCode(), nationalNumber);
} else {
From dcfa7e3b365ffbd763db38fc3003c297e998f30a Mon Sep 17 00:00:00 2001
From: Cody Henthorne
Date: Mon, 1 Feb 2021 11:58:33 -0500
Subject: [PATCH 04/17] Allow contact support from registration lock and screen
lock screens.
---
.../securesms/PassphrasePromptActivity.java | 23 +++++++++++++++---
.../fragments/RegistrationLockFragment.java | 24 +++++++++++++++++--
.../{log_submit.xml => passphrase_prompt.xml} | 3 +++
app/src/main/res/values/strings.xml | 3 +++
4 files changed, 48 insertions(+), 5 deletions(-)
rename app/src/main/res/menu/{log_submit.xml => passphrase_prompt.xml} (69%)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java b/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java
index 23a19dc753b..5509d40abac 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java
@@ -57,8 +57,10 @@
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity;
+import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.DynamicIntroTheme;
import org.thoughtcrime.securesms.util.DynamicLanguage;
+import org.thoughtcrime.securesms.util.SupportEmailUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
/**
@@ -137,7 +139,7 @@ public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = this.getMenuInflater();
menu.clear();
- inflater.inflate(R.menu.log_submit, menu);
+ inflater.inflate(R.menu.passphrase_prompt, menu);
super.onCreateOptionsMenu(menu);
return true;
@@ -146,8 +148,12 @@ public boolean onCreateOptionsMenu(Menu menu) {
@Override
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
- switch (item.getItemId()) {
- case R.id.menu_submit_debug_logs: handleLogSubmit(); return true;
+ if (item.getItemId() == R.id.menu_submit_debug_logs) {
+ handleLogSubmit();
+ return true;
+ } else if (item.getItemId() == R.id.menu_contact_support) {
+ sendEmailToSupport();
+ return true;
}
return false;
@@ -294,6 +300,17 @@ private void pauseScreenLock() {
}
}
+ private void sendEmailToSupport() {
+ String body = SupportEmailUtil.generateSupportEmailBody(this,
+ R.string.PassphrasePromptActivity_signal_android_lock_screen,
+ null,
+ null);
+ CommunicationActions.openEmail(this,
+ SupportEmailUtil.getSupportEmailAddress(this),
+ getString(R.string.PassphrasePromptActivity_signal_android_lock_screen),
+ body);
+ }
+
private class PassphraseActionListener implements TextView.OnEditorActionListener {
@Override
public boolean onEditorAction(TextView exampleView, int actionId, KeyEvent keyEvent) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationLockFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationLockFragment.java
index 8891b9d87c5..2596cf193b0 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationLockFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationLockFragment.java
@@ -29,7 +29,9 @@
import org.thoughtcrime.securesms.registration.service.CodeVerificationRequest;
import org.thoughtcrime.securesms.registration.service.RegistrationService;
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
+import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.ServiceUtil;
+import org.thoughtcrime.securesms.util.SupportEmailUtil;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import java.util.concurrent.TimeUnit;
@@ -47,6 +49,7 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment {
private TextView errorLabel;
private TextView keyboardToggle;
private long timeRemaining;
+ private boolean isV1RegistrationLock;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@@ -67,9 +70,10 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
RegistrationLockFragmentArgs args = RegistrationLockFragmentArgs.fromBundle(requireArguments());
- timeRemaining = args.getTimeRemaining();
+ timeRemaining = args.getTimeRemaining();
+ isV1RegistrationLock = args.getIsV1RegistrationLock();
- if (args.getIsV1RegistrationLock()) {
+ if (isV1RegistrationLock) {
keyboardToggle.setVisibility(View.GONE);
}
@@ -117,6 +121,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
.setTitle(R.string.RegistrationLockFragment__not_many_tries_left)
.setMessage(getTriesRemainingDialogMessage(triesRemaining, daysRemaining))
.setPositiveButton(android.R.string.ok, null)
+ .setNeutralButton(R.string.PinRestoreEntryFragment_contact_support, (dialog, which) -> sendEmailToSupport())
.show();
}
@@ -264,6 +269,7 @@ private void handleForgottenPin(long timeRemainingMs) {
.setTitle(R.string.RegistrationLockFragment__forgot_your_pin)
.setMessage(requireContext().getResources().getQuantityString(R.plurals.RegistrationLockFragment__for_your_privacy_and_security_there_is_no_way_to_recover, lockoutDays, lockoutDays))
.setPositiveButton(android.R.string.ok, null)
+ .setNeutralButton(R.string.PinRestoreEntryFragment_contact_support, (dialog, which) -> sendEmailToSupport())
.show();
}
@@ -320,4 +326,18 @@ private void handleSuccessfulPinEntry() {
Navigation.findNavController(requireView()).navigate(RegistrationLockFragmentDirections.actionSuccessfulRegistration());
});
}
+
+ private void sendEmailToSupport() {
+ int subject = isV1RegistrationLock ? R.string.RegistrationLockFragment__signal_registration_need_help_with_pin_for_android_v1_pin
+ : R.string.RegistrationLockFragment__signal_registration_need_help_with_pin_for_android_v2_pin;
+
+ String body = SupportEmailUtil.generateSupportEmailBody(requireContext(),
+ subject,
+ null,
+ null);
+ CommunicationActions.openEmail(requireContext(),
+ SupportEmailUtil.getSupportEmailAddress(requireContext()),
+ getString(subject),
+ body);
+ }
}
diff --git a/app/src/main/res/menu/log_submit.xml b/app/src/main/res/menu/passphrase_prompt.xml
similarity index 69%
rename from app/src/main/res/menu/log_submit.xml
rename to app/src/main/res/menu/passphrase_prompt.xml
index abd321d48da..a3ee2a9b9a6 100644
--- a/app/src/main/res/menu/log_submit.xml
+++ b/app/src/main/res/menu/passphrase_prompt.xml
@@ -4,4 +4,7 @@
+
+
\ 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 86013f71e4a..4580b949403 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1278,6 +1278,7 @@
Submit passphrase
Invalid passphrase!
Unlock Signal
+ Signal Android - Lock Screen
Map
@@ -2563,6 +2564,8 @@
Incorrect PIN
Forgot your PIN?
Not many tries left!
+ Signal Registration - Need Help with PIN for Android (v1 PIN)
+ Signal Registration - Need Help with PIN for Android (v2 PIN)
- For your privacy and security, there is no way to recover your PIN. If you can\'t remember your PIN, you can re-verify with SMS after %1$d day of inactivity. In this case, your account will be wiped and all content deleted.
From 904593c1039baaee8c17a7de071087e54d6a2d0d Mon Sep 17 00:00:00 2001
From: Alan Evans
Date: Mon, 1 Feb 2021 13:01:29 -0400
Subject: [PATCH 05/17] Add additional logging for conflict resolution.
---
.../securesms/groups/GroupManagerV2.java | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java
index 4a74f8b944e..c9a95ed195f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java
@@ -75,6 +75,7 @@
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
@@ -578,7 +579,16 @@ private GroupChange.Actions.Builder resolveConflict(@NonNull GroupChange.Actions
GroupsV2StateProcessor.GroupUpdateResult groupUpdateResult = groupsV2StateProcessor.forGroup(groupMasterKey)
.updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, System.currentTimeMillis(), null);
- if (groupUpdateResult.getGroupState() != GroupsV2StateProcessor.GroupState.GROUP_UPDATED || groupUpdateResult.getLatestServer() == null) {
+ if (groupUpdateResult.getLatestServer() == null) {
+ Log.w(TAG, "Latest server state null.");
+ throw new GroupChangeFailedException();
+ }
+
+ if (groupUpdateResult.getGroupState() != GroupsV2StateProcessor.GroupState.GROUP_UPDATED) {
+ int serverRevision = groupUpdateResult.getLatestServer().getRevision();
+ int localRevision = groupDatabase.requireGroup(groupId).requireV2GroupProperties().getGroupRevision();
+ int revisionDelta = serverRevision - localRevision;
+ Log.w(TAG, String.format(Locale.US, "Server is ahead by %d revisions", revisionDelta));
throw new GroupChangeFailedException();
}
From 857b9454109e70c601beb5253e899bbf72b4a2a5 Mon Sep 17 00:00:00 2001
From: Greyson Parrelli
Date: Mon, 1 Feb 2021 18:04:52 -0500
Subject: [PATCH 06/17] Fix storage sync issue related to duplicate remote
contacts.
The theory is that if multiple remote keys map to the *same* local
entry, then when we go to update the local contact the second time, we
won't find the entry by StorageID, because we changed it during the
*first* update, which will then lead to a crash.
This change makes it so dupes are considered invalid, so we'll delete
them and upload our own local copy.
---
.../storage/ContactConflictMerger.java | 41 +++++++++++++++++--
.../storage/ContactConflictMergerTest.java | 23 +++++++++++
2 files changed, 60 insertions(+), 4 deletions(-)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/ContactConflictMerger.java b/app/src/main/java/org/thoughtcrime/securesms/storage/ContactConflictMerger.java
index d488bb04db7..414f7c993d0 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/storage/ContactConflictMerger.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/storage/ContactConflictMerger.java
@@ -7,6 +7,7 @@
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.util.Base64;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
@@ -15,9 +16,11 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Set;
import java.util.UUID;
class ContactConflictMerger implements StorageSyncHelper.ConflictMerger {
@@ -52,11 +55,41 @@ class ContactConflictMerger implements StorageSyncHelper.ConflictMerger getInvalidEntries(@NonNull Collection remoteRecords) {
- List invalid = Stream.of(remoteRecords)
- .filter(r -> r.getAddress().getUuid().equals(self.getUuid()) || r.getAddress().getNumber().equals(self.getE164()))
- .toList();
+ Map> localIdToRemoteRecords = new HashMap<>();
+
+ for (SignalContactRecord remote : remoteRecords) {
+ Optional local = getMatching(remote);
+
+ if (local.isPresent()) {
+ String serializedLocalId = Base64.encodeBytes(local.get().getId().getRaw());
+ Set matches = localIdToRemoteRecords.get(serializedLocalId);
+
+ if (matches == null) {
+ matches = new HashSet<>();
+ }
+
+ matches.add(remote);
+ localIdToRemoteRecords.put(serializedLocalId, matches);
+ }
+ }
+
+ Set duplicates = new HashSet<>();
+ for (Set matches : localIdToRemoteRecords.values()) {
+ if (matches.size() > 1) {
+ duplicates.addAll(matches);
+ }
+ }
+
+ List selfRecords = Stream.of(remoteRecords)
+ .filter(r -> r.getAddress().getUuid().equals(self.getUuid()) || r.getAddress().getNumber().equals(self.getE164()))
+ .toList();
+
+ Set invalid = new HashSet<>();
+ invalid.addAll(selfRecords);
+ invalid.addAll(duplicates);
+
if (invalid.size() > 0) {
- Log.w(TAG, "Found invalid contact entries! Count: " + invalid.size());
+ Log.w(TAG, "Found invalid contact entries! Self Records: " + selfRecords.size() + ", Duplicates: " + duplicates.size());
}
return invalid;
diff --git a/app/src/test/java/org/thoughtcrime/securesms/storage/ContactConflictMergerTest.java b/app/src/test/java/org/thoughtcrime/securesms/storage/ContactConflictMergerTest.java
index e37890a0376..5dfd65da4d0 100644
--- a/app/src/test/java/org/thoughtcrime/securesms/storage/ContactConflictMergerTest.java
+++ b/app/src/test/java/org/thoughtcrime/securesms/storage/ContactConflictMergerTest.java
@@ -150,6 +150,16 @@ public void merge_returnLocalIfEndResultMatchesLocal() {
assertEquals(local, merged);
}
+ @Test
+ public void getInvalidEntries_nothingInvalid() {
+ SignalContactRecord a = new SignalContactRecord.Builder(byteArray(1), new SignalServiceAddress(UUID_A, E164_A)).build();
+ SignalContactRecord b = new SignalContactRecord.Builder(byteArray(2), new SignalServiceAddress(UUID_B, E164_B)).build();
+
+ Collection invalid = new ContactConflictMerger(Collections.emptyList(), SELF).getInvalidEntries(setOf(a, b));
+
+ assertContentsEqual(setOf(), invalid);
+ }
+
@Test
public void getInvalidEntries_selfIsInvalid() {
SignalContactRecord a = new SignalContactRecord.Builder(byteArray(1), new SignalServiceAddress(UUID_A, E164_A)).build();
@@ -160,4 +170,17 @@ public void getInvalidEntries_selfIsInvalid() {
assertContentsEqual(setOf(self), invalid);
}
+
+ @Test
+ public void getInvalidEntries_duplicatesInvalid() {
+ SignalContactRecord aLocal = new SignalContactRecord.Builder(byteArray(1), new SignalServiceAddress(UUID_A, E164_A)).build();
+ SignalContactRecord bRemote = new SignalContactRecord.Builder(byteArray(2), new SignalServiceAddress(UUID_B, E164_B)).build();
+ SignalContactRecord aRemote1 = new SignalContactRecord.Builder(byteArray(3), new SignalServiceAddress(UUID_A, null)).build();
+ SignalContactRecord aRemote2 = new SignalContactRecord.Builder(byteArray(4), new SignalServiceAddress(null, E164_A)).build();
+ SignalContactRecord aRemote3 = new SignalContactRecord.Builder(byteArray(5), new SignalServiceAddress(UUID_A, E164_A)).build();
+
+ Collection invalid = new ContactConflictMerger(Collections.singleton(aLocal), SELF).getInvalidEntries(setOf(aRemote1, aRemote2, aRemote3, bRemote));
+
+ assertContentsEqual(setOf(aRemote1, aRemote2, aRemote3), invalid);
+ }
}
From 53177bf40e1c0b8f004938ceec19bcd7bcf4d79c Mon Sep 17 00:00:00 2001
From: Greyson Parrelli
Date: Mon, 1 Feb 2021 20:56:25 -0500
Subject: [PATCH 07/17] Clean up unnecessary GCM stuff, improve FCM logging.
---
app/src/main/AndroidManifest.xml | 1 -
.../securesms/gcm/FcmReceiveService.java | 13 ++++++++++++-
2 files changed, 12 insertions(+), 2 deletions(-)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f55961a7920..db55855b0cc 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -64,7 +64,6 @@
-
diff --git a/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmReceiveService.java b/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmReceiveService.java
index b24411c1770..9568973181f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmReceiveService.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmReceiveService.java
@@ -18,9 +18,10 @@ public class FcmReceiveService extends FirebaseMessagingService {
private static final String TAG = FcmReceiveService.class.getSimpleName();
+
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
- Log.i(TAG, "onMessageReceived() ID: " + remoteMessage.getMessageId() + ", Delay: " + (System.currentTimeMillis() - remoteMessage.getSentTime()));
+ Log.i(TAG, "onMessageReceived() ID: " + remoteMessage.getMessageId() + ", Delay: " + (System.currentTimeMillis() - remoteMessage.getSentTime()) + ", Original Priority: " + remoteMessage.getOriginalPriority());
String challenge = remoteMessage.getData().get("challenge");
if (challenge != null) {
@@ -48,6 +49,16 @@ public void onNewToken(String token) {
ApplicationDependencies.getJobManager().add(new FcmRefreshJob());
}
+ @Override
+ public void onMessageSent(@NonNull String s) {
+ Log.i(TAG, "onMessageSent()" + s);
+ }
+
+ @Override
+ public void onSendError(@NonNull String s, @NonNull Exception e) {
+ Log.w(TAG, "onSendError()", e);
+ }
+
private static void handleReceivedNotification(Context context) {
try {
context.startService(new Intent(context, FcmFetchService.class));
From 7f2b6178d5a8a1d5eb17e8cd406ae806ea174575 Mon Sep 17 00:00:00 2001
From: Moxie Marlinspike
Date: Mon, 1 Feb 2021 18:52:01 -0800
Subject: [PATCH 08/17] Add support for configuring a signal proxy.
---
.../push/SignalServiceNetworkAccess.java | 5 +
.../api/SignalServiceMessageReceiver.java | 6 +-
.../api/util/TlsProxySocketFactory.java | 306 ++++++++++++++++++
.../internal/configuration/SignalProxy.java | 19 ++
.../SignalServiceConfiguration.java | 7 +
.../internal/push/PushServiceSocket.java | 35 +-
.../websocket/WebSocketConnection.java | 11 +-
7 files changed, 373 insertions(+), 16 deletions(-)
create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/TlsProxySocketFactory.java
create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalProxy.java
diff --git a/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java b/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java
index 09b1e781ba5..4826e2cea13 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java
@@ -185,6 +185,7 @@ public SignalServiceNetworkAccess(Context context) {
new SignalStorageUrl[] {egyptGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage},
interceptors,
dns,
+ Optional.absent(),
zkGroupServerPublicParams));
put(COUNTRY_CODE_UAE, new SignalServiceConfiguration(new SignalServiceUrl[] {uaeGoogleService, baseAndroidService, baseGoogleService, mapsOneAndroidService, mapsTwoAndroidService, mailAndroidService},
@@ -195,6 +196,7 @@ public SignalServiceNetworkAccess(Context context) {
new SignalStorageUrl[] {uaeGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage},
interceptors,
dns,
+ Optional.absent(),
zkGroupServerPublicParams));
put(COUNTRY_CODE_OMAN, new SignalServiceConfiguration(new SignalServiceUrl[] {omanGoogleService, baseAndroidService, baseGoogleService, mapsOneAndroidService, mapsTwoAndroidService, mailAndroidService},
@@ -205,6 +207,7 @@ public SignalServiceNetworkAccess(Context context) {
new SignalStorageUrl[] {omanGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage},
interceptors,
dns,
+ Optional.absent(),
zkGroupServerPublicParams));
@@ -216,6 +219,7 @@ public SignalServiceNetworkAccess(Context context) {
new SignalStorageUrl[] {qatarGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage},
interceptors,
dns,
+ Optional.absent(),
zkGroupServerPublicParams));
}};
@@ -227,6 +231,7 @@ public SignalServiceNetworkAccess(Context context) {
new SignalStorageUrl[] {new SignalStorageUrl(BuildConfig.STORAGE_URL, new SignalServiceTrustStore(context))},
interceptors,
dns,
+ Optional.absent(),
zkGroupServerPublicParams);
this.censoredCountries = this.censorshipConfiguration.keySet().toArray(new String[0]);
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java
index 2ab3478cdaa..fe2ef191196 100644
--- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java
@@ -243,7 +243,8 @@ public SignalServiceMessagePipe createMessagePipe() {
Optional.of(credentialsProvider), signalAgent, connectivityListener,
sleepTimer,
urls.getNetworkInterceptors(),
- urls.getDns());
+ urls.getDns(),
+ urls.getSignalProxy());
return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider), clientZkProfileOperations);
}
@@ -254,7 +255,8 @@ public SignalServiceMessagePipe createUnidentifiedMessagePipe() {
Optional.absent(), signalAgent, connectivityListener,
sleepTimer,
urls.getNetworkInterceptors(),
- urls.getDns());
+ urls.getDns(),
+ urls.getSignalProxy());
return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider), clientZkProfileOperations);
}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/TlsProxySocketFactory.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/TlsProxySocketFactory.java
new file mode 100644
index 00000000000..a5eb3f792a6
--- /dev/null
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/TlsProxySocketFactory.java
@@ -0,0 +1,306 @@
+package org.whispersystems.signalservice.api.util;
+
+import org.whispersystems.libsignal.util.guava.Optional;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.net.SocketOption;
+import java.net.UnknownHostException;
+import java.nio.channels.SocketChannel;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.util.List;
+import java.util.Set;
+
+import javax.net.SocketFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+
+import okhttp3.Dns;
+
+public class TlsProxySocketFactory extends SocketFactory {
+
+ private final SSLSocketFactory system;
+
+ private final String proxyHost;
+ private final int proxyPort;
+ private final Optional dns;
+
+ public TlsProxySocketFactory(String proxyHost, int proxyPort, Optional dns) {
+ try {
+ SSLContext context = SSLContext.getInstance("TLS");
+ context.init(null, null, null);
+
+ this.system = context.getSocketFactory();
+ this.proxyHost = proxyHost;
+ this.proxyPort = proxyPort;
+ this.dns = dns;
+ } catch (NoSuchAlgorithmException | KeyManagementException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ @Override
+ public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
+ if (dns.isPresent()) {
+ List resolved = dns.get().lookup(host);
+
+ if (resolved.size() > 0) {
+ return createSocket(resolved.get(0), port);
+ }
+ }
+
+ return new ProxySocket(system.createSocket(proxyHost, proxyPort));
+ }
+
+ @Override
+ public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
+ if (dns.isPresent()) {
+ List resolved = dns.get().lookup(host);
+
+ if (resolved.size() > 0) {
+ return createSocket(resolved.get(0), port, localHost, localPort);
+ }
+ }
+
+ return new ProxySocket(system.createSocket(proxyHost, proxyPort, localHost, localPort));
+ }
+
+ @Override
+ public Socket createSocket(InetAddress host, int port) throws IOException {
+ return new ProxySocket(system.createSocket(proxyHost, proxyPort));
+ }
+
+ @Override
+ public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
+ return new ProxySocket(system.createSocket(proxyHost, proxyPort, localAddress, localPort));
+ }
+
+ @Override
+ public Socket createSocket() throws IOException {
+ SSLSocket socket = (SSLSocket)system.createSocket(proxyHost, proxyPort);
+ socket.startHandshake();
+
+ return new ProxySocket(socket);
+ }
+
+ private static class ProxySocket extends Socket {
+
+ private final Socket delegate;
+
+ private ProxySocket(Socket delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public void bind(SocketAddress bindpoint) throws IOException {
+ delegate.bind(bindpoint);
+ }
+
+ @Override
+ public InetAddress getInetAddress() {
+ return delegate.getInetAddress();
+ }
+
+ @Override
+ public InetAddress getLocalAddress() {
+ return delegate.getLocalAddress();
+ }
+
+ @Override
+ public int getPort() {
+ return delegate.getPort();
+ }
+
+ @Override
+ public int getLocalPort() {
+ return delegate.getLocalPort();
+ }
+
+ @Override
+ public SocketAddress getRemoteSocketAddress() {
+ return delegate.getRemoteSocketAddress();
+ }
+
+ @Override
+ public SocketAddress getLocalSocketAddress() {
+ return delegate.getLocalSocketAddress();
+ }
+
+ @Override
+ public SocketChannel getChannel() {
+ return delegate.getChannel();
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ return delegate.getInputStream();
+ }
+
+ @Override
+ public OutputStream getOutputStream() throws IOException {
+ return delegate.getOutputStream();
+ }
+
+ @Override
+ public void setTcpNoDelay(boolean on) throws SocketException {
+ delegate.setTcpNoDelay(on);
+ }
+
+ @Override
+ public boolean getTcpNoDelay() throws SocketException {
+ return delegate.getTcpNoDelay();
+ }
+
+ @Override
+ public void setSoLinger(boolean on, int linger) throws SocketException {
+ delegate.setSoLinger(on, linger);
+ }
+
+ @Override
+ public int getSoLinger() throws SocketException {
+ return delegate.getSoLinger();
+ }
+
+ @Override
+ public void sendUrgentData(int data) throws IOException {
+ delegate.sendUrgentData(data);
+ }
+
+ @Override
+ public void setOOBInline(boolean on) throws SocketException {
+ delegate.setOOBInline(on);
+ }
+
+ @Override
+ public boolean getOOBInline() throws SocketException {
+ return delegate.getOOBInline();
+ }
+
+ @Override
+ public void setSoTimeout(int timeout) throws SocketException {
+ delegate.setSoTimeout(timeout);
+ }
+
+ @Override
+ public int getSoTimeout() throws SocketException {
+ return delegate.getSoTimeout();
+ }
+
+ @Override
+ public void setSendBufferSize(int size) throws SocketException {
+ delegate.setSendBufferSize(size);
+ }
+
+ @Override
+ public int getSendBufferSize() throws SocketException {
+ return delegate.getSendBufferSize();
+ }
+
+ @Override
+ public void setReceiveBufferSize(int size) throws SocketException {
+ delegate.setReceiveBufferSize(size);
+ }
+
+ @Override
+ public int getReceiveBufferSize() throws SocketException {
+ return delegate.getReceiveBufferSize();
+ }
+
+ @Override
+ public void setKeepAlive(boolean on) throws SocketException {
+ delegate.setKeepAlive(on);
+ }
+
+ @Override
+ public boolean getKeepAlive() throws SocketException {
+ return delegate.getKeepAlive();
+ }
+
+ @Override
+ public void setTrafficClass(int tc) throws SocketException {
+ delegate.setTrafficClass(tc);
+ }
+
+ @Override
+ public int getTrafficClass() throws SocketException {
+ return delegate.getTrafficClass();
+ }
+
+ @Override
+ public void setReuseAddress(boolean on) throws SocketException {
+ delegate.setReuseAddress(on);
+ }
+
+ @Override
+ public boolean getReuseAddress() throws SocketException {
+ return delegate.getReuseAddress();
+ }
+
+ @Override
+ public void close() throws IOException {
+ delegate.close();
+ }
+
+ @Override
+ public void shutdownInput() throws IOException {
+ delegate.shutdownInput();
+ }
+
+ @Override
+ public void shutdownOutput() throws IOException {
+ delegate.shutdownOutput();
+ }
+
+ @Override
+ public String toString() {
+ return delegate.toString();
+ }
+
+ @Override
+ public boolean isConnected() {
+ return delegate.isConnected();
+ }
+
+ @Override
+ public boolean isBound() {
+ return delegate.isBound();
+ }
+
+ @Override
+ public boolean isClosed() {
+ return delegate.isClosed();
+ }
+
+ @Override
+ public boolean isInputShutdown() {
+ return delegate.isInputShutdown();
+ }
+
+ @Override
+ public boolean isOutputShutdown() {
+ return delegate.isOutputShutdown();
+ }
+
+ @Override
+ public void setPerformancePreferences(int connectionTime, int latency, int bandwidth) {
+ delegate.setPerformancePreferences(connectionTime, latency, bandwidth);
+ }
+
+ @Override
+ public void connect(SocketAddress endpoint) throws IOException {
+ // Already connected
+ }
+
+ @Override
+ public void connect(SocketAddress endpoint, int timeout) throws IOException {
+ // Already connected
+ }
+ }
+}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalProxy.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalProxy.java
new file mode 100644
index 00000000000..d855689b0ea
--- /dev/null
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalProxy.java
@@ -0,0 +1,19 @@
+package org.whispersystems.signalservice.internal.configuration;
+
+public class SignalProxy {
+ private final String host;
+ private final int port;
+
+ public SignalProxy(String host, int port) {
+ this.host = host;
+ this.port = port;
+ }
+
+ public String getHost() {
+ return host;
+ }
+
+ public int getPort() {
+ return port;
+ }
+}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.java
index 44b065721c6..85f91fbfc59 100644
--- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.java
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.java
@@ -17,6 +17,7 @@ public final class SignalServiceConfiguration {
private final SignalStorageUrl[] signalStorageUrls;
private final List networkInterceptors;
private final Optional dns;
+ private final Optional proxy;
private final byte[] zkGroupServerPublicParams;
public SignalServiceConfiguration(SignalServiceUrl[] signalServiceUrls,
@@ -26,6 +27,7 @@ public SignalServiceConfiguration(SignalServiceUrl[] signalServiceUrls,
SignalStorageUrl[] signalStorageUrls,
List networkInterceptors,
Optional dns,
+ Optional proxy,
byte[] zkGroupServerPublicParams)
{
this.signalServiceUrls = signalServiceUrls;
@@ -35,6 +37,7 @@ public SignalServiceConfiguration(SignalServiceUrl[] signalServiceUrls,
this.signalStorageUrls = signalStorageUrls;
this.networkInterceptors = networkInterceptors;
this.dns = dns;
+ this.proxy = proxy;
this.zkGroupServerPublicParams = zkGroupServerPublicParams;
}
@@ -69,4 +72,8 @@ public Optional getDns() {
public byte[] getZkGroupServerPublicParams() {
return zkGroupServerPublicParams;
}
+
+ public Optional getSignalProxy() {
+ return proxy;
+ }
}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java
index 22ca7fec859..79d5a6dcaba 100644
--- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java
@@ -71,8 +71,10 @@
import org.whispersystems.signalservice.api.storage.StorageAuthResponse;
import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.api.util.Tls12SocketFactory;
+import org.whispersystems.signalservice.api.util.TlsProxySocketFactory;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
+import org.whispersystems.signalservice.internal.configuration.SignalProxy;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import org.whispersystems.signalservice.internal.configuration.SignalUrl;
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryRequest;
@@ -129,6 +131,7 @@
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
+import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
@@ -245,11 +248,11 @@ public PushServiceSocket(SignalServiceConfiguration configuration,
this.credentialsProvider = credentialsProvider;
this.signalAgent = signalAgent;
this.automaticNetworkRetry = automaticNetworkRetry;
- this.serviceClients = createServiceConnectionHolders(configuration.getSignalServiceUrls(), configuration.getNetworkInterceptors(), configuration.getDns());
- this.cdnClientsMap = createCdnClientsMap(configuration.getSignalCdnUrlMap(), configuration.getNetworkInterceptors(), configuration.getDns());
- this.contactDiscoveryClients = createConnectionHolders(configuration.getSignalContactDiscoveryUrls(), configuration.getNetworkInterceptors(), configuration.getDns());
- this.keyBackupServiceClients = createConnectionHolders(configuration.getSignalKeyBackupServiceUrls(), configuration.getNetworkInterceptors(), configuration.getDns());
- this.storageClients = createConnectionHolders(configuration.getSignalStorageUrls(), configuration.getNetworkInterceptors(), configuration.getDns());
+ this.serviceClients = createServiceConnectionHolders(configuration.getSignalServiceUrls(), configuration.getNetworkInterceptors(), configuration.getDns(), configuration.getSignalProxy());
+ this.cdnClientsMap = createCdnClientsMap(configuration.getSignalCdnUrlMap(), configuration.getNetworkInterceptors(), configuration.getDns(), configuration.getSignalProxy());
+ this.contactDiscoveryClients = createConnectionHolders(configuration.getSignalContactDiscoveryUrls(), configuration.getNetworkInterceptors(), configuration.getDns(), configuration.getSignalProxy());
+ this.keyBackupServiceClients = createConnectionHolders(configuration.getSignalKeyBackupServiceUrls(), configuration.getNetworkInterceptors(), configuration.getDns(), configuration.getSignalProxy());
+ this.storageClients = createConnectionHolders(configuration.getSignalStorageUrls(), configuration.getNetworkInterceptors(), configuration.getDns(), configuration.getSignalProxy());
this.random = new SecureRandom();
this.clientZkProfileOperations = clientZkProfileOperations;
}
@@ -1741,13 +1744,14 @@ public CallingResponse makeCallingRequest(long requestId, String url, String htt
private ServiceConnectionHolder[] createServiceConnectionHolders(SignalUrl[] urls,
List interceptors,
- Optional dns)
+ Optional dns,
+ Optional proxy)
{
List serviceConnectionHolders = new LinkedList<>();
for (SignalUrl url : urls) {
- serviceConnectionHolders.add(new ServiceConnectionHolder(createConnectionClient(url, interceptors, dns),
- createConnectionClient(url, interceptors, dns),
+ serviceConnectionHolders.add(new ServiceConnectionHolder(createConnectionClient(url, interceptors, dns, proxy),
+ createConnectionClient(url, interceptors, dns, proxy),
url.getUrl(), url.getHostHeader()));
}
@@ -1756,12 +1760,13 @@ private ServiceConnectionHolder[] createServiceConnectionHolders(SignalUrl[] url
private static Map createCdnClientsMap(final Map signalCdnUrlMap,
final List interceptors,
- final Optional dns) {
+ final Optional dns,
+ final Optional proxy) {
validateConfiguration(signalCdnUrlMap);
final Map result = new HashMap<>();
for (Map.Entry entry : signalCdnUrlMap.entrySet()) {
result.put(entry.getKey(),
- createConnectionHolders(entry.getValue(), interceptors, dns));
+ createConnectionHolders(entry.getValue(), interceptors, dns, proxy));
}
return Collections.unmodifiableMap(result);
}
@@ -1772,17 +1777,17 @@ private static void validateConfiguration(Map signalCdn
}
}
- private static ConnectionHolder[] createConnectionHolders(SignalUrl[] urls, List interceptors, Optional dns) {
+ private static ConnectionHolder[] createConnectionHolders(SignalUrl[] urls, List interceptors, Optional dns, Optional proxy) {
List connectionHolders = new LinkedList<>();
for (SignalUrl url : urls) {
- connectionHolders.add(new ConnectionHolder(createConnectionClient(url, interceptors, dns), url.getUrl(), url.getHostHeader()));
+ connectionHolders.add(new ConnectionHolder(createConnectionClient(url, interceptors, dns, proxy), url.getUrl(), url.getHostHeader()));
}
return connectionHolders.toArray(new ConnectionHolder[0]);
}
- private static OkHttpClient createConnectionClient(SignalUrl url, List interceptors, Optional dns) {
+ private static OkHttpClient createConnectionClient(SignalUrl url, List interceptors, Optional dns, Optional proxy) {
try {
TrustManager[] trustManagers = BlacklistingTrustManager.createFor(url.getTrustStore());
@@ -1794,6 +1799,10 @@ private static OkHttpClient createConnectionClient(SignalUrl url, List interceptors;
private final Optional dns;
+ private final Optional signalProxy;
private WebSocket client;
private KeepAliveSender keepAliveSender;
@@ -76,7 +79,8 @@ public WebSocketConnection(String httpUri,
ConnectivityListener listener,
SleepTimer timer,
List interceptors,
- Optional dns)
+ Optional dns,
+ Optional signalProxy)
{
this.trustStore = trustStore;
this.credentialsProvider = credentialsProvider;
@@ -85,6 +89,7 @@ public WebSocketConnection(String httpUri,
this.sleepTimer = timer;
this.interceptors = interceptors;
this.dns = dns;
+ this.signalProxy = signalProxy;
this.attempts = 0;
this.connected = false;
@@ -120,6 +125,10 @@ public synchronized void connect() {
clientBuilder.addInterceptor(interceptor);
}
+ if (signalProxy.isPresent()) {
+ clientBuilder.socketFactory(new TlsProxySocketFactory(signalProxy.get().getHost(), signalProxy.get().getPort(), dns));
+ }
+
OkHttpClient okHttpClient = clientBuilder.build();
Request.Builder requestBuilder = new Request.Builder().url(filledUri);
From d6061fb699d968627a6a028acecbdc251a33811c Mon Sep 17 00:00:00 2001
From: Alan Evans
Date: Tue, 2 Feb 2021 15:19:06 -0400
Subject: [PATCH 09/17] Fix migration of null titled group.
---
.../java/org/thoughtcrime/securesms/groups/GroupManagerV2.java | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java
index c9a95ed195f..43adc87fb73 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java
@@ -49,6 +49,7 @@
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sms.MessageSender;
+import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
import org.whispersystems.signalservice.api.groupsv2.GroupCandidate;
@@ -216,7 +217,7 @@ void migrateGroupOnToServer(@NonNull GroupId.V1 groupIdV1, @NonNull Collection memberIds = Stream.of(members)
From c15ea8c0b48dc0d3d0f1efc2afe25520f360a559 Mon Sep 17 00:00:00 2001
From: Alan Evans
Date: Tue, 2 Feb 2021 15:30:20 -0400
Subject: [PATCH 10/17] Skip automigration of nameless groups.
---
.../thoughtcrime/securesms/groups/GroupsV1MigrationUtil.java | 5 +++++
.../org/thoughtcrime/securesms/recipients/Recipient.java | 5 ++++-
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV1MigrationUtil.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV1MigrationUtil.java
index a1840a3aec0..046aeaee209 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV1MigrationUtil.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV1MigrationUtil.java
@@ -101,6 +101,11 @@ public static void migrate(@NonNull Context context, @NonNull RecipientId recipi
List possibleMembers = forced ? getMigratableManualMigrationMembers(registeredMembers)
: getMigratableAutoMigrationMembers(registeredMembers);
+ if (!forced && !groupRecipient.hasName()) {
+ Log.w(TAG, "Group has no name. Skipping auto-migration.");
+ throw new InvalidMigrationStateException();
+ }
+
if (!forced && possibleMembers.size() != registeredMembers.size()) {
Log.w(TAG, "Not allowed to invite or leave registered users behind in an auto-migration! Skipping.");
throw new InvalidMigrationStateException();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java
index 5acc8f0b5c0..e9da73848d7 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java
@@ -41,7 +41,6 @@
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.StringUtil;
-import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
import org.whispersystems.libsignal.util.guava.Optional;
@@ -421,6 +420,10 @@ public boolean isSelf() {
}
}
+ public boolean hasName() {
+ return name != null;
+ }
+
/**
* False iff it {@link #getDisplayName} would fall back to e164, email or unknown.
*/
From 0d215d609bbe90ca297f0e5801cf9a8f93cca035 Mon Sep 17 00:00:00 2001
From: Cody Henthorne
Date: Tue, 2 Feb 2021 14:50:08 -0500
Subject: [PATCH 11/17] Fix empty conversation update item text.
For some reason, if an EmojiTextView has a wrap content width and some other set of conditions occur, the view will not request a relayout when text changes.
This change inelegantly calls request layout more often to prevent that from happening.
---
.../securesms/components/emoji/EmojiTextView.java | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java
index 210f209dd5f..7448cd9510e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java
@@ -10,6 +10,7 @@
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.TypedValue;
+import android.view.ViewGroup;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
@@ -137,6 +138,10 @@ protected void onDraw(Canvas canvas) {
}
}
}
+
+ if (getLayoutParams() != null && getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
+ requestLayout();
+ }
}
public void setOverflowText(@Nullable CharSequence overflowText) {
From 46344776a4d89dcdded0aa5d2c47e1e1be1470cb Mon Sep 17 00:00:00 2001
From: Greyson Parrelli
Date: Tue, 2 Feb 2021 16:42:47 -0500
Subject: [PATCH 12/17] Add UI support for configuring a proxy.
---
app/src/main/AndroidManifest.xml | 11 +-
.../securesms/ApplicationContext.java | 12 +-
.../ApplicationPreferencesActivity.java | 4 +
.../thoughtcrime/securesms/MainActivity.java | 9 +
.../conversation/ConversationFragment.java | 5 +-
.../ConversationListFragment.java | 38 ++++
.../ConversationListViewModel.java | 5 +
.../dependencies/ApplicationDependencies.java | 31 +++-
.../ApplicationDependencyProvider.java | 60 +++---
.../securesms/keyvalue/ProxyValues.java | 70 +++++++
.../securesms/keyvalue/SignalStore.java | 7 +
.../messages/IncomingMessageObserver.java | 24 ++-
.../net/PipeConnectivityListener.java | 86 +++++++++
.../DataAndStoragePreferenceFragment.java | 8 +
.../preferences/EditProxyFragment.java | 174 ++++++++++++++++++
.../preferences/EditProxyViewModel.java | 99 ++++++++++
.../profiles/manage/EditAboutFragment.java | 3 +
.../manage/EditProfileNameFragment.java | 3 +
.../proxy/ProxyBottomSheetFragment.java | 117 ++++++++++++
.../push/SignalServiceNetworkAccess.java | 2 +-
.../securesms/util/CommunicationActions.java | 16 ++
.../securesms/util/SignalProxyUtil.java | 123 +++++++++++++
.../drawable-night/ic_proxy_connected_24.xml | 13 ++
.../drawable-night/ic_proxy_connecting_24.xml | 9 +
.../res/drawable-night/ic_proxy_failed_24.xml | 9 +
.../res/drawable/ic_proxy_connected_24.xml | 12 ++
.../res/drawable/ic_proxy_connecting_24.xml | 15 ++
.../main/res/drawable/ic_proxy_failed_24.xml | 15 ++
app/src/main/res/drawable/proxy_avatar_96.xml | 15 ++
.../res/layout/conversation_list_fragment.xml | 17 +-
.../main/res/layout/edit_proxy_fragment.xml | 93 ++++++++++
.../main/res/layout/proxy_bottom_sheet.xml | 118 ++++++++++++
app/src/main/res/values/strings.xml | 23 +++
.../res/xml/preferences_data_and_storage.xml | 11 ++
.../api/util/TlsProxySocketFactory.java | 2 -
.../api/websocket/ConnectivityListener.java | 3 +
.../websocket/WebSocketConnection.java | 10 +-
37 files changed, 1217 insertions(+), 55 deletions(-)
create mode 100644 app/src/main/java/org/thoughtcrime/securesms/keyvalue/ProxyValues.java
create mode 100644 app/src/main/java/org/thoughtcrime/securesms/net/PipeConnectivityListener.java
create mode 100644 app/src/main/java/org/thoughtcrime/securesms/preferences/EditProxyFragment.java
create mode 100644 app/src/main/java/org/thoughtcrime/securesms/preferences/EditProxyViewModel.java
create mode 100644 app/src/main/java/org/thoughtcrime/securesms/proxy/ProxyBottomSheetFragment.java
create mode 100644 app/src/main/java/org/thoughtcrime/securesms/util/SignalProxyUtil.java
create mode 100644 app/src/main/res/drawable-night/ic_proxy_connected_24.xml
create mode 100644 app/src/main/res/drawable-night/ic_proxy_connecting_24.xml
create mode 100644 app/src/main/res/drawable-night/ic_proxy_failed_24.xml
create mode 100644 app/src/main/res/drawable/ic_proxy_connected_24.xml
create mode 100644 app/src/main/res/drawable/ic_proxy_connecting_24.xml
create mode 100644 app/src/main/res/drawable/ic_proxy_failed_24.xml
create mode 100644 app/src/main/res/drawable/proxy_avatar_96.xml
create mode 100644 app/src/main/res/layout/edit_proxy_fragment.xml
create mode 100644 app/src/main/res/layout/proxy_bottom_sheet.xml
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index db55855b0cc..580f23a221d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -254,6 +254,14 @@
+
+
+
+
+
+
+
+ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
+ android:windowSoftInputMode="adjustResize">
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java
index 0d97c39acb6..681d9223e22 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java
@@ -17,7 +17,6 @@
package org.thoughtcrime.securesms;
import android.content.Context;
-import android.hardware.SensorManager;
import android.os.Build;
import androidx.annotation.NonNull;
@@ -33,7 +32,6 @@
import org.conscrypt.Conscrypt;
import org.signal.aesgcmprovider.AesGcmProvider;
-import org.signal.core.util.ShakeDetector;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.AndroidLogger;
import org.signal.core.util.logging.Log;
@@ -46,7 +44,6 @@
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
import org.thoughtcrime.securesms.gcm.FcmJobService;
-import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
import org.thoughtcrime.securesms.jobs.GroupV1MigrationJob;
@@ -71,7 +68,6 @@
import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
-import org.thoughtcrime.securesms.shakereport.ShakeToReport;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.DynamicTheme;
@@ -145,6 +141,12 @@ public void onCreate() {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}
})
+ .addBlocking("proxy-init", () -> {
+ if (SignalStore.proxy().isProxyEnabled()) {
+ Log.w(TAG, "Proxy detected. Enabling Conscrypt.setUseEngineSocketByDefault()");
+ Conscrypt.setUseEngineSocketByDefault(true);
+ }
+ })
.addNonBlocking(this::initializeRevealableMessageManager)
.addNonBlocking(this::initializeGcmCheck)
.addNonBlocking(this::initializeSignedPreKeyCheck)
@@ -272,7 +274,7 @@ public void initializeMessageRetrieval() {
}
private void initializeAppDependencies() {
- ApplicationDependencies.init(this, new ApplicationDependencyProvider(this, new SignalServiceNetworkAccess(this)));
+ ApplicationDependencies.init(this, new ApplicationDependencyProvider(this));
}
private void initializeFirstEverAppLaunch() {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java
index bf79472b949..03a94645cd8 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java
@@ -40,6 +40,7 @@
import org.thoughtcrime.securesms.preferences.ChatsPreferenceFragment;
import org.thoughtcrime.securesms.preferences.CorrectedPreferenceFragment;
import org.thoughtcrime.securesms.preferences.DataAndStoragePreferenceFragment;
+import org.thoughtcrime.securesms.preferences.EditProxyFragment;
import org.thoughtcrime.securesms.preferences.NotificationsPreferenceFragment;
import org.thoughtcrime.securesms.preferences.SmsMmsPreferenceFragment;
import org.thoughtcrime.securesms.preferences.widgets.ProfilePreference;
@@ -67,6 +68,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
{
public static final String LAUNCH_TO_BACKUPS_FRAGMENT = "launch.to.backups.fragment";
public static final String LAUNCH_TO_HELP_FRAGMENT = "launch.to.help.fragment";
+ public static final String LAUNCH_TO_PROXY_FRAGMENT = "launch.to.proxy.fragment";
@SuppressWarnings("unused")
private static final String TAG = ApplicationPreferencesActivity.class.getSimpleName();
@@ -108,6 +110,8 @@ protected void onCreate(Bundle icicle, boolean ready) {
initFragment(android.R.id.content, new BackupsPreferenceFragment());
} else if (getIntent() != null && getIntent().getBooleanExtra(LAUNCH_TO_HELP_FRAGMENT, false)) {
initFragment(android.R.id.content, new HelpFragment());
+ } else if (getIntent() != null && getIntent().getBooleanExtra(LAUNCH_TO_PROXY_FRAGMENT, false)) {
+ initFragment(android.R.id.content, EditProxyFragment.newInstance());
} else if (icicle == null) {
initFragment(android.R.id.content, new ApplicationPreferenceFragment());
} else {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.java
index 4a34710d1c7..1584412926d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.java
@@ -41,6 +41,7 @@ protected void onCreate(Bundle savedInstanceState, boolean ready) {
navigator.onCreate(savedInstanceState);
handleGroupLinkInIntent(getIntent());
+ handleProxyInIntent(getIntent());
CachedInflater.from(this).clear();
}
@@ -56,6 +57,7 @@ public Intent getIntent() {
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
handleGroupLinkInIntent(intent);
+ handleProxyInIntent(intent);
}
@Override
@@ -95,4 +97,11 @@ private void handleGroupLinkInIntent(Intent intent) {
CommunicationActions.handlePotentialGroupLinkUrl(this, data.toString());
}
}
+
+ private void handleProxyInIntent(Intent intent) {
+ Uri data = intent.getData();
+ if (data != null) {
+ CommunicationActions.handlePotentialProxyLinkUrl(this, data.toString());
+ }
+ }
}
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 57bf93c9a9b..328b606b342 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java
@@ -131,6 +131,7 @@
import org.thoughtcrime.securesms.util.RemoteDeleteUtil;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.SetUtil;
+import org.thoughtcrime.securesms.util.SignalProxyUtil;
import org.thoughtcrime.securesms.util.SnapToTopDataObserver;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.StorageUtil;
@@ -336,6 +337,7 @@ public void onAttach(Activity activity) {
public void onStart() {
super.onStart();
initializeTypingObserver();
+ SignalProxyUtil.startListeningToWebsocket();
}
@Override
@@ -1429,7 +1431,8 @@ public void onUnregisterVoiceNoteCallbacks(@NonNull Observer searchToolbar;
+ private ImageView proxyStatus;
private ImageView searchAction;
private View toolbarShadow;
private ConversationListViewModel viewModel;
@@ -199,6 +203,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
searchEmptyState = view.findViewById(R.id.search_no_results);
searchAction = view.findViewById(R.id.search_action);
toolbarShadow = view.findViewById(R.id.conversation_list_toolbar_shadow);
+ proxyStatus = view.findViewById(R.id.conversation_list_proxy_status);
reminderView = new Stub<>(view.findViewById(R.id.reminder));
emptyState = new Stub<>(view.findViewById(R.id.empty_state));
searchToolbar = new Stub<>(view.findViewById(R.id.search_toolbar));
@@ -208,6 +213,8 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
toolbar.setVisibility(View.VISIBLE);
((AppCompatActivity) requireActivity()).setSupportActionBar(toolbar);
+ proxyStatus.setOnClickListener(v -> onProxyStatusClicked());
+
fab.show();
cameraFab.show();
@@ -262,6 +269,8 @@ public void onResume() {
if (activeAdapter != null) {
activeAdapter.notifyDataSetChanged();
}
+
+ SignalProxyUtil.startListeningToWebsocket();
}
@Override
@@ -543,6 +552,7 @@ private void initializeViewModel() {
viewModel.getMegaphone().observe(getViewLifecycleOwner(), this::onMegaphoneChanged);
viewModel.getConversationList().observe(getViewLifecycleOwner(), this::onSubmitList);
viewModel.hasNoConversations().observe(getViewLifecycleOwner(), this::updateEmptyState);
+ viewModel.getPipeState().observe(getViewLifecycleOwner(), this::updateProxyStatus);
visibilityLifecycleObserver = new DefaultLifecycleObserver() {
@Override
@@ -856,6 +866,34 @@ void updateEmptyState(boolean isConversationEmpty) {
}
}
+ private void updateProxyStatus(@NonNull PipeConnectivityListener.State state) {
+ if (SignalStore.proxy().isProxyEnabled()) {
+ proxyStatus.setVisibility(View.VISIBLE);
+
+ switch (state) {
+ case CONNECTING:
+ case DISCONNECTED:
+ proxyStatus.setImageResource(R.drawable.ic_proxy_connecting_24);
+ break;
+ case CONNECTED:
+ proxyStatus.setImageResource(R.drawable.ic_proxy_connected_24);
+ break;
+ case FAILURE:
+ proxyStatus.setImageResource(R.drawable.ic_proxy_failed_24);
+ break;
+ }
+ } else {
+ proxyStatus.setVisibility(View.GONE);
+ }
+ }
+
+ private void onProxyStatusClicked() {
+ Intent intent = new Intent(requireContext(), ApplicationPreferencesActivity.class);
+ intent.putExtra(ApplicationPreferencesActivity.LAUNCH_TO_PROXY_FRAGMENT, true);
+
+ startActivity(intent);
+ }
+
protected void onPostSubmitList(int conversationCount) {
if (conversationCount >= 6 && (SignalStore.onboarding().shouldShowInviteFriends() || SignalStore.onboarding().shouldShowNewGroup())) {
SignalStore.onboarding().clearAll();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.java
index 716f1093299..234133ff737 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.java
@@ -21,6 +21,7 @@
import org.thoughtcrime.securesms.megaphone.Megaphone;
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
import org.thoughtcrime.securesms.megaphone.Megaphones;
+import org.thoughtcrime.securesms.net.PipeConnectivityListener;
import org.thoughtcrime.securesms.search.SearchRepository;
import org.thoughtcrime.securesms.util.Debouncer;
import org.thoughtcrime.securesms.util.Util;
@@ -100,6 +101,10 @@ public LiveData hasNoConversations() {
return pagedData.getController();
}
+ @NonNull LiveData getPipeState() {
+ return ApplicationDependencies.getPipeListener().getState();
+ }
+
public int getPinnedCount() {
return pinnedCount;
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java
index 0c52c1a00f0..a60f37855e3 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java
@@ -18,6 +18,7 @@
import org.thoughtcrime.securesms.messages.BackgroundMessageRetriever;
import org.thoughtcrime.securesms.messages.IncomingMessageObserver;
import org.thoughtcrime.securesms.messages.IncomingMessageProcessor;
+import org.thoughtcrime.securesms.net.PipeConnectivityListener;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.recipients.LiveRecipientCache;
@@ -79,9 +80,9 @@ public static void init(@NonNull Application application, @NonNull Provider prov
throw new IllegalStateException("Already initialized!");
}
- ApplicationDependencies.application = application;
- ApplicationDependencies.provider = provider;
- ApplicationDependencies.messageNotifier = provider.provideMessageNotifier();
+ ApplicationDependencies.application = application;
+ ApplicationDependencies.provider = provider;
+ ApplicationDependencies.messageNotifier = provider.provideMessageNotifier();
}
}
@@ -89,6 +90,10 @@ public static void init(@NonNull Application application, @NonNull Provider prov
return application;
}
+ public static @NonNull PipeConnectivityListener getPipeListener() {
+ return provider.providePipeListener();
+ }
+
public static @NonNull SignalServiceAccountManager getSignalServiceAccountManager() {
if (accountManager == null) {
synchronized (LOCK) {
@@ -179,6 +184,25 @@ public static void resetSignalServiceMessageReceiver() {
}
}
+ public static void resetNetworkConnectionsAfterProxyChange() {
+ synchronized (LOCK) {
+ getPipeListener().reset();
+
+ if (incomingMessageObserver != null) {
+ incomingMessageObserver.terminate();
+ }
+
+ if (messageSender != null) {
+ messageSender.cancelInFlightRequests();
+ }
+
+ incomingMessageObserver = null;
+ messageReceiver = null;
+ accountManager = null;
+ messageSender = null;
+ }
+ }
+
public static @NonNull SignalServiceNetworkAccess getSignalServiceNetworkAccess() {
return provider.provideSignalServiceNetworkAccess();
}
@@ -336,6 +360,7 @@ public static TypingStatusSender getTypingStatusSender() {
}
public interface Provider {
+ @NonNull PipeConnectivityListener providePipeListener();
@NonNull GroupsV2Operations provideGroupsV2Operations();
@NonNull SignalServiceAccountManager provideSignalServiceAccountManager();
@NonNull SignalServiceMessageSender provideSignalServiceMessageSender();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java
index da6dd27118f..18498a3598c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java
@@ -4,6 +4,7 @@
import android.content.Context;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import org.greenrobot.eventbus.EventBus;
import org.signal.core.util.concurrent.SignalExecutors;
@@ -35,10 +36,12 @@
import org.thoughtcrime.securesms.jobs.PushTextSendJob;
import org.thoughtcrime.securesms.jobs.ReactionSendJob;
import org.thoughtcrime.securesms.jobs.TypingSendJob;
+import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
import org.thoughtcrime.securesms.messages.BackgroundMessageRetriever;
import org.thoughtcrime.securesms.messages.IncomingMessageObserver;
import org.thoughtcrime.securesms.messages.IncomingMessageProcessor;
+import org.thoughtcrime.securesms.net.PipeConnectivityListener;
import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.notifications.OptimizedMessageNotifier;
@@ -53,12 +56,14 @@
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.FrameRateTracker;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
+import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
+import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.api.util.SleepTimer;
import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
@@ -66,6 +71,8 @@
import java.util.UUID;
+import okhttp3.Response;
+
/**
* Implementation of {@link ApplicationDependencies.Provider} that provides real app dependencies.
*/
@@ -73,16 +80,21 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
private static final String TAG = Log.tag(ApplicationDependencyProvider.class);
- private final Application context;
- private final SignalServiceNetworkAccess networkAccess;
+ private final Application context;
+ private final PipeConnectivityListener pipeListener;
- public ApplicationDependencyProvider(@NonNull Application context, @NonNull SignalServiceNetworkAccess networkAccess) {
- this.context = context;
- this.networkAccess = networkAccess;
+ public ApplicationDependencyProvider(@NonNull Application context) {
+ this.context = context;
+ this.pipeListener = new PipeConnectivityListener(context);
}
private @NonNull ClientZkOperations provideClientZkOperations() {
- return ClientZkOperations.create(networkAccess.getConfiguration(context));
+ return ClientZkOperations.create(provideSignalServiceNetworkAccess().getConfiguration(context));
+ }
+
+ @Override
+ public @NonNull PipeConnectivityListener providePipeListener() {
+ return pipeListener;
}
@Override
@@ -92,7 +104,7 @@ public ApplicationDependencyProvider(@NonNull Application context, @NonNull Sign
@Override
public @NonNull SignalServiceAccountManager provideSignalServiceAccountManager() {
- return new SignalServiceAccountManager(networkAccess.getConfiguration(context),
+ return new SignalServiceAccountManager(provideSignalServiceNetworkAccess().getConfiguration(context),
new DynamicCredentialsProvider(context),
BuildConfig.SIGNAL_AGENT,
provideGroupsV2Operations(),
@@ -101,7 +113,7 @@ public ApplicationDependencyProvider(@NonNull Application context, @NonNull Sign
@Override
public @NonNull SignalServiceMessageSender provideSignalServiceMessageSender() {
- return new SignalServiceMessageSender(networkAccess.getConfiguration(context),
+ return new SignalServiceMessageSender(provideSignalServiceNetworkAccess().getConfiguration(context),
new DynamicCredentialsProvider(context),
new SignalProtocolStoreImpl(context),
BuildConfig.SIGNAL_AGENT,
@@ -119,10 +131,10 @@ public ApplicationDependencyProvider(@NonNull Application context, @NonNull Sign
public @NonNull SignalServiceMessageReceiver provideSignalServiceMessageReceiver() {
SleepTimer sleepTimer = TextSecurePreferences.isFcmDisabled(context) ? new AlarmSleepTimer(context)
: new UptimeSleepTimer();
- return new SignalServiceMessageReceiver(networkAccess.getConfiguration(context),
+ return new SignalServiceMessageReceiver(provideSignalServiceNetworkAccess().getConfiguration(context),
new DynamicCredentialsProvider(context),
BuildConfig.SIGNAL_AGENT,
- new PipeConnectivityListener(),
+ pipeListener,
sleepTimer,
provideClientZkOperations().getProfileOperations(),
FeatureFlags.okHttpAutomaticRetry());
@@ -130,7 +142,7 @@ public ApplicationDependencyProvider(@NonNull Application context, @NonNull Sign
@Override
public @NonNull SignalServiceNetworkAccess provideSignalServiceNetworkAccess() {
- return networkAccess;
+ return new SignalServiceNetworkAccess(context);
}
@Override
@@ -240,30 +252,4 @@ public String getSignalingKey() {
return TextSecurePreferences.getSignalingKey(context);
}
}
-
- private class PipeConnectivityListener implements ConnectivityListener {
-
- @Override
- public void onConnected() {
- Log.i(TAG, "onConnected()");
- TextSecurePreferences.setUnauthorizedReceived(context, false);
- }
-
- @Override
- public void onConnecting() {
- Log.i(TAG, "onConnecting()");
- }
-
- @Override
- public void onDisconnected() {
- Log.w(TAG, "onDisconnected()");
- }
-
- @Override
- public void onAuthenticationFailure() {
- Log.w(TAG, "onAuthenticationFailure()");
- TextSecurePreferences.setUnauthorizedReceived(context, true);
- EventBus.getDefault().post(new ReminderUpdateEvent());
- }
- }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/ProxyValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/ProxyValues.java
new file mode 100644
index 00000000000..d45c2881cc3
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/ProxyValues.java
@@ -0,0 +1,70 @@
+package org.thoughtcrime.securesms.keyvalue;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.whispersystems.signalservice.internal.configuration.SignalProxy;
+
+public final class ProxyValues extends SignalStoreValues {
+
+ private static final String KEY_PROXY_ENABLED = "proxy.enabled";
+ private static final String KEY_HOST = "proxy.host";
+ private static final String KEY_PORT = "proxy.port";
+
+ ProxyValues(@NonNull KeyValueStore store) {
+ super(store);
+ }
+
+ @Override
+ void onFirstEverAppLaunch() {
+ }
+
+
+ public void enableProxy(@NonNull SignalProxy proxy) {
+ getStore().beginWrite()
+ .putBoolean(KEY_PROXY_ENABLED, true)
+ .putString(KEY_HOST, proxy.getHost())
+ .putInteger(KEY_PORT, proxy.getPort())
+ .apply();
+ }
+
+ /**
+ * Disables the proxy, but does not clear out the last-chosen host.
+ */
+ public void disableProxy() {
+ putBoolean(KEY_PROXY_ENABLED, false);
+ }
+
+ public boolean isProxyEnabled() {
+ return getBoolean(KEY_PROXY_ENABLED, false);
+ }
+
+ /**
+ * Sets the proxy. This does not *enable* the proxy. This is because the user may want to set a
+ * proxy and then enabled it and disable it at will.
+ */
+ public void setProxy(@Nullable SignalProxy proxy) {
+ if (proxy != null) {
+ getStore().beginWrite()
+ .putString(KEY_HOST, proxy.getHost())
+ .putInteger(KEY_PORT, proxy.getPort())
+ .apply();
+ } else {
+ getStore().beginWrite()
+ .remove(KEY_HOST)
+ .remove(KEY_PORT)
+ .apply();
+ }
+ }
+
+ public @Nullable SignalProxy getProxy() {
+ String host = getString(KEY_HOST, null);
+ int port = getInteger(KEY_PORT, 0);
+
+ if (host != null) {
+ return new SignalProxy(host, port);
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java
index d6a31b436cb..aaab35b719d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java
@@ -30,6 +30,7 @@ public final class SignalStore {
private final PhoneNumberPrivacyValues phoneNumberPrivacyValues;
private final OnboardingValues onboardingValues;
private final WallpaperValues wallpaperValues;
+ private final ProxyValues proxyValues;
private SignalStore() {
this.store = new KeyValueStore(ApplicationDependencies.getApplication());
@@ -48,6 +49,7 @@ private SignalStore() {
this.phoneNumberPrivacyValues = new PhoneNumberPrivacyValues(store);
this.onboardingValues = new OnboardingValues(store);
this.wallpaperValues = new WallpaperValues(store);
+ this.proxyValues = new ProxyValues(store);
}
public static void onFirstEverAppLaunch() {
@@ -65,6 +67,7 @@ public static void onFirstEverAppLaunch() {
phoneNumberPrivacy().onFirstEverAppLaunch();
onboarding().onFirstEverAppLaunch();
wallpaper().onFirstEverAppLaunch();
+ proxy().onFirstEverAppLaunch();
}
public static @NonNull KbsValues kbsValues() {
@@ -127,6 +130,10 @@ public static void onFirstEverAppLaunch() {
return INSTANCE.wallpaperValues;
}
+ public static @NonNull ProxyValues proxy() {
+ return INSTANCE.proxyValues;
+ }
+
public static @NonNull GroupsV2AuthorizationSignalStoreCache groupsV2AuthorizationCache() {
return new GroupsV2AuthorizationSignalStoreCache(getStore());
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.java b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.java
index 1a9e596a0f3..c9f8924c048 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.java
@@ -22,6 +22,7 @@
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.jobs.PushDecryptDrainedJob;
+import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.messages.IncomingMessageProcessor.Processor;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
@@ -56,6 +57,7 @@ public class IncomingMessageObserver {
private volatile boolean networkDrained;
private volatile boolean decryptionDrained;
+ private volatile boolean terminated;
public IncomingMessageObserver(@NonNull Application context) {
this.context = context;
@@ -138,9 +140,10 @@ private synchronized boolean isConnectionNecessary() {
boolean websocketRegistered = TextSecurePreferences.isWebsocketRegistered(context);
boolean isGcmDisabled = TextSecurePreferences.isFcmDisabled(context);
boolean hasNetwork = NetworkConstraint.isMet(context);
+ boolean hasProxy = SignalStore.proxy().isProxyEnabled();
- Log.d(TAG, String.format("Network: %s, Foreground: %s, FCM: %s, Censored: %s, Registered: %s, Websocket Registered: %s",
- hasNetwork, appVisible, !isGcmDisabled, networkAccess.isCensored(context), registered, websocketRegistered));
+ Log.d(TAG, String.format("Network: %s, Foreground: %s, FCM: %s, Censored: %s, Registered: %s, Websocket Registered: %s, Proxy: %s",
+ hasNetwork, appVisible, !isGcmDisabled, networkAccess.isCensored(context), registered, websocketRegistered, hasProxy));
return registered &&
websocketRegistered &&
@@ -157,10 +160,19 @@ private synchronized void waitForConnectionNecessary() {
}
}
+ public void terminate() {
+ Log.w(TAG, "Beginning termination.");
+ terminated = true;
+ shutdown(pipe, unidentifiedPipe);
+ }
+
private void shutdown(@Nullable SignalServiceMessagePipe pipe, @Nullable SignalServiceMessagePipe unidentifiedPipe) {
try {
if (pipe != null) {
+ Log.w(TAG, "Shutting down normal pipe.");
pipe.shutdown();
+ } else {
+ Log.w(TAG, "No need to shutdown normal pipe, it doesn't exist.");
}
} catch (Throwable t) {
Log.w(TAG, "Closing normal pipe failed!", t);
@@ -168,7 +180,10 @@ private void shutdown(@Nullable SignalServiceMessagePipe pipe, @Nullable SignalS
try {
if (unidentifiedPipe != null) {
+ Log.w(TAG, "Shutting down unidentified pipe.");
unidentifiedPipe.shutdown();
+ } else {
+ Log.w(TAG, "No need to shutdown unidentified pipe, it doesn't exist.");
}
} catch (Throwable t) {
Log.w(TAG, "Closing unidentified pipe failed!", t);
@@ -187,12 +202,13 @@ private class MessageRetrievalThread extends Thread implements Thread.UncaughtEx
MessageRetrievalThread() {
super("MessageRetrievalService");
+ Log.i(TAG, "Initializing! (" + this.hashCode() + ")");
setUncaughtExceptionHandler(this);
}
@Override
public void run() {
- while (true) {
+ while (!terminated) {
Log.i(TAG, "Waiting for websocket state change....");
waitForConnectionNecessary();
@@ -236,6 +252,8 @@ public void run() {
Log.i(TAG, "Looping...");
}
+
+ Log.w(TAG, "Terminated! (" + this.hashCode() + ")");
}
@Override
diff --git a/app/src/main/java/org/thoughtcrime/securesms/net/PipeConnectivityListener.java b/app/src/main/java/org/thoughtcrime/securesms/net/PipeConnectivityListener.java
new file mode 100644
index 00000000000..ed3fad2212d
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/net/PipeConnectivityListener.java
@@ -0,0 +1,86 @@
+package org.thoughtcrime.securesms.net;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+
+import org.greenrobot.eventbus.EventBus;
+import org.signal.core.util.logging.Log;
+import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
+import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
+import org.thoughtcrime.securesms.keyvalue.SignalStore;
+import org.thoughtcrime.securesms.util.DefaultValueLiveData;
+import org.thoughtcrime.securesms.util.TextSecurePreferences;
+import org.whispersystems.signalservice.api.websocket.ConnectivityListener;
+
+import okhttp3.Response;
+
+/**
+ * Our standard listener for reacting to the state of the websocket. Translates the state into a
+ * LiveData for observation.
+ */
+public class PipeConnectivityListener implements ConnectivityListener {
+
+ private static final String TAG = Log.tag(PipeConnectivityListener.class);
+
+ private final Application application;
+ private final DefaultValueLiveData state;
+
+ public PipeConnectivityListener(@NonNull Application application) {
+ this.application = application;
+ this.state = new DefaultValueLiveData<>(State.DISCONNECTED);
+ }
+
+ @Override
+ public void onConnected() {
+ Log.i(TAG, "onConnected()");
+ TextSecurePreferences.setUnauthorizedReceived(application, false);
+ state.postValue(State.CONNECTED);
+ }
+
+ @Override
+ public void onConnecting() {
+ Log.i(TAG, "onConnecting()");
+ state.postValue(State.CONNECTING);
+ }
+
+ @Override
+ public void onDisconnected() {
+ Log.w(TAG, "onDisconnected()");
+ state.postValue(State.DISCONNECTED);
+ }
+
+ @Override
+ public void onAuthenticationFailure() {
+ Log.w(TAG, "onAuthenticationFailure()");
+ TextSecurePreferences.setUnauthorizedReceived(application, true);
+ EventBus.getDefault().post(new ReminderUpdateEvent());
+ state.postValue(State.FAILURE);
+ }
+
+ @Override
+ public boolean onGenericFailure(Response response, Throwable throwable) {
+ Log.w(TAG, "onGenericFailure() Response: " + response, throwable);
+ state.postValue(State.FAILURE);
+
+ if (SignalStore.proxy().isProxyEnabled()) {
+ Log.w(TAG, "Encountered an error while we had a proxy set! Terminating the connection to prevent retry spam.");
+ ApplicationDependencies.getIncomingMessageObserver().terminate();
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ public void reset() {
+ state.postValue(State.DISCONNECTED);
+ }
+
+ public @NonNull DefaultValueLiveData getState() {
+ return state;
+ }
+
+ public enum State {
+ DISCONNECTED, CONNECTING, CONNECTED, FAILURE
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/DataAndStoragePreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/DataAndStoragePreferenceFragment.java
index 39c1259a750..f7b0b0459e6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/preferences/DataAndStoragePreferenceFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/DataAndStoragePreferenceFragment.java
@@ -25,6 +25,7 @@ public class DataAndStoragePreferenceFragment extends ListSummaryPreferenceFragm
private static final String TAG = Log.tag(DataAndStoragePreferenceFragment.class);
private static final String MANAGE_STORAGE_KEY = "pref_data_manage";
+ private static final String USE_PROXY_KEY = "pref_use_proxy";
@Override
public void onCreate(Bundle icicle) {
@@ -52,6 +53,12 @@ public void onCreate(Bundle icicle) {
viewModel.getStorageBreakdown()
.observe(requireActivity(),
breakdown -> manageStorage.setSummary(Util.getPrettyFileSize(breakdown.getTotalSize())));
+
+
+ findPreference(USE_PROXY_KEY).setOnPreferenceClickListener(unused -> {
+ requireApplicationPreferencesActivity().pushFragment(EditProxyFragment.newInstance());
+ return false;
+ });
}
@Override
@@ -65,6 +72,7 @@ public void onResume() {
requireApplicationPreferencesActivity().getSupportActionBar().setTitle(R.string.preferences__data_and_storage);
setMediaDownloadSummaries();
ApplicationPreferencesViewModel.getApplicationPreferencesViewModel(requireActivity()).refreshStorageBreakdown(requireContext());
+ findPreference(USE_PROXY_KEY).setSummary(SignalStore.proxy().isProxyEnabled() ? R.string.preferences_on : R.string.preferences_off);
}
private @NonNull ApplicationPreferencesActivity requireApplicationPreferencesActivity() {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/EditProxyFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/EditProxyFragment.java
new file mode 100644
index 00000000000..a001fac050c
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/EditProxyFragment.java
@@ -0,0 +1,174 @@
+package org.thoughtcrime.securesms.preferences;
+
+import android.app.AlertDialog;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.SwitchCompat;
+import androidx.core.app.ShareCompat;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProviders;
+import androidx.navigation.Navigation;
+
+import com.dd.CircularProgressButton;
+
+import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.keyvalue.SignalStore;
+import org.thoughtcrime.securesms.net.PipeConnectivityListener;
+import org.whispersystems.libsignal.util.guava.Optional;
+import org.whispersystems.signalservice.internal.configuration.SignalProxy;
+
+public class EditProxyFragment extends Fragment {
+
+ private SwitchCompat proxySwitch;
+ private EditText proxyText;
+ private TextView proxyTitle;
+ private TextView proxyStatus;
+ private View shareButton;
+ private CircularProgressButton saveButton;
+ private EditProxyViewModel viewModel;
+
+ public static EditProxyFragment newInstance() {
+ return new EditProxyFragment();
+ }
+
+ @Override
+ public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.edit_proxy_fragment, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ this.proxySwitch = view.findViewById(R.id.edit_proxy_switch);
+ this.proxyTitle = view.findViewById(R.id.edit_proxy_address_title);
+ this.proxyText = view.findViewById(R.id.edit_proxy_host);
+ this.proxyStatus = view.findViewById(R.id.edit_proxy_status);
+ this.saveButton = view.findViewById(R.id.edit_proxy_save);
+ this.shareButton = view.findViewById(R.id.edit_proxy_share);
+
+ this.proxyText.setText(Optional.fromNullable(SignalStore.proxy().getProxy()).transform(SignalProxy::getHost).or(""));
+ this.proxySwitch.setChecked(SignalStore.proxy().isProxyEnabled());
+
+ initViewModel();
+
+ saveButton.setOnClickListener(v -> onSaveClicked());
+ shareButton.setOnClickListener(v -> onShareClicked());
+ proxySwitch.setOnCheckedChangeListener((buttonView, isChecked) -> viewModel.onToggleProxy(isChecked));
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ ((ApplicationPreferencesActivity) requireActivity()).requireSupportActionBar().setTitle(R.string.preferences_use_proxy);
+ }
+
+ private void initViewModel() {
+ viewModel = ViewModelProviders.of(this).get(EditProxyViewModel.class);
+
+ viewModel.getUiState().observe(getViewLifecycleOwner(), this::presentUiState);
+ viewModel.getProxyState().observe(getViewLifecycleOwner(), this::presentProxyState);
+ viewModel.getEvents().observe(getViewLifecycleOwner(), this::presentEvent);
+ viewModel.getSaveState().observe(getViewLifecycleOwner(), this::presentSaveState);
+ }
+
+ private void presentUiState(@NonNull EditProxyViewModel.UiState uiState) {
+ switch (uiState) {
+ case ALL_ENABLED:
+ proxyText.setEnabled(true);
+ proxyText.setAlpha(1);
+ saveButton.setEnabled(true);
+ saveButton.setAlpha(1);
+ shareButton.setEnabled(true);
+ shareButton.setAlpha(1);
+ proxyTitle.setAlpha(1);
+ proxyStatus.setVisibility(View.VISIBLE);
+ break;
+ case ALL_DISABLED:
+ proxyText.setEnabled(false);
+ proxyText.setAlpha(0.5f);
+ saveButton.setEnabled(false);
+ saveButton.setAlpha(0.5f);
+ shareButton.setEnabled(false);
+ shareButton.setAlpha(0.5f);
+ proxyTitle.setAlpha(0.5f);
+ proxyStatus.setVisibility(View.GONE);
+ break;
+ }
+ }
+
+ private void presentProxyState(@NonNull PipeConnectivityListener.State proxyState) {
+ switch (proxyState) {
+ case DISCONNECTED:
+ case CONNECTING:
+ proxyStatus.setText(R.string.preferences_connecting_to_proxy);
+ proxyStatus.setTextColor(getResources().getColor(R.color.signal_text_secondary));
+ break;
+ case CONNECTED:
+ proxyStatus.setText(R.string.preferences_connected_to_proxy);
+ proxyStatus.setTextColor(getResources().getColor(R.color.signal_accent_green));
+ break;
+ case FAILURE:
+ proxyStatus.setText(R.string.preferences_connection_failed);
+ proxyStatus.setTextColor(getResources().getColor(R.color.signal_alert_primary));
+ break;
+ }
+ }
+
+ private void presentEvent(@NonNull EditProxyViewModel.Event event) {
+ switch (event) {
+ case PROXY_SUCCESS:
+ proxyStatus.setVisibility(View.VISIBLE);
+ proxyText.setText(Optional.fromNullable(SignalStore.proxy().getProxy()).transform(SignalProxy::getHost).or(""));
+ new AlertDialog.Builder(requireContext())
+ .setTitle(R.string.preferences_success)
+ .setMessage(R.string.preferences_you_are_connected_to_the_proxy)
+ .setPositiveButton(android.R.string.ok, (d, i) -> d.dismiss())
+ .show();
+ break;
+ case PROXY_FAILURE:
+ proxyStatus.setVisibility(View.GONE);
+ proxyText.setText(Optional.fromNullable(SignalStore.proxy().getProxy()).transform(SignalProxy::getHost).or(""));
+ new AlertDialog.Builder(requireContext())
+ .setTitle(R.string.preferences_failed_to_connect)
+ .setMessage(R.string.preferences_couldnt_connect_to_the_proxy)
+ .setPositiveButton(android.R.string.ok, (d, i) -> d.dismiss())
+ .show();
+ break;
+ }
+ }
+
+ private void presentSaveState(@NonNull EditProxyViewModel.SaveState state) {
+ switch (state) {
+ case IDLE:
+ saveButton.setClickable(true);
+ saveButton.setIndeterminateProgressMode(false);
+ saveButton.setProgress(0);
+ break;
+ case IN_PROGRESS:
+ saveButton.setClickable(false);
+ saveButton.setIndeterminateProgressMode(true);
+ saveButton.setProgress(50);
+ break;
+ }
+ }
+
+ private void onSaveClicked() {
+ viewModel.onSaveClicked(proxyText.getText().toString());
+ }
+
+ private void onShareClicked() {
+ String host = proxyText.getText().toString();
+ ShareCompat.IntentBuilder.from(requireActivity())
+ .setText(host)
+ .setType("text/plain")
+ .startChooser();
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/EditProxyViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/EditProxyViewModel.java
new file mode 100644
index 00000000000..e67dafbf376
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/EditProxyViewModel.java
@@ -0,0 +1,99 @@
+package org.thoughtcrime.securesms.preferences;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+
+import org.signal.core.util.concurrent.SignalExecutors;
+import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
+import org.thoughtcrime.securesms.keyvalue.SignalStore;
+import org.thoughtcrime.securesms.net.PipeConnectivityListener;
+import org.thoughtcrime.securesms.util.SignalProxyUtil;
+import org.thoughtcrime.securesms.util.SingleLiveEvent;
+import org.whispersystems.signalservice.internal.configuration.SignalProxy;
+
+import java.util.concurrent.TimeUnit;
+
+public class EditProxyViewModel extends ViewModel {
+
+ private final SingleLiveEvent events;
+ private final MutableLiveData uiState;
+ private final MutableLiveData saveState;
+
+ public EditProxyViewModel() {
+ this.events = new SingleLiveEvent<>();
+ this.uiState = new MutableLiveData<>();
+ this.saveState = new MutableLiveData<>(SaveState.IDLE);
+
+ if (SignalStore.proxy().isProxyEnabled()) {
+ uiState.setValue(UiState.ALL_ENABLED);
+ } else {
+ uiState.setValue(UiState.ALL_DISABLED);
+ }
+ }
+
+ void onToggleProxy(boolean enabled) {
+ if (enabled) {
+ SignalProxy currentProxy = SignalStore.proxy().getProxy();
+
+ if (currentProxy != null) {
+ SignalProxyUtil.enableProxy(currentProxy);
+ }
+ uiState.postValue(UiState.ALL_ENABLED);
+ } else {
+ SignalProxyUtil.disableProxy();
+ uiState.postValue(UiState.ALL_DISABLED);
+ }
+ }
+
+ public void onSaveClicked(@NonNull String host) {
+ String parsedHost = SignalProxyUtil.parseHostFromProxyLink(host);
+ String trueHost = parsedHost != null ? parsedHost : host;
+
+ saveState.postValue(SaveState.IN_PROGRESS);
+
+ SignalExecutors.BOUNDED.execute(() -> {
+ SignalProxyUtil.enableProxy(new SignalProxy(trueHost, 443));
+
+ boolean success = SignalProxyUtil.testWebsocketConnection(TimeUnit.SECONDS.toMillis(10));
+
+ if (success) {
+ events.postValue(Event.PROXY_SUCCESS);
+ } else {
+ SignalProxyUtil.disableProxy();
+ events.postValue(Event.PROXY_FAILURE);
+ }
+
+ saveState.postValue(SaveState.IDLE);
+ });
+ }
+
+ @NonNull LiveData getUiState() {
+ return uiState;
+ }
+
+ public @NonNull LiveData getEvents() {
+ return events;
+ }
+
+ @NonNull LiveData getProxyState() {
+ return ApplicationDependencies.getPipeListener().getState();
+ }
+
+ public @NonNull LiveData getSaveState() {
+ return saveState;
+ }
+
+ enum UiState {
+ ALL_DISABLED, ALL_ENABLED
+ }
+
+ public enum Event {
+ PROXY_SUCCESS, PROXY_FAILURE
+ }
+
+ public enum SaveState {
+ IDLE, IN_PROGRESS
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditAboutFragment.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditAboutFragment.java
index 3df411b75ae..6ed6f1d1be5 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditAboutFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditAboutFragment.java
@@ -167,14 +167,17 @@ private void presentCount(@NonNull String aboutBody) {
private void presentSaveState(@NonNull EditAboutViewModel.SaveState state) {
switch (state) {
case IDLE:
+ saveButton.setClickable(true);
saveButton.setIndeterminateProgressMode(false);
saveButton.setProgress(0);
break;
case IN_PROGRESS:
+ saveButton.setClickable(false);
saveButton.setIndeterminateProgressMode(true);
saveButton.setProgress(50);
break;
case DONE:
+ saveButton.setClickable(false);
Navigation.findNavController(requireView()).popBackStack();
break;
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditProfileNameFragment.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditProfileNameFragment.java
index 015cc263681..4f1c0125de7 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditProfileNameFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditProfileNameFragment.java
@@ -81,14 +81,17 @@ private void initializeViewModel() {
private void presentSaveState(@NonNull EditProfileNameViewModel.SaveState state) {
switch (state) {
case IDLE:
+ saveButton.setClickable(true);
saveButton.setIndeterminateProgressMode(false);
saveButton.setProgress(0);
break;
case IN_PROGRESS:
+ saveButton.setClickable(false);
saveButton.setIndeterminateProgressMode(true);
saveButton.setProgress(50);
break;
case DONE:
+ saveButton.setClickable(false);
Navigation.findNavController(requireView()).popBackStack();
break;
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/proxy/ProxyBottomSheetFragment.java b/app/src/main/java/org/thoughtcrime/securesms/proxy/ProxyBottomSheetFragment.java
new file mode 100644
index 00000000000..18a68599148
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/proxy/ProxyBottomSheetFragment.java
@@ -0,0 +1,117 @@
+package org.thoughtcrime.securesms.proxy;
+
+import android.app.AlertDialog;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.lifecycle.ViewModelProviders;
+
+import com.dd.CircularProgressButton;
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
+
+import org.signal.core.util.logging.Log;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.preferences.EditProxyViewModel;
+import org.thoughtcrime.securesms.util.BottomSheetUtil;
+import org.thoughtcrime.securesms.util.ThemeUtil;
+
+/**
+ * A bottom sheet shown in response to a deep link. Allows a user to set a proxy.
+ */
+public final class ProxyBottomSheetFragment extends BottomSheetDialogFragment {
+
+ private static final String TAG = Log.tag(ProxyBottomSheetFragment.class);
+
+ private static final String ARG_PROXY_LINK = "proxy_link";
+
+ private TextView proxyText;
+ private View cancelButton;
+ private CircularProgressButton useProxyButton;
+ private EditProxyViewModel viewModel;
+
+ public static void showForProxy(@NonNull FragmentManager manager, @NonNull String proxyLink) {
+ ProxyBottomSheetFragment fragment = new ProxyBottomSheetFragment();
+
+ Bundle args = new Bundle();
+ args.putString(ARG_PROXY_LINK, proxyLink);
+ fragment.setArguments(args);
+
+ fragment.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.proxy_bottom_sheet, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ this.proxyText = view.findViewById(R.id.proxy_sheet_host);
+ this.useProxyButton = view.findViewById(R.id.proxy_sheet_use_proxy);
+ this.cancelButton = view.findViewById(R.id.proxy_sheet_cancel);
+
+ String host = getArguments().getString(ARG_PROXY_LINK);
+ proxyText.setText(host);
+
+ initViewModel();
+
+ useProxyButton.setOnClickListener(v -> viewModel.onSaveClicked(host));
+ cancelButton.setOnClickListener(v -> dismiss());
+ }
+
+ private void initViewModel() {
+ this.viewModel = ViewModelProviders.of(this).get(EditProxyViewModel.class);
+
+ viewModel.getSaveState().observe(getViewLifecycleOwner(), this::presentSaveState);
+ viewModel.getEvents().observe(getViewLifecycleOwner(), this::presentEvents);
+ }
+
+ private void presentSaveState(@NonNull EditProxyViewModel.SaveState state) {
+ switch (state) {
+ case IDLE:
+ useProxyButton.setClickable(true);
+ useProxyButton.setIndeterminateProgressMode(false);
+ useProxyButton.setProgress(0);
+ break;
+ case IN_PROGRESS:
+ useProxyButton.setClickable(false);
+ useProxyButton.setIndeterminateProgressMode(true);
+ useProxyButton.setProgress(50);
+ break;
+ }
+ }
+
+ private void presentEvents(@NonNull EditProxyViewModel.Event event) {
+ switch (event) {
+ case PROXY_SUCCESS:
+ Toast.makeText(requireContext(), R.string.ProxyBottomSheetFragment_successfully_connected_to_proxy, Toast.LENGTH_LONG).show();
+ dismiss();
+ break;
+ case PROXY_FAILURE:
+ new AlertDialog.Builder(requireContext())
+ .setTitle(R.string.preferences_failed_to_connect)
+ .setMessage(R.string.preferences_couldnt_connect_to_the_proxy)
+ .setPositiveButton(android.R.string.ok, (d, i) -> d.dismiss())
+ .show();
+ dismiss();
+ break;
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java b/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java
index 4826e2cea13..2b580b790b0 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java
@@ -231,7 +231,7 @@ public SignalServiceNetworkAccess(Context context) {
new SignalStorageUrl[] {new SignalStorageUrl(BuildConfig.STORAGE_URL, new SignalServiceTrustStore(context))},
interceptors,
dns,
- Optional.absent(),
+ SignalStore.proxy().isProxyEnabled() ? Optional.of(SignalStore.proxy().getProxy()) : Optional.absent(),
zkGroupServerPublicParams);
this.censoredCountries = this.censorshipConfiguration.keySet().toArray(new String[0]);
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 91c7b50a355..992da003416 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java
@@ -32,6 +32,7 @@
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.joining.GroupJoinUpdateRequiredBottomSheetDialogFragment;
import org.thoughtcrime.securesms.groups.v2.GroupInviteLinkUrl;
import org.thoughtcrime.securesms.permissions.Permissions;
+import org.thoughtcrime.securesms.proxy.ProxyBottomSheetFragment;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.WebRtcCallService;
@@ -207,6 +208,21 @@ public static void handleGroupLinkUrl(@NonNull FragmentActivity activity,
});
}
+ /**
+ * If the url is a proxy link it will handle it.
+ * Otherwise returns false, indicating was not a proxy link.
+ */
+ public static boolean handlePotentialProxyLinkUrl(@NonNull FragmentActivity activity, @NonNull String potentialProxyLinkUrl) {
+ String proxy = SignalProxyUtil.parseHostFromProxyLink(potentialProxyLinkUrl);
+
+ if (proxy != null) {
+ ProxyBottomSheetFragment.showForProxy(activity.getSupportFragmentManager(), proxy);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
private static void startInsecureCallInternal(@NonNull Activity activity, @NonNull Recipient recipient) {
try {
Intent dialIntent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + recipient.requireSmsAddress()));
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SignalProxyUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/SignalProxyUtil.java
new file mode 100644
index 00000000000..8cda94a8173
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/SignalProxyUtil.java
@@ -0,0 +1,123 @@
+package org.thoughtcrime.securesms.util;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+import androidx.lifecycle.Observer;
+
+import org.conscrypt.Conscrypt;
+import org.signal.core.util.logging.Log;
+import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
+import org.thoughtcrime.securesms.keyvalue.SignalStore;
+import org.thoughtcrime.securesms.net.PipeConnectivityListener;
+import org.whispersystems.signalservice.internal.configuration.SignalProxy;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public final class SignalProxyUtil {
+
+ private static final String TAG = Log.tag(SignalProxyUtil.class);
+
+ private static final String PROXY_LINK_HOST = "signal.tube";
+
+ private SignalProxyUtil() {}
+
+ public static void startListeningToWebsocket() {
+ ApplicationDependencies.getIncomingMessageObserver();
+ }
+
+ /**
+ * Handles all things related to enabling a proxy, including saving it and resetting the relevant
+ * network connections.
+ */
+ public static void enableProxy(@NonNull SignalProxy proxy) {
+ SignalStore.proxy().enableProxy(proxy);
+ Conscrypt.setUseEngineSocketByDefault(true);
+ ApplicationDependencies.resetNetworkConnectionsAfterProxyChange();
+ startListeningToWebsocket();
+ }
+
+ /**
+ * Handles all things related to disabling a proxy, including saving the change and resetting the
+ * relevant network connections.
+ */
+ public static void disableProxy() {
+ SignalStore.proxy().disableProxy();
+ Conscrypt.setUseEngineSocketByDefault(false);
+ ApplicationDependencies.resetNetworkConnectionsAfterProxyChange();
+ startListeningToWebsocket();
+ }
+
+ /**
+ * A blocking call that will wait until the websocket either successfully connects, or fails.
+ * It is assumed that the app state is already configured how you would like it, e.g. you've
+ * already configured a proxy if relevant.
+ *
+ * @return True if the connection is successful within the specified timeout, otherwise false.
+ */
+ @WorkerThread
+ public static boolean testWebsocketConnection(long timeout) {
+ startListeningToWebsocket();
+
+ CountDownLatch latch = new CountDownLatch(1);
+ AtomicBoolean success = new AtomicBoolean(false);
+
+ Observer observer = state -> {
+ if (state == PipeConnectivityListener.State.CONNECTED) {
+ success.set(true);
+ latch.countDown();
+ } else if (state == PipeConnectivityListener.State.FAILURE) {
+ success.set(false);
+ latch.countDown();
+ }
+ };
+
+ Util.runOnMainSync(() -> ApplicationDependencies.getPipeListener().getState().observeForever(observer));
+
+ try {
+ latch.await(timeout, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ Log.w(TAG, "Interrupted!", e);
+ } finally {
+ Util.runOnMainSync(() -> ApplicationDependencies.getPipeListener().getState().removeObserver(observer));
+ }
+
+ return success.get();
+ }
+
+ /**
+ * If this is a valid proxy link, this will return the embedded host. If not, it will return
+ * null.
+ */
+ public static @Nullable String parseHostFromProxyLink(@NonNull String proxyLink) {
+ try {
+ URI uri = new URI(proxyLink);
+
+ if (!"https".equalsIgnoreCase(uri.getScheme())) {
+ return null;
+ }
+
+ if (!PROXY_LINK_HOST.equalsIgnoreCase(uri.getHost())) {
+ return null;
+ }
+
+ String path = uri.getPath();
+
+ if (Util.isEmpty(path) || "/".equals(path)) {
+ return null;
+ }
+
+ if (path.startsWith("/")) {
+ return path.substring(1);
+ } else {
+ return path;
+ }
+ } catch (URISyntaxException e) {
+ return null;
+ }
+ }
+}
diff --git a/app/src/main/res/drawable-night/ic_proxy_connected_24.xml b/app/src/main/res/drawable-night/ic_proxy_connected_24.xml
new file mode 100644
index 00000000000..c2427ddaad1
--- /dev/null
+++ b/app/src/main/res/drawable-night/ic_proxy_connected_24.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/app/src/main/res/drawable-night/ic_proxy_connecting_24.xml b/app/src/main/res/drawable-night/ic_proxy_connecting_24.xml
new file mode 100644
index 00000000000..ea644a3478c
--- /dev/null
+++ b/app/src/main/res/drawable-night/ic_proxy_connecting_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable-night/ic_proxy_failed_24.xml b/app/src/main/res/drawable-night/ic_proxy_failed_24.xml
new file mode 100644
index 00000000000..a08e2e7583a
--- /dev/null
+++ b/app/src/main/res/drawable-night/ic_proxy_failed_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_proxy_connected_24.xml b/app/src/main/res/drawable/ic_proxy_connected_24.xml
new file mode 100644
index 00000000000..beda943e834
--- /dev/null
+++ b/app/src/main/res/drawable/ic_proxy_connected_24.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_proxy_connecting_24.xml b/app/src/main/res/drawable/ic_proxy_connecting_24.xml
new file mode 100644
index 00000000000..5668ea09d06
--- /dev/null
+++ b/app/src/main/res/drawable/ic_proxy_connecting_24.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_proxy_failed_24.xml b/app/src/main/res/drawable/ic_proxy_failed_24.xml
new file mode 100644
index 00000000000..c202dcea6ed
--- /dev/null
+++ b/app/src/main/res/drawable/ic_proxy_failed_24.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/proxy_avatar_96.xml b/app/src/main/res/drawable/proxy_avatar_96.xml
new file mode 100644
index 00000000000..1cc237f1078
--- /dev/null
+++ b/app/src/main/res/drawable/proxy_avatar_96.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/conversation_list_fragment.xml b/app/src/main/res/layout/conversation_list_fragment.xml
index 636f2a967fd..34f5aaaa882 100644
--- a/app/src/main/res/layout/conversation_list_fragment.xml
+++ b/app/src/main/res/layout/conversation_list_fragment.xml
@@ -39,6 +39,7 @@
tools:src="@drawable/ic_contact_picture" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/proxy_bottom_sheet.xml b/app/src/main/res/layout/proxy_bottom_sheet.xml
new file mode 100644
index 00000000000..28997689846
--- /dev/null
+++ b/app/src/main/res/layout/proxy_bottom_sheet.xml
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 4580b949403..1364b2c2fb0 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1396,6 +1396,14 @@
Can\'t receive audio and video from %1$s
This may be because they have not verified your safety number change, there\'s a problem with their device, or they have blocked you.
+
+ Proxy server
+ Proxy address
+ Do you want to use this proxy address?
+ Use proxy
+ Successfully connected to proxy.
+
+
Select your country
You must specify your
@@ -2303,6 +2311,21 @@
Enable sealed sender for incoming messages from non-contacts and people with whom you have not shared your profile.
Learn more
Setup a username
+ Proxy
+ Use proxy
+ Off
+ On
+ Proxy address
+ Share
+ Save
+ Connecting to proxy…
+ Connected to proxy
+ Connection failed
+ Couldn\'t connect to the proxy. Check the proxy address and try again.
+ You are connected to the proxy. You can turn the proxy off at any time from Settings.
+ Success
+ Failed to connect
+
Customize option
diff --git a/app/src/main/res/xml/preferences_data_and_storage.xml b/app/src/main/res/xml/preferences_data_and_storage.xml
index 26864ab56bb..8c7b4345e56 100644
--- a/app/src/main/res/xml/preferences_data_and_storage.xml
+++ b/app/src/main/res/xml/preferences_data_and_storage.xml
@@ -44,4 +44,15 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/TlsProxySocketFactory.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/TlsProxySocketFactory.java
index a5eb3f792a6..a404a64744d 100644
--- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/TlsProxySocketFactory.java
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/TlsProxySocketFactory.java
@@ -9,13 +9,11 @@
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
-import java.net.SocketOption;
import java.net.UnknownHostException;
import java.nio.channels.SocketChannel;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
-import java.util.Set;
import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/websocket/ConnectivityListener.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/websocket/ConnectivityListener.java
index 0f06de8b092..bae701edb49 100644
--- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/websocket/ConnectivityListener.java
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/websocket/ConnectivityListener.java
@@ -1,9 +1,12 @@
package org.whispersystems.signalservice.api.websocket;
+import okhttp3.Response;
+
public interface ConnectivityListener {
void onConnected();
void onConnecting();
void onDisconnected();
void onAuthenticationFailure();
+ boolean onGenericFailure(Response response, Throwable throwable);
}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/websocket/WebSocketConnection.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/websocket/WebSocketConnection.java
index afb454040e5..294a238ff43 100644
--- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/websocket/WebSocketConnection.java
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/websocket/WebSocketConnection.java
@@ -304,7 +304,15 @@ public synchronized void onFailure(WebSocket webSocket, Throwable t, Response re
Log.w(TAG, "onFailure()", t);
if (response != null && (response.code() == 401 || response.code() == 403)) {
- if (listener != null) listener.onAuthenticationFailure();
+ if (listener != null) {
+ listener.onAuthenticationFailure();
+ }
+ } else if (listener != null) {
+ boolean shouldRetryConnection = listener.onGenericFailure(response, t);
+ if (!shouldRetryConnection) {
+ Log.w(TAG, "Experienced a failure, and the listener indicated we should not retry the connection. Disconnecting.");
+ disconnect();
+ }
}
if (client != null) {
From 30563ed3e5f79922a0298f046b431451443adf54 Mon Sep 17 00:00:00 2001
From: Greyson Parrelli
Date: Tue, 2 Feb 2021 19:56:50 -0500
Subject: [PATCH 13/17] Allow using a proxy during registration.
---
.../dependencies/ApplicationDependencies.java | 13 +++--
.../messages/IncomingMessageObserver.java | 11 +++--
.../net/PipeConnectivityListener.java | 7 ++-
.../preferences/EditProxyFragment.java | 47 +++++++++++--------
.../preferences/EditProxyViewModel.java | 12 +++--
.../fragments/EnterPhoneNumberFragment.java | 30 ++++++++++++
.../securesms/util/SignalProxyUtil.java | 22 ++++++---
...agment_registration_enter_phone_number.xml | 9 +++-
app/src/main/res/menu/enter_phone_number.xml | 7 +++
app/src/main/res/navigation/registration.xml | 14 ++++++
10 files changed, 130 insertions(+), 42 deletions(-)
create mode 100644 app/src/main/res/menu/enter_phone_number.xml
diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java
index a60f37855e3..3671715a4f4 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java
@@ -184,12 +184,10 @@ public static void resetSignalServiceMessageReceiver() {
}
}
- public static void resetNetworkConnectionsAfterProxyChange() {
+ public static void closeConnectionsAfterProxyFailure() {
synchronized (LOCK) {
- getPipeListener().reset();
-
if (incomingMessageObserver != null) {
- incomingMessageObserver.terminate();
+ incomingMessageObserver.terminateAsync();
}
if (messageSender != null) {
@@ -203,6 +201,13 @@ public static void resetNetworkConnectionsAfterProxyChange() {
}
}
+ public static void resetNetworkConnectionsAfterProxyChange() {
+ synchronized (LOCK) {
+ getPipeListener().reset();
+ closeConnectionsAfterProxyFailure();
+ }
+ }
+
public static @NonNull SignalServiceNetworkAccess getSignalServiceNetworkAccess() {
return provider.provideSignalServiceNetworkAccess();
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.java b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.java
index c9f8924c048..f49cab0ee7f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.java
@@ -17,6 +17,7 @@
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner;
+import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
@@ -160,10 +161,12 @@ private synchronized void waitForConnectionNecessary() {
}
}
- public void terminate() {
- Log.w(TAG, "Beginning termination.");
- terminated = true;
- shutdown(pipe, unidentifiedPipe);
+ public void terminateAsync() {
+ SignalExecutors.BOUNDED.execute(() -> {
+ Log.w(TAG, "Beginning termination.");
+ terminated = true;
+ shutdown(pipe, unidentifiedPipe);
+ });
}
private void shutdown(@Nullable SignalServiceMessagePipe pipe, @Nullable SignalServiceMessagePipe unidentifiedPipe) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/net/PipeConnectivityListener.java b/app/src/main/java/org/thoughtcrime/securesms/net/PipeConnectivityListener.java
index ed3fad2212d..628f878e24c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/net/PipeConnectivityListener.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/net/PipeConnectivityListener.java
@@ -47,7 +47,10 @@ public void onConnecting() {
@Override
public void onDisconnected() {
Log.w(TAG, "onDisconnected()");
- state.postValue(State.DISCONNECTED);
+
+ if (state.getValue() != State.FAILURE) {
+ state.postValue(State.DISCONNECTED);
+ }
}
@Override
@@ -65,7 +68,7 @@ public boolean onGenericFailure(Response response, Throwable throwable) {
if (SignalStore.proxy().isProxyEnabled()) {
Log.w(TAG, "Encountered an error while we had a proxy set! Terminating the connection to prevent retry spam.");
- ApplicationDependencies.getIncomingMessageObserver().terminate();
+ ApplicationDependencies.closeConnectionsAfterProxyFailure();
return false;
} else {
return true;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/EditProxyFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/EditProxyFragment.java
index a001fac050c..dc150f5cefd 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/preferences/EditProxyFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/EditProxyFragment.java
@@ -5,24 +5,24 @@
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.view.WindowManager;
import android.widget.EditText;
import android.widget.TextView;
-import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.SwitchCompat;
import androidx.core.app.ShareCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProviders;
-import androidx.navigation.Navigation;
import com.dd.CircularProgressButton;
-import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.net.PipeConnectivityListener;
+import org.thoughtcrime.securesms.util.SignalProxyUtil;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.internal.configuration.SignalProxy;
@@ -62,12 +62,15 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
saveButton.setOnClickListener(v -> onSaveClicked());
shareButton.setOnClickListener(v -> onShareClicked());
proxySwitch.setOnCheckedChangeListener((buttonView, isChecked) -> viewModel.onToggleProxy(isChecked));
+
+ requireActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
}
@Override
public void onResume() {
super.onResume();
- ((ApplicationPreferencesActivity) requireActivity()).requireSupportActionBar().setTitle(R.string.preferences_use_proxy);
+ ((AppCompatActivity) requireActivity()).getSupportActionBar().setTitle(R.string.preferences_use_proxy);
+ SignalProxyUtil.startListeningToWebsocket();
}
private void initViewModel() {
@@ -99,26 +102,30 @@ private void presentUiState(@NonNull EditProxyViewModel.UiState uiState) {
shareButton.setEnabled(false);
shareButton.setAlpha(0.5f);
proxyTitle.setAlpha(0.5f);
- proxyStatus.setVisibility(View.GONE);
+ proxyStatus.setVisibility(View.INVISIBLE);
break;
}
}
private void presentProxyState(@NonNull PipeConnectivityListener.State proxyState) {
- switch (proxyState) {
- case DISCONNECTED:
- case CONNECTING:
- proxyStatus.setText(R.string.preferences_connecting_to_proxy);
- proxyStatus.setTextColor(getResources().getColor(R.color.signal_text_secondary));
- break;
- case CONNECTED:
- proxyStatus.setText(R.string.preferences_connected_to_proxy);
- proxyStatus.setTextColor(getResources().getColor(R.color.signal_accent_green));
- break;
- case FAILURE:
- proxyStatus.setText(R.string.preferences_connection_failed);
- proxyStatus.setTextColor(getResources().getColor(R.color.signal_alert_primary));
- break;
+ if (SignalStore.proxy().getProxy() != null) {
+ switch (proxyState) {
+ case DISCONNECTED:
+ case CONNECTING:
+ proxyStatus.setText(R.string.preferences_connecting_to_proxy);
+ proxyStatus.setTextColor(getResources().getColor(R.color.signal_text_secondary));
+ break;
+ case CONNECTED:
+ proxyStatus.setText(R.string.preferences_connected_to_proxy);
+ proxyStatus.setTextColor(getResources().getColor(R.color.signal_accent_green));
+ break;
+ case FAILURE:
+ proxyStatus.setText(R.string.preferences_connection_failed);
+ proxyStatus.setTextColor(getResources().getColor(R.color.signal_alert_primary));
+ break;
+ }
+ } else {
+ proxyStatus.setText("");
}
}
@@ -134,7 +141,7 @@ private void presentEvent(@NonNull EditProxyViewModel.Event event) {
.show();
break;
case PROXY_FAILURE:
- proxyStatus.setVisibility(View.GONE);
+ proxyStatus.setVisibility(View.INVISIBLE);
proxyText.setText(Optional.fromNullable(SignalStore.proxy().getProxy()).transform(SignalProxy::getHost).or(""));
new AlertDialog.Builder(requireContext())
.setTitle(R.string.preferences_failed_to_connect)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/EditProxyViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/EditProxyViewModel.java
index e67dafbf376..f198144f65a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/preferences/EditProxyViewModel.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/EditProxyViewModel.java
@@ -11,20 +11,24 @@
import org.thoughtcrime.securesms.net.PipeConnectivityListener;
import org.thoughtcrime.securesms.util.SignalProxyUtil;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
+import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.internal.configuration.SignalProxy;
import java.util.concurrent.TimeUnit;
public class EditProxyViewModel extends ViewModel {
- private final SingleLiveEvent events;
- private final MutableLiveData uiState;
- private final MutableLiveData saveState;
+ private final SingleLiveEvent events;
+ private final MutableLiveData uiState;
+ private final MutableLiveData saveState;
+ private final LiveData pipeState;
public EditProxyViewModel() {
this.events = new SingleLiveEvent<>();
this.uiState = new MutableLiveData<>();
this.saveState = new MutableLiveData<>(SaveState.IDLE);
+ this.pipeState = TextSecurePreferences.getLocalNumber(ApplicationDependencies.getApplication()) == null ? new MutableLiveData<>()
+ : ApplicationDependencies.getPipeListener().getState();
if (SignalStore.proxy().isProxyEnabled()) {
uiState.setValue(UiState.ALL_ENABLED);
@@ -78,7 +82,7 @@ public void onSaveClicked(@NonNull String host) {
}
@NonNull LiveData getProxyState() {
- return ApplicationDependencies.getPipeListener().getState();
+ return pipeState;
}
public @NonNull LiveData getSaveState() {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterPhoneNumberFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterPhoneNumberFragment.java
index a8ae24c6e08..5aa806b8cbf 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterPhoneNumberFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterPhoneNumberFragment.java
@@ -7,6 +7,9 @@
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
@@ -20,6 +23,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
@@ -55,6 +60,12 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
private View cancel;
private ScrollView scrollView;
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ }
+
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@@ -99,6 +110,25 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
}
countryCode.getInput().setImeOptions(EditorInfo.IME_ACTION_NEXT);
+
+ Toolbar toolbar = view.findViewById(R.id.toolbar);
+ ((AppCompatActivity) requireActivity()).setSupportActionBar(toolbar);
+ ((AppCompatActivity) requireActivity()).getSupportActionBar().setTitle(null);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
+ inflater.inflate(R.menu.enter_phone_number, menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+ if (item.getItemId() == R.id.phone_menu_use_proxy) {
+ Navigation.findNavController(requireView()).navigate(EnterPhoneNumberFragmentDirections.actionEditProxy());
+ return true;
+ } else {
+ return false;
+ }
}
private void setUpNumberInput() {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SignalProxyUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/SignalProxyUtil.java
index 8cda94a8173..37e1cde09fb 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/SignalProxyUtil.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/SignalProxyUtil.java
@@ -27,6 +27,11 @@ public final class SignalProxyUtil {
private SignalProxyUtil() {}
public static void startListeningToWebsocket() {
+ if (SignalStore.proxy().isProxyEnabled() && ApplicationDependencies.getPipeListener().getState().getValue() == PipeConnectivityListener.State.FAILURE) {
+ Log.w(TAG, "Proxy is in a failed state. Restarting.");
+ ApplicationDependencies.closeConnectionsAfterProxyFailure();
+ }
+
ApplicationDependencies.getIncomingMessageObserver();
}
@@ -63,6 +68,11 @@ public static void disableProxy() {
public static boolean testWebsocketConnection(long timeout) {
startListeningToWebsocket();
+ if (TextSecurePreferences.getLocalNumber(ApplicationDependencies.getApplication()) == null) {
+ Log.i(TAG, "User is unregistered! Assuming success.");
+ return true;
+ }
+
CountDownLatch latch = new CountDownLatch(1);
AtomicBoolean success = new AtomicBoolean(false);
@@ -105,17 +115,17 @@ public static boolean testWebsocketConnection(long timeout) {
return null;
}
- String path = uri.getPath();
+ String fragment = uri.getFragment();
- if (Util.isEmpty(path) || "/".equals(path)) {
+ if (Util.isEmpty(fragment)) {
return null;
}
- if (path.startsWith("/")) {
- return path.substring(1);
- } else {
- return path;
+ if (fragment.startsWith("#")) {
+ fragment = fragment.substring(1);
}
+
+ return Util.isEmpty(fragment) ? null : fragment;
} catch (URISyntaxException e) {
return null;
}
diff --git a/app/src/main/res/layout/fragment_registration_enter_phone_number.xml b/app/src/main/res/layout/fragment_registration_enter_phone_number.xml
index 000fb9bacb2..dcb0bed2d65 100644
--- a/app/src/main/res/layout/fragment_registration_enter_phone_number.xml
+++ b/app/src/main/res/layout/fragment_registration_enter_phone_number.xml
@@ -12,6 +12,12 @@
android:layout_width="match_parent"
android:layout_height="0dp">
+
+
+ app:layout_constraintTop_toBottomOf="@id/toolbar" />
+
\ No newline at end of file
diff --git a/app/src/main/res/navigation/registration.xml b/app/src/main/res/navigation/registration.xml
index ae841b01470..4fffd87df6b 100644
--- a/app/src/main/res/navigation/registration.xml
+++ b/app/src/main/res/navigation/registration.xml
@@ -94,6 +94,14 @@
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
+
+
+
+
\ No newline at end of file
From cfd4399685aff7ca584cd022d404ddc733c2c089 Mon Sep 17 00:00:00 2001
From: Greyson Parrelli
Date: Tue, 2 Feb 2021 20:14:35 -0500
Subject: [PATCH 14/17] Remove conversation update min width.
---
app/src/main/res/layout/conversation_item_update.xml | 1 -
1 file changed, 1 deletion(-)
diff --git a/app/src/main/res/layout/conversation_item_update.xml b/app/src/main/res/layout/conversation_item_update.xml
index 33046f0d5ae..b5aff3d77b0 100644
--- a/app/src/main/res/layout/conversation_item_update.xml
+++ b/app/src/main/res/layout/conversation_item_update.xml
@@ -14,7 +14,6 @@
android:id="@+id/conversation_update_background"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:minWidth="100dp"
android:layout_gravity="center"
android:layout_marginTop="@dimen/conversation_update_vertical_margin"
android:layout_marginBottom="@dimen/conversation_update_vertical_margin"
From 4bb214cb2af6a10091438067202138ccc61aed59 Mon Sep 17 00:00:00 2001
From: AsamK
Date: Sun, 31 Jan 2021 17:03:44 +0100
Subject: [PATCH 15/17] Configure keep alive duration for okhttp connection
pool to 1 minute.
The signal http server supports http keep alive, but closes idle
connections after 1 minute.
The default OkHttp connection pool will keep idle connections in the pool
for 5 minutes and doesn't notice it when the server closes connections.
As currently the automatic okhttp retries are disabled, reusing such a
stale connection will be fatal.
Issue is especially severe for incoming calls, which fail because the request
to retrieve the turn servers fails and isn't retried: #10787
---
.../signalservice/internal/push/PushServiceSocket.java | 3 +++
1 file changed, 3 insertions(+)
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java
index 79d5a6dcaba..786abd6dd7d 100644
--- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java
@@ -138,6 +138,7 @@
import okhttp3.Call;
import okhttp3.Callback;
+import okhttp3.ConnectionPool;
import okhttp3.ConnectionSpec;
import okhttp3.Credentials;
import okhttp3.Dns;
@@ -1807,6 +1808,8 @@ private static OkHttpClient createConnectionClient(SignalUrl url, List
Date: Tue, 2 Feb 2021 20:13:27 -0500
Subject: [PATCH 16/17] Updated language translations.
---
app/src/main/res/values-bs/strings.xml | 2 +-
app/src/main/res/values-el/strings.xml | 2 +-
app/src/main/res/values-et/strings.xml | 1 +
app/src/main/res/values-eu/strings.xml | 2 +
app/src/main/res/values-fi/strings.xml | 1 +
app/src/main/res/values-in/strings.xml | 26 ++
app/src/main/res/values-iw/strings.xml | 1 +
app/src/main/res/values-ja/strings.xml | 24 +-
app/src/main/res/values-ku/strings.xml | 6 +
app/src/main/res/values-mk/strings.xml | 17 +-
app/src/main/res/values-ml/strings.xml | 73 +++---
app/src/main/res/values-mr/strings.xml | 14 +-
app/src/main/res/values-sk/strings.xml | 1 +
app/src/main/res/values-sr/strings.xml | 350 +++++++++++++++++++++++--
app/src/main/res/values-sv/strings.xml | 2 +-
app/src/main/res/values-tr/strings.xml | 3 +-
16 files changed, 439 insertions(+), 86 deletions(-)
diff --git a/app/src/main/res/values-bs/strings.xml b/app/src/main/res/values-bs/strings.xml
index afd7752f280..b2ca9260839 100644
--- a/app/src/main/res/values-bs/strings.xml
+++ b/app/src/main/res/values-bs/strings.xml
@@ -677,7 +677,7 @@
Neuspjelo postavljanje slike profila
Dodaj među kontakte u telefonski adresar
- Ova osoba ne nalazi se među Vašim kontaktima
+ Ova se osoba nalazi među Vašim kontaktima
Nestajuće poruke
Boja poruka
Pozadinska slika
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index 36d449ab436..7744c968b03 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -1735,7 +1735,7 @@
Μέσω
Εκκρεμεί
- Στάλθηκε προς
+ Στάλθηκε σε
Στάλθηκε από
Παραδόθηκε σε
Διαβάστηκε από
diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml
index 87890654017..66de09123a6 100644
--- a/app/src/main/res/values-et/strings.xml
+++ b/app/src/main/res/values-et/strings.xml
@@ -1701,6 +1701,7 @@
Teave
Kirjuta paar sõna enda kohta…
+ %1$d/%2$d
Räägi vabalt
Krüptitud
Ole lahke
diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml
index a90ba67a8c1..7fab05088a9 100644
--- a/app/src/main/res/values-eu/strings.xml
+++ b/app/src/main/res/values-eu/strings.xml
@@ -1702,6 +1702,7 @@ Gakoaren elkar-trukeraro mezua jaso da protokoloaren bertsio baliogabe baterako.
Honi buruz
Idatzi zuri buruzko hitz batzuk…
+ %1$d / %2$d
Hitz egin
Zifratuta
Izan atsegina…
@@ -2388,6 +2389,7 @@ Gakoaren elkar-trukeraro mezua jaso da protokoloaren bertsio baliogabe baterako.
Elkarbanatu
Bidali
%1$s,
+ Hainbat solasalditara partekatzea Singal mezuekin bakarrik egin daiteke
Huts egin du erabiltzaile batzuei bidaltzean
Gehienez ere %1$d elkarrizketarekin parteka dezakezu
diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml
index 9897191dab6..b6a6c893502 100644
--- a/app/src/main/res/values-fi/strings.xml
+++ b/app/src/main/res/values-fi/strings.xml
@@ -1695,6 +1695,7 @@ Vastaanotetiin avaintenvaihtoviesti, joka kuuluu väärälle protokollaversiolle
Tiedot
Kuvaile itseäsi muutamalla sanalla…
+ %1$d/%2$d
Puhu vapaasti
Salattu
Ole ystävällinen
diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml
index a33752f16fb..223ca496869 100644
--- a/app/src/main/res/values-in/strings.xml
+++ b/app/src/main/res/values-in/strings.xml
@@ -569,6 +569,7 @@
Senyapkan pemberitahuan
Notifikasi khusus
Menyebut
+ Gambar latar percakapan
Sampai %1$s
Mati
Aktif
@@ -608,6 +609,7 @@
Orang ini ada di kontak Anda
Penghilangan pesan
Warna percakapan
+ Gambar latar percakapan
Blokir
Buka blokir
Lihat nomor keamanan
@@ -1595,6 +1597,11 @@ Menerima pesan pertukaran kunci untuk versi protokol yang tidak valid.
Nama grup MMS khusus dan foto-foto hanya bisa dilihat oleh Anda.
Tentang
+ %1$d/%2$d
+ Ramah
+ Penyuka kopi
+ Seggang untuk mengobrol
+ Istirahat
Ubah nama grup dan foto
Nama grup
@@ -1762,6 +1769,7 @@ Menerima pesan pertukaran kunci untuk versi protokol yang tidak valid.
Kata Sandi MMSC
Laporan pengiriman SMS
Minta laporan pengiriman untuk tiap SMS yang dikirim
+ Data dan penyimpanan
Penyimpanan
Batas panjang percakapan
Simpan pesan
@@ -1771,6 +1779,7 @@ Menerima pesan pertukaran kunci untuk versi protokol yang tidak valid.
Gelap
Penampilan
Tema
+ Gambar latar percakapan
Nonaktifkan PIN
Aktifkan PIN
Pengaturan dasar sistem
@@ -2247,12 +2256,29 @@ Menerima pesan pertukaran kunci untuk versi protokol yang tidak valid.
Teruskan pesan
+ Gambar latar percakapan
+ Atur gambar latar
+ Tema gelap meredupkan gambar latar
+ Hapus gambar latar
+ Hapus gambar latar percakapan ini?
+ Hapus gambar latar? Anda akan menghapus gambar latar semua percakapan.
+ Kembalikan semua gambar latar.
+ Kembalikan semua gambar latar, termasuk gambar untuk percakapan?
Atur ulang
+ Pratinjau gambar latar
+ Atur gambar latar
+ Usap untuk lihat lebih banyak
+ Atur gambar latar untuk semua percakapan
+ Atur gambar latar untuk %1$s
Melihat galeri Anda memerlukan izin akses ke penyimpanan.
+ Pilih gambar latar
+ Atur gambar latar semua percakapan.
+ Atur gambar latar untuk %s.
+ Gagal mengatur gambar latar.
diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml
index 0e2767f43d1..f6445784142 100644
--- a/app/src/main/res/values-iw/strings.xml
+++ b/app/src/main/res/values-iw/strings.xml
@@ -1845,6 +1845,7 @@
אודות
כתוב כמה מילים על עצמך…
+ %1$d/%2$d
דבר בחופשיות
מוצפן
תהיו אדיבים
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index f7f5bab1b2f..585370f2bc0 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -114,7 +114,7 @@
削除する
プロフィール画像を削除しますか?
- グループ写真を削除しますか?
+ グループアイコンを削除しますか?
Signalをアップデートしてください
このバージョンはサポートされていません。メッセージの送受信を続けるには、最新バージョンにアップデートしてください。
@@ -538,7 +538,7 @@
グループの作成に失敗しました
あとで再度試してください。
Signalグループに対応していない連絡先が選択されたため、このグループはMMSとなります。
- MMSグループの名前と画像はあなただけに表示されます。
+ MMSグループの名前とアイコンはあなただけに表示されます。
削除する
SMS連絡先
このグループから %1$s を削除しますか?
@@ -687,8 +687,8 @@
リンク済み端末で使用中のSignalがグループリンクをサポートしていません。このグループに参加するには、リンク済み端末のSignalをアップデートしてください。
グループリンクが正しくありません
- 友達を招待しましょう
- 友達とリンクを共有して、すぐにこのグループに参加できるようにしましょう。
+ 友達を招待
+ 友達とリンクを共有して、すぐにこのグループに参加してもらいましょう。
リンクを有効にして共有する
リンクを共有する
グループリンクを有効にできません。再度試してください。
@@ -781,7 +781,7 @@
PINの確認
始めましょう
新規グループ
- 友達を招待する
+ 友達を招待しましょう
SMSを使用する
Signal通話中
@@ -848,7 +848,7 @@
グループを作成しました。
グループが更新されました。
- グループリンク経由で友達をこのグループに招待する
+ グループリンク経由で友達をこのグループに招待しましょう
%1$s を追加しました。
%1$s が %2$s を追加しました。
@@ -908,9 +908,9 @@
%1$s がグループ名を「%2$s」に変更しました。
グループ名が「%1$s」に変更されました。
- グループのアバターを変更しました。
- %1$s がグループの画像を変更しました。
- グループのアバターが変更されました。
+ グループアイコンを変更しました。
+ %1$s がグループアイコンを変更しました。
+ グループアイコンが変更されました。
グループ情報を編集できるユーザを「%1$s」に変更しました。
%1$s がグループ情報を編集できるユーザを「%2$s」に変更しました。
@@ -1507,7 +1507,7 @@
通話に参加する
通話に戻る
満席です
- 友達を招待する
+ 友達を招待
再生 … 停止
ダウンロード
@@ -1615,7 +1615,7 @@
次へ
ユーザ名
ユーザ名の作成
- MMSグループの名前と画像はあなただけに表示されます。
+ MMSグループの名前とアイコンはあなただけに表示されます。
自己紹介
簡単な自己紹介を記入してください…
@@ -1628,7 +1628,7 @@
休憩中
新しいことをしています
- グループ名と画像の編集
+ グループ名とアイコンの編集
グループ名
あなたの名前
diff --git a/app/src/main/res/values-ku/strings.xml b/app/src/main/res/values-ku/strings.xml
index b8f53e38fc6..13625befb8d 100644
--- a/app/src/main/res/values-ku/strings.xml
+++ b/app/src/main/res/values-ku/strings.xml
@@ -980,6 +980,8 @@
Asteng bike
Astengê rake
Ev Koma Kevn êdî nikare bê bikaranîn, ji ber ku ew gelek mezine. Mezintaya koma a herî zêde %1$d e.
+ Tu dixwazî tevlî vê komê bibî û nav û wêneyê xwe bi endamên komê re parve bikî? Heta tu qebûl nekî ew ê nizanibin ku te peyamên wan dîtiye.
+ Li tevlî vê komê bibî? Heta tu qebûl nekî ew ê nizanibin ku te peyamên wan dîtine.
Tu dixwazî astenga vê komê rakî û nav û wêneyê xwe bi endamên komê re parve bikî? Heta ku tu astengê ranekî tu yê ti peyaman wernegirî.
Endamê koma %1$s\'ê
Endamê komên %1$s û %2$s\'ê
@@ -1509,6 +1511,7 @@
Welat tên barkirin…
Legerîn
+ Tu welatên lihevtên nînin
Ji bo girê bidî, kamerayê bide ser QR koda li ser cîhazê tê xuyan
@@ -1666,6 +1669,7 @@
Pêşve
Bi me re têkeve têkiliyê
Ji me re bibêje çi diqewime
+ Tomargeha neqandina çewtiyan bixe tev
Ev çi ye?
Tu xwe çawa his dikî?
Agahiya piştgiriyê
@@ -2012,6 +2016,7 @@
PIN\'ê neçalak bike
PIN\'a Signala xwe bikevê
+ Ji bo ku em alîkariya bîranîn PINa we bikin, em ê dem bi dem ji we bipirsin ku hun wê bikevinê. Demê tên da emê wê kêmtir bipirsin.
Derbas bibe
Bişîne
Te PIN\'a xwe ji bîr kir?
@@ -2250,6 +2255,7 @@
Parve bike
Bişîne
%1$s,
+ Parvekirina zêdetir axaftina tenê ji bo peyamên Signal tê piştgirîkirin
Ji hinek bikarhêneran re nehate şandin
Tu dikarî herî zêde bi %1$d gotûbêjan re parve bikî
diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml
index 3c4e4b1581b..29ad2fd8bd8 100644
--- a/app/src/main/res/values-mk/strings.xml
+++ b/app/src/main/res/values-mk/strings.xml
@@ -28,8 +28,8 @@
Постави ја Signal како стандардна SMS апликација
вклучено
Вклучено
- исклучи
- Исклучи
+ исклучено
+ Исклучено
SMS %1$s, MMS %2$s
Заклучување на екранот %1$s, Заклучување на регистрацијата %2$s
Тема %1$s, Јазик %2$s
@@ -419,7 +419,7 @@
Додај во група
Додај во групи
Оваа личност не може да биде додадена во стари групи.
- Додадете…
+ Додај…
Избери нов администратор
Готово
@@ -550,7 +550,7 @@
Готово
Оваа личност не може да биде додадена во стари групи.
- - Да го додадам %1$s во %2$s?
+ - Да го/ја додадам %1$s во %2$s?
- Да ги додадам %3$d членови во \"%2$s\"?
Додадете…
@@ -604,7 +604,7 @@
Спомнувања
Позадина за разговорот
До %1$s
- Исклучи
+ Исклучено
Вклучи
Види ги сите членови
Види сѐ
@@ -654,7 +654,7 @@
Исклучи известувања
Прилагодени известувања
До %1$s
- Исклучи
+ Исклучено
Вклучено
Додај во група
Види ги сите групи
@@ -1697,6 +1697,7 @@
За
Неколку зборови за Вас…
+ %1$d/%2$d
Тука сум, кажи
Енкриптирано
Бидете љубезни
@@ -1735,11 +1736,11 @@
Преку
На чекање
- Испрати на
+ Испратено на
Испратено од
Испорачано на
Прочитано од
- Не е испратено
+ Не е испратено на
Неуспешно праќање
Нов сигурносен број
diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml
index 1b2b2eba3e2..7e420cd3e3f 100644
--- a/app/src/main/res/values-ml/strings.xml
+++ b/app/src/main/res/values-ml/strings.xml
@@ -87,6 +87,7 @@
%1$s-നെ തടഞ്ഞത് മാറ്റണോ?
തടയുക
ബ്ലോക്ക് ചെയ്ത് വിടുക
+ തടഞ്ഞ് ഇല്ലാതാക്കൂ
ഇന്ന്
ഇന്നലെ
@@ -162,6 +163,7 @@
അയയ്ക്കുന്നത് പരാജയപ്പെട്ടു
കീ കൈമാറ്റ സന്ദേശം ലഭിച്ചു, പ്രോസസ്സുചെയ്യാൻ തൊടുക
%1$s ഈ ഗ്രൂപ്പില് നിന്നും പോയി.
+ അയയ്ക്കാൻ പറ്റിയില്ല, ഭദ്രമല്ലാത്ത ഇതരരീതിക്കായി തൊടുക
എൻക്രിപ്റ്റ് ചെയ്യാത്ത SMS പകരം ഉപയോഗിക്കട്ടെ?
എൻക്രിപ്റ്റ് ചെയ്യാത്ത MMS പകരം ഉപയോഗിക്കട്ടെ?
സ്വീകർത്താവ് ഇപ്പോൾ ഒരു Signal ഉപയോക്താവല്ലാത്തതിനാൽ ഈ സന്ദേശം എൻക്രിപ്റ്റ് ചെയ്യില്ല. പകരം, സുരക്ഷിതമല്ലാത്ത സന്ദേശം അയയ്ക്കണോ?
@@ -196,6 +198,7 @@
നമുക്ക് Signal ലേക്ക് മാറാം %1$s
ദയവായി ഒരു കോൺടാക്റ്റ് തിരഞ്ഞെടുക്കുക
തടഞ്ഞത് മാറ്റുക
+ അറ്റാച്ചുമെന്റ് നിങ്ങൾ അയയ്ക്കുന്ന സന്ദേശ ഇനത്തിന്റെ വലുപ്പ പരിധി കവിയുന്നു.
ഓഡിയോ റെക്കോർഡുചെയ്യാനായില്ല!
നിങ്ങൾ മേലിൽ അംഗമല്ലാത്തതിനാൽ നിങ്ങൾക്ക് ഈ ഗ്രൂപ്പിലേക്ക് സന്ദേശങ്ങൾ അയയ്ക്കാൻ കഴിയില്ല.
നിങ്ങളുടെ ഉപകരണത്തിൽ ഈ ലിങ്ക് കൈകാര്യം ചെയ്യാൻ ഒരു അപ്ലിക്കേഷനും ലഭ്യമല്ല.
@@ -319,13 +322,13 @@
നിങ്ങളുടെ പ്രൊഫൈൽ ഏൻഡ് ടു ഏൻഡ് എൻക്രിപ്റ്റഡാണ്. നിങ്ങൾ പുതിയ സംഭാഷണങ്ങൾ ആരംഭിക്കുമ്പോഴോ സ്വീകരിക്കുമ്പോഴോ പുതിയ ഗ്രൂപ്പുകളിൽ ചേരുമ്പോഴോ നിങ്ങളുടെ പ്രൊഫൈലും അതിലെ മാറ്റങ്ങളും നിങ്ങളുടെ കോൺടാക്റ്റുകൾക്ക് ദൃശ്യമാകും.
അവതാർ സജ്ജമാക്കുക
- ബാക്കപ്പിൽ നിന്ന് റിസ്റ്റോർ ചെയ്യണോ?
+ ബാക്കപ്പിൽ വീണ്ടെടുക്കണോ?
ഒരു ലോക്കൽ ബാക്കപ്പിൽ നിന്ന് നിങ്ങളുടെ സന്ദേശങ്ങളും മീഡിയയും റിസ്റ്റോർ ചെയ്യുക. നിങ്ങൾ ഇപ്പോൾ റിസ്റ്റോർ ചെയ്തില്ലെങ്കിൽ, നിങ്ങൾക്ക് പിന്നീട് റിസ്റ്റോർ ചെയ്യാൻ കഴിയില്ല.
- ബാക്കപ്പ് ഐക്കണിൽ നിന്ന് റിസ്റ്റോർ ചെയ്യുക
+ ബാക്കപ്പ് ഐക്കണിൽ നിന്ന് വീണ്ടെടുക്കൂ
ബാക്കപ്പ് തിരഞ്ഞെടുക്കുക
കൂടുതൽ അറിയുക
- റീസ്റ്റോര് ചെയ്യല് പൂർത്തിയായി
+ വീണ്ടെടുക്കൽ പൂർത്തിയായി
ബാക്കപ്പുകൾ ഉപയോഗിക്കുന്നത് തുടരാൻ, ദയവായി ഒരു ഫോൾഡർ തിരഞ്ഞെടുക്കുക. പുതിയ ബാക്കപ്പുകൾ ഇവിടെ സൂക്ഷിക്കും.
ഫോൾഡർ തിരഞ്ഞെടുക്കുക
ഇപ്പോൾ വേണ്ട
@@ -364,22 +367,23 @@
ചാറ്റ് സെഷൻ പുതുക്കി
Signal ആദ്യാവസാന-എൻക്രിപ്ഷൻ ഉപയോക്കുന്നതിനാൽ ചിലപ്പോൾ മുമ്പത്തെ ചാറ്റുകൾ പുതുക്കേണ്ടി വരും. ഇത് ചാറ്റിന്റെ സുരക്ഷയെ ബാധിക്കില്ല, പക്ഷെ ഈ കോണ്ടാക്ടിൽ നിന്നുള്ളൊരു മെസ്സേജ് നിങ്ങൾക്ക് നഷ്ടപെട്ടിരിക്കാം, എങ്കിൽ അതവരോട് നിങ്ങൾക്ക് വീണ്ടുമയക്കാൻ പറയാം.
- അൺലിങ്ക് \'%s\'?
+ \'%s\' വിച്ഛേദിക്കണോ?
ഈ ഉപകരണം അൺലിങ്കുചെയ്യുന്നതിലൂടെ, ഇതിന് മേലിൽ സന്ദേശങ്ങൾ അയയ്ക്കാനോ സ്വീകരിക്കാനോ കഴിയില്ല.
നെറ്റ്വർക്ക് കണക്ഷൻ പരാജയപ്പെട്ടു
വീണ്ടും ശ്രമിക്കുക
- ഉപകരണം അൺലിങ്കുചെയ്യുന്നു…
- ഉപകരണം അൺലിങ്കുചെയ്യുന്നു…
+ ഉപകരണം വിച്ഛേദിക്കുന്നൂ…
+ ഉപകരണം വിച്ഛേദിക്കുന്നൂ…
നെറ്റ്വർക്ക് പരാജയപ്പെട്ടു!
പേരിടാത്ത ഉപകരണം
- %sലിങ്കുചെയ്തു
+ %s ബന്ധിപ്പിച്ചൂ
അവസാനം സജീവമായ %s
ഇന്ന്
പേരിടാത്ത ഫയൽ
Signal-ന് സംഭാവന ചെയ്യുക
+ Signal-ന്റെ പിന്നണിയിൽ നിങ്ങളെപോലുള്ളവരാണ്. നിങ്ങളുടെ പിന്തുണ അറിയിക്കൂ!
സംഭാവനചെയ്യുക
വേണ്ട, നന്ദി
@@ -575,15 +579,15 @@
- %d അംഗങ്ങൾക്ക് പുതിയ ഗ്രൂപ്പുകള് പിന്തുണയ്ക്കുന്നില്ല, അതിനാൽ ഈ ഗ്രൂപ്പ് സൃഷ്ടിക്കാൻ കഴിയില്ല.
- “%1$s” Signal(സിഗ്നലി)ന്റെ പഴയ പതിപ്പ് ഉപയോഗിക്കുന്നതിനാൽ ഒരു ലെഗസി ഗ്രൂപ്പ് സൃഷ്ടിക്കപ്പെടും. അവര് Signal(സിഗ്നൽ) അപ്ഡേറ്റുചെയ്തതിനുശേഷം നിങ്ങൾക്ക് അവരുടെ കൂടെ പുതിയ ശൈലിയില് ഒരു ഗ്രൂപ്പ് സൃഷ്ടിക്കാൻ കഴിയും, അല്ലെങ്കിൽ ഗ്രൂപ്പ് സൃഷ്ടിക്കുന്നതിന് മുമ്പ് അവരെ നീക്കംചെയ്യുക.
+ “%1$s” Signal ന്റെ പഴയ പതിപ്പ് ഉപയോഗിക്കുന്നതിനാൽ ഒരു ലെഗസി ഗ്രൂപ്പ് സൃഷ്ടിക്കപ്പെടും. അവര് Signal അപ്ഡേറ്റുചെയ്തതിനുശേഷം നിങ്ങൾക്ക് അവരുടെ കൂടെ പുതിയ ശൈലിയില് ഒരു ഗ്രൂപ്പ് സൃഷ്ടിക്കാൻ കഴിയും, അല്ലെങ്കിൽ ഗ്രൂപ്പ് സൃഷ്ടിക്കുന്നതിന് മുമ്പ് അവരെ നീക്കംചെയ്യുക.
- - %1$d അംഗം Signal(സിഗ്നലി)ന്റെ പഴയ പതിപ്പ് ഉപയോഗിക്കുന്നതിനാൽ ഒരു ലെഗസി ഗ്രൂപ്പ് സൃഷ്ടിക്കപ്പെടും. അവര് Signal(സിഗ്നൽ) അപ്ഡേറ്റുചെയ്തതിനുശേഷം നിങ്ങൾക്ക് അവരുടെ കൂടെ പുതിയ ശൈലിയില് ഒരു ഗ്രൂപ്പ് സൃഷ്ടിക്കാൻ കഴിയും, അല്ലെങ്കിൽ ഗ്രൂപ്പ് സൃഷ്ടിക്കുന്നതിന് മുമ്പ് അവരെ നീക്കംചെയ്യുക.
- - %1$d അംഗങ്ങള് Signal(സിഗ്നലി)ന്റെ പഴയ പതിപ്പ് ഉപയോഗിക്കുന്നതിനാൽ ഒരു ലെഗസി ഗ്രൂപ്പ് സൃഷ്ടിക്കപ്പെടും. അവര് Signal(സിഗ്നൽ) അപ്ഡേറ്റുചെയ്തതിനുശേഷം നിങ്ങൾക്ക് അവരുടെ കൂടെ പുതിയ ശൈലിയില് ഒരു ഗ്രൂപ്പ് സൃഷ്ടിക്കാൻ കഴിയും, അല്ലെങ്കിൽ ഗ്രൂപ്പ് സൃഷ്ടിക്കുന്നതിന് മുമ്പ് അവരെ നീക്കംചെയ്യുക.
+ - %1$d അംഗം Signal ന്റെ പഴയ പതിപ്പ് ഉപയോഗിക്കുന്നതിനാൽ ഒരു ലെഗസി ഗ്രൂപ്പ് സൃഷ്ടിക്കപ്പെടും. അവര് Signal അപ്ഡേറ്റുചെയ്തതിനുശേഷം നിങ്ങൾക്ക് അവരുടെ കൂടെ പുതിയ ശൈലിയില് ഒരു ഗ്രൂപ്പ് സൃഷ്ടിക്കാൻ കഴിയും, അല്ലെങ്കിൽ ഗ്രൂപ്പ് സൃഷ്ടിക്കുന്നതിന് മുമ്പ് അവരെ നീക്കംചെയ്യുക.
+ - %1$d അംഗങ്ങള് Signal ന്റെ പഴയ പതിപ്പ് ഉപയോഗിക്കുന്നതിനാൽ ഒരു ലെഗസി ഗ്രൂപ്പ് സൃഷ്ടിക്കപ്പെടും. അവര് Signal അപ്ഡേറ്റുചെയ്തതിനുശേഷം നിങ്ങൾക്ക് അവരുടെ കൂടെ പുതിയ ശൈലിയില് ഒരു ഗ്രൂപ്പ് സൃഷ്ടിക്കാൻ കഴിയും, അല്ലെങ്കിൽ ഗ്രൂപ്പ് സൃഷ്ടിക്കുന്നതിന് മുമ്പ് അവരെ നീക്കംചെയ്യുക.
- \"%1$s\" Signal(സിഗ്നലി)ന്റെ പഴയ പതിപ്പ് ഉപയോഗിക്കുന്നതിനാൽ ഈ ഗ്രൂപ്പ് സൃഷ്ടിക്കാൻ കഴിയില്ല. ഗ്രൂപ്പ് സൃഷ്ടിക്കുന്നതിന് മുമ്പ് നിങ്ങൾ അവരെ നീക്കംചെയ്യണം.
+ \"%1$s\" Signal ന്റെ പഴയ പതിപ്പ് ഉപയോഗിക്കുന്നതിനാൽ ഈ ഗ്രൂപ്പ് സൃഷ്ടിക്കാൻ കഴിയില്ല. ഗ്രൂപ്പ് സൃഷ്ടിക്കുന്നതിന് മുമ്പ് നിങ്ങൾ അവരെ നീക്കംചെയ്യണം.
- %1$d അംഗം Signal(സിഗ്നലി)ന്റെ പഴയ പതിപ്പ് ഉപയോഗിക്കുന്നതിനാൽ ഈ ഗ്രൂപ്പ് സൃഷ്ടിക്കാൻ കഴിയില്ല. ഗ്രൂപ്പ് സൃഷ്ടിക്കുന്നതിന് മുമ്പ് നിങ്ങൾ അവരെ നീക്കംചെയ്യണം.
- - %1$d അംഗങ്ങള് Signal(സിഗ്നലി)ന്റെ പഴയ പതിപ്പ് ഉപയോഗിക്കുന്നതിനാൽ ഈ ഗ്രൂപ്പ് സൃഷ്ടിക്കാൻ കഴിയില്ല. ഗ്രൂപ്പ് സൃഷ്ടിക്കുന്നതിന് മുമ്പ് നിങ്ങൾ അവരെ നീക്കംചെയ്യണം.
+ - %1$d അംഗങ്ങള് Signal ന്റെ പഴയ പതിപ്പ് ഉപയോഗിക്കുന്നതിനാൽ ഈ ഗ്രൂപ്പ് സൃഷ്ടിക്കാൻ കഴിയില്ല. ഗ്രൂപ്പ് സൃഷ്ടിക്കുന്നതിന് മുമ്പ് നിങ്ങൾ അവരെ നീക്കംചെയ്യണം.
അംഗത്വ അഭ്യർത്ഥനകളും ക്ഷണങ്ങളും
@@ -717,7 +721,7 @@
ഗ്രൂപ്പ് ലിങ്കുകൾ ഉപയോഗിക്കുന്നതിന് Signal അപ്ഡേറ്റുചെയ്യുക
നിങ്ങൾ ഉപയോഗിക്കുന്ന Signal-ന്റെ പതിപ്പ് ഈ ഗ്രൂപ്പ് ലിങ്കിനെ പിന്തുണയ്ക്കുന്നില്ല. ലിങ്ക് വഴി ഈ ഗ്രൂപ്പിൽ ചേരുന്നതിന് ഏറ്റവും പുതിയ പതിപ്പിലേക്ക് അപ്ഡേറ്റുചെയ്യുക.
Signal അപ്ഡേറ്റ് ചെയ്യുക
- നിങ്ങളുടെ ലിങ്കുചെയ്ത ഒന്നോ അതിലധികമോ ഉപകരണങ്ങൾ ഗ്രൂപ്പ് ലിങ്കുകളെ പിന്തുണയ്ക്കാത്ത Signal(സിഗ്നലി)ന്റെ ഒരു പതിപ്പ് പ്രവർത്തിപ്പിക്കുന്നു. ഈ ഗ്രൂപ്പിൽ ചേരുന്നതിന് നിങ്ങളുടെ ലിങ്കുചെയ്ത ഉപകരണത്തിൽ (ഉപകരണങ്ങളിൽ) സിഗ്നൽ അപ്ഡേറ്റുചെയ്യുക.
+ നിങ്ങളുടെ ലിങ്കുചെയ്ത ഒന്നോ അതിലധികമോ ഉപകരണങ്ങൾ ഗ്രൂപ്പ് ലിങ്കുകളെ പിന്തുണയ്ക്കാത്ത Signal ന്റെ ഒരു പതിപ്പ് പ്രവർത്തിപ്പിക്കുന്നു. ഈ ഗ്രൂപ്പിൽ ചേരുന്നതിന് നിങ്ങളുടെ ലിങ്കുചെയ്ത ഉപകരണത്തിൽ (ഉപകരണങ്ങളിൽ) സിഗ്നൽ അപ്ഡേറ്റുചെയ്യുക.
ഗ്രൂപ്പ് ലിങ്ക് അസാധുവാണ്
സുഹൃത്തുക്കളെ ക്ഷണിക്കുക
@@ -1071,15 +1075,15 @@
• നിങ്ങളുടെ എല്ലാ സന്ദേശങ്ങളും വായിക്കുക
\n• നിങ്ങളുടെ പേരിൽ സന്ദേശങ്ങൾ അയയ്ക്കുക
- ഉപകരണം ലിങ്കുചെയ്യുന്നു
- പുതിയ ഉപകരണം ലിങ്കുചെയ്യുന്നു…
+ ഉപകരണം ബന്ധിപ്പിക്കുന്നൂ
+ പുതിയ ഉപകരണം ബന്ധിപ്പിക്കുന്നൂ…
ഉപകരണം അംഗീകരിച്ചു!
ഉപകരണങ്ങളൊന്നും കണ്ടെത്തിയില്ല.
നെറ്റ്വർക്ക് പിശക്.
QR കോഡ് അസാധുവാണ്.
ക്ഷമിക്കണം, നിങ്ങൾക്ക് ഇതിനകം തന്നെ നിരവധി ഉപകരണങ്ങൾ ലിങ്കുചെയ്തിട്ടുണ്ട്, ചിലത് നീക്കംചെയ്യാൻ ശ്രമിക്കുക
ക്ഷമിക്കണം, ഇത് സാധുവായ ഉപകരണ ലിങ്ക് QR കോഡല്ല.
- ഒരു Signal ഉപകരണം ലിങ്കുചെയ്യണോ?
+ ഒരു Signal ഉപകരണം ബന്ധിപ്പിക്കണോ?
ഒരു മൂന്നാം കക്ഷി സ്കാനർ ഉപയോഗിച്ച് നിങ്ങൾ ഒരു Signal ഉപകരണം ലിങ്കുചെയ്യാൻ ശ്രമിക്കുന്നതായി തോന്നുന്നു. നിങ്ങളുടെ പരിരക്ഷയ്ക്കായി, Signal-നുള്ളിൽ നിന്ന് കോഡ് വീണ്ടും സ്കാൻ ചെയ്യുക.
ഒരു QR കോഡ് സ്കാൻ ചെയ്യുന്നതിന് Signal-ന് ക്യാമറ അനുമതി ആവശ്യമാണ്, പക്ഷേ ഇത് ശാശ്വതമായി നിരസിക്കപ്പെട്ടു. അപ്ലിക്കേഷൻ ക്രമീകരണങ്ങളിൽ തുടരുക, \"അനുമതികൾ\" തിരഞ്ഞെടുത്ത് \"ക്യാമറ\" പ്രവർത്തനക്ഷമമാക്കുക.
ക്യാമറ അനുമതിയില്ലാതെ ഒരു QR കോഡ് സ്കാൻ ചെയ്യാൻ കഴിയില്ല
@@ -1099,7 +1103,7 @@
Signal ഐക്കൺ
പാസ്ഫ്രെയ്സ് സമർപ്പിക്കുക
പാസ്ഫ്രെയ്സ് അസാധുവാണ്!
- Signal അൺലോക്കുചെയ്യുക
+ Signal തുറക്കൂ
ഭൂപടം
ഡ്രോപ്പ് പിൻ
@@ -1399,7 +1403,7 @@
സിസ്റ്റം ഡാറ്റാബേസ് ഇറക്കുമതി പൂർത്തിയായി.
തുറക്കാൻ തൊടുക
- Signal അൺലോക്കുചെയ്തിരിക്കുന്നു
+ Signal തുറന്നിരിക്കുന്നൂ
Signal പൂട്ടുക
നിങ്ങൾ
@@ -1415,7 +1419,7 @@
%2$d സംഭാഷണങ്ങളിൽ %1$d പുതിയ സന്ദേശങ്ങൾ
ഏറ്റവും പുതിയത്: %1$s നിന്ന്
- ലോക്കുചെയ്ത സന്ദേശം
+ പൂട്ടിയ സന്ദേശം
സന്ദേശ വിതരണം പരാജയപ്പെട്ടു.
സന്ദേശം കൈമാറുന്നതിൽ പരാജയപ്പെട്ടു.
സന്ദേശം കൈമാറുന്നതിൽ പിശക്.
@@ -1480,7 +1484,7 @@
%s നിന്നുള്ള കോളിന് മറുപടി നൽകാൻ, നിങ്ങളുടെ മൈക്രോഫോണിലേക്ക് Signal-ന് ആക്സസ് നൽകുക.
കോളുകൾ വിളിക്കുന്നതിനോ സ്വീകരിക്കുന്നതിനോ Signal-ന് മൈക്രോഫോൺ, ക്യാമറ അനുമതികൾ ആവശ്യമാണ്, പക്ഷേ അവ ശാശ്വതമായി നിരസിക്കപ്പെട്ടു. അപ്ലിക്കേഷൻ ക്രമീകരണങ്ങളിൽ തുടരുക, \"അനുമതികൾ\" തിരഞ്ഞെടുത്ത് \"മൈക്രോഫോൺ\", \"ക്യാമറ\" എന്നിവ പ്രവർത്തനക്ഷമമാക്കുക.
- ലിങ്കുചെയ്ത ഉപകരണത്തിൽ ഉത്തരം നൽകി.
+ ഒരു ബന്ധിപ്പിച്ച ഉപകരണത്തിൽ ഉത്തരം നൽകി.
ലിങ്കുചെയ്ത ഉപകരണത്തിൽ നിരസിച്ചു.
ലിങ്കുചെയ്ത ഉപകരണത്തിൽ തിരക്കിലാണ്.
മാറ്റിയ സുരക്ഷാ നമ്പറുമായി ആരോ ഈ കോളിൽ ചേർന്നിട്ടുണ്ട്.
@@ -1601,10 +1605,10 @@
ലിങ്കുചെയ്യുന്നതിന് ഉപകരണത്തിൽ പ്രദർശിപ്പിച്ചിരിക്കുന്ന QR കോഡ് സ്കാൻ ചെയ്യുക
- ഉപകരണം ലിങ്കുചെയ്യുക
+ ഉപകരണം ബന്ധിപ്പിക്കൂ
ഉപകരണങ്ങളൊന്നും ലിങ്കുചെയ്തിട്ടില്ല
- പുതിയ ഉപകരണം ലിങ്കുചെയ്യുക
+ പുതിയ ഉപകരണം ബന്ധിപ്പിക്കൂ
ഓഫ്
@@ -1675,7 +1679,7 @@
ഗ്രൂപ്പിന്റെ പേര് ഇപ്പോൾ \'%1$s\' ആണ്.
- അൺലോക്കുചെയ്യുക
+ അൺലോക്കുചെയ്യൂ
നിങ്ങളുടെ വയർലെസ് കാരിയർ വഴി മീഡിയയും ഗ്രൂപ്പ് സന്ദേശങ്ങളും കൈമാറാൻ Signal-ന് MMS ക്രമീകരണങ്ങൾ ആവശ്യമാണ്. നിങ്ങളുടെ ഉപകരണം ഈ വിവരങ്ങൾ ലഭ്യമാക്കുന്നില്ല, ഇത് ലോക്കുചെയ്ത ഉപകരണങ്ങൾക്കും മറ്റ് നിയന്ത്രിത കോൺഫിഗറേഷനുകൾക്കും ഇടയ്ക്കിടെ ശരിയാണ്.
മീഡിയയും ഗ്രൂപ്പ് സന്ദേശങ്ങളും അയയ്ക്കാൻ, \'ശരി\' ടാപ്പുചെയ്ത് അഭ്യർത്ഥിച്ച ക്രമീകരണങ്ങൾ പൂർത്തിയാക്കുക. \'നിങ്ങളുടെ കാരിയർ APN\' എന്നതിനായി തിരയുന്നതിലൂടെ നിങ്ങളുടെ കാരിയറിനായുള്ള MMS ക്രമീകരണങ്ങൾ സാധാരണയായി കണ്ടെത്താനാകും. നിങ്ങൾ ഇത് ഒരു തവണ ചെയ്താൽ മതി.
@@ -1689,6 +1693,7 @@
കുറിച്ച്
നിങ്ങളെ പറ്റി രണ്ടുവാക്ക് എഴുതൂ…
+ %1$d/%2$d
സ്വതന്ത്രമായി സംസാരിക്കുക
എന്ക്രിപ്റ്റ് ചെയ്തത്
ദയ കാട്ടുക
@@ -1743,7 +1748,7 @@
ഡീബഗ് ലോഗ് സമർപ്പിക്കുക
മീഡിയ പ്രിവ്യൂ
സന്ദേശ വിശദാംശങ്ങൾ
- ലിങ്കുചെയ്ത ഉപകരണങ്ങൾ
+ ബന്ധിപ്പിച്ച ഉപകരണങ്ങൾ
സുഹൃത്തുക്കളെ ക്ഷണിക്കുക
ആർക്കൈവുചെയ്ത സംഭാഷണങ്ങൾ
ഫോട്ടോ നീക്കംചെയ്യുക
@@ -1872,7 +1877,7 @@
സംഭാഷണ ദൈർഘ്യ പരിധി
സന്ദേശങ്ങൾ സൂക്ഷിക്കുക
സന്ദേശ ചരിത്രം മായ്ക്കുക
- ലിങ്കുചെയ്ത ഉപകരണങ്ങൾ
+ ബന്ധിപ്പിച്ച ഉപകരണങ്ങൾ
ലൈറ്റ്
ഡാർക്ക്
ദൃശ്യത
@@ -2175,7 +2180,9 @@
ഞങ്ങൾ സ്വകാര്യതയിൽ വിശ്വസിക്കുന്നു.
Signal നിങ്ങളെ ട്രാക്കുചെയ്യുകയോ നിങ്ങളുടെ ഡാറ്റ ശേഖരിക്കുകയോ ചെയ്യുന്നില്ല. എല്ലാവർക്കുമായി Signal മെച്ചപെടുത്താൻ, ഞങ്ങൾ ഉപയോക്തൃ ഫീഡ്ബാക്കിനെ ആശ്രയിക്കുന്നു, നിങ്ങളുടെ ഫീഡ്ബാക്ക് ലഭിക്കാൻ ഞങ്ങൾ ആഗ്രഹിക്കുന്നു.
നിങ്ങൾ Signal എങ്ങനെ ഉപയോഗിക്കുന്നുവെന്ന് മനസിലാക്കാൻ ഞങ്ങൾ ഒരു സർവേ നടത്തുന്നുണ്ട് . നിങ്ങളെ തിരിച്ചറിയുന്ന ഒരു ഡാറ്റയും ഞങ്ങളുടെ സർവേ ശേഖരിക്കില്ല. അധിക ഫീഡ്ബാക്ക് പങ്കിടാൻ നിങ്ങൾക്ക് താൽപ്പര്യമുണ്ടെങ്കിൽ, നിങ്ങളെ ബന്ധപ്പെടാനുള്ള കോൺടാക്ട് വിവരങ്ങൾ നൽകാനുള്ള ഓപ്ഷൻ നിങ്ങൾക്ക് ലഭിക്കും.
നിങ്ങൾക്ക് കുറച്ച് സമയവും ഫീഡ്ബാക്കും വാഗ്ദാനം ചെയ്യാൻ ഉണ്ടെങ്കിൽ, അത് നിങ്ങളിൽ നിന്ന് കേൾക്കാൻ ഞങ്ങൾ സന്തോഷത്തോടെ ആഗ്രഹിക്കുന്നു.
]]>
സർവേ എടുക്കുക
വേണ്ട, നന്ദി
+ സുരക്ഷിത ഡൊമെയ്ൻ ആയ surveys.signalusers.org-ൽ Alchemer ഹോസ്റ്റ് ചെയ്തതാണ് ഈ സർവേ
+ ട്രാൻസ്പോർട് ഐക്കൺ
ലഭ്യമാക്കുന്നു…
ബന്ധിപ്പിക്കുന്നു…
അനുമതി ആവശ്യമാണ്
@@ -2185,23 +2192,23 @@
SIGNAL സന്ദേശങ്ങൾ പ്രവർത്തനക്ഷമമാക്കുക
Signal ഡാറ്റാബേസ് മൈഗ്രേറ്റുചെയ്യുന്നു
ലോക്കുചെയ്ത പുതിയ സന്ദേശം
- തീർച്ചപ്പെടുത്തിയിട്ടില്ലാത്ത സന്ദേശങ്ങൾ കാണുന്നതിന് അൺലോക്ക് ചെയ്യുക
+ തീർച്ചപ്പെടുത്തിയിട്ടില്ലാത്ത സന്ദേശങ്ങൾ കാണുന്നതിന് തുറക്കൂ
പാസ്ഫ്രേസ് ബാക്കപ്പ് ചെയ്യുക
ബാക്കപ്പുകൾ ബാഹ്യ സ്റ്റോറേജിലെക് സംരക്ഷിക്കുകയും ചുവടെയുള്ള പാസ്ഫ്രെയ്സ് ഉപയോഗിച്ച് എൻക്രിപ്റ്റ് ചെയ്യുകയും ചെയ്യും. ഒരു ബാക്കപ്പ് പുന റിസ്റ്റോർ ചെയ്യാൻ നിങ്ങൾക്ക് ഈ പാസ്ഫ്രേസ് ഉണ്ടായിരിക്കണം.
- ഒരു ബാക്കപ്പ് പുന.സ്ഥാപിക്കുന്നതിന് നിങ്ങൾക്ക് ഈ പാസ്ഫ്രെയ്സ് ഉണ്ടായിരിക്കണം.
+ ഒരു ബാക്കപ്പ് വീണ്ടെടുക്കുന്നതിന് നിങ്ങൾക്ക് ഈ പാസ്ഫ്രെയ്സ് ഉണ്ടായിരിക്കണം.
ഫോള്ഡര്
ഞാൻ ഈ പാസ്ഫ്രെയ്സ് എഴുതി. ഇത് ഇല്ലാതെ, എനിക്ക് ഒരു ബാക്കപ്പ് പുന റിസ്റ്റോർ ചെയ്യാൻ കഴിയില്ല.
- ബാക്കപ്പ് റിസ്റ്റോർ ചെയ്യുക
+ ബാക്കപ്പ് വീണ്ടെടുക്കൂ
ഒഴിവാക്കുക
ചാറ്റ് ബാക്കപ്പുകൾ
ബാഹ്യ സ്റ്റോറേജിലെക് ചാറ്റുകൾ ബാക്കപ്പ് ചെയ്യുക
ബാക്കപ്പ് പാസ്ഫ്രെയ്സ് നൽകുക
- റിസ്റ്റോർ
+ വീണ്ടെടുക്കൂ
Signal-ന്റെ പുതിയ പതിപ്പുകളിൽ നിന്ന് ബാക്കപ്പുകൾ ഇറക്കുമതി ചെയ്യാൻ കഴിയില്ല
തെറ്റായ ബാക്കപ്പ് പാസ്ഫ്രെയ്സ്
പരിശോധിക്കുന്നു…
ഇതുവരെ %d സന്ദേശങ്ങൾ…
- ബാക്കപ്പിൽ നിന്ന് റിസ്റ്റോർ ചെയ്യാൻ ആഗ്രഹിക്കുന്നുണ്ടോ?
+ ബാക്കപ്പിൽ നിന്ന് വീണ്ടെടുക്കണോ?
ഒരു ലോക്കൽ ബാക്കപ്പിൽ നിന്ന് നിങ്ങളുടെ സന്ദേശങ്ങളും മീഡിയയും റിസ്റ്റോർ ചെയ്യുക. നിങ്ങൾ ഇപ്പോൾ റിസ്റ്റോർ ചെയ്തില്ലെങ്കിൽ, നിങ്ങൾക്ക് പിന്നീട് റിസ്റ്റോർ ചെയ്യാൻ കഴിയില്ല.
ബാക്കപ്പ് വലുപ്പം: %s
ബാക്കപ്പ് ടൈംസ്റ്റാമ്പ്: %s
@@ -2248,6 +2255,7 @@
PIN ഓർമ്മപ്പെടുത്തലുകൾ
Signal എൻക്രിപ്റ്റ് ചെയ്ത് സ്റ്റോർ ചെയ്ത വിവരങ്ങൾ PIN കൾ സൂക്ഷിക്കുന്നു, അതിനാൽ നിങ്ങൾക്ക് മാത്രമെ അത് പ്രാപ്യമാവുകയുള്ളു. നിങ്ങൾ Signal റീഇൻസ്റ്റാൾ ചെയ്യുമ്പോൾ, നിങ്ങളുടെ പ്രൊഫൈൽ, സെറ്റിംഗ്സ്, കോണ്ടാക്റ്റുകൾ എന്നിവ റീസ്റ്റോർ ചെയ്യപ്പെടും.
Signal ഉപയോഗിച്ച് നിങ്ങളുടെ ഫോൺ നമ്പർ വീണ്ടും രജിസ്റ്റർ ചെയ്യുന്നതിന് നിങ്ങളുടെ Signal PIN ആവശ്യപ്പെടുന്നതിലൂടെ അധിക സുരക്ഷ ചേർക്കുക.
+ നിങ്ങളുടെ പിൻ വീണ്ടെടുക്കാൻ പറ്റാത്തതിനാൽ ഓർമ്മിക്കാൻ ഓർമ്മപ്പെടുത്തലുകൾ സഹായിക്കും. പലപ്പോഴായി നിങ്ങളോടു ചോദിക്കും.
ഓഫ് ആക്കുക
PIN ഉറപ്പാക്കു
നിങ്ങളുടെ Signal PIN ഉറപ്പാക്കു
@@ -2264,8 +2272,8 @@
നിങ്ങൾ വളരെയധികം ശ്രമങ്ങൾ നടത്തി. പിന്നീട് വീണ്ടും ശ്രമിക്കുക.
സേവനത്തിലേക്ക് കണക്റ്റുചെയ്യുന്നതിൽ പിശക്
ബാക്കപ്പുകൾ
- Signal പൂട്ടിയിരിക്കുന്നു
- അൺലോക്കുചെയ്യാൻ തൊടുക
+ Signal പൂട്ടിയിരിക്കുന്നൂ
+ തുറക്കാൻ തൊടുക
അജ്ഞാതം
തടയുക
@@ -2373,6 +2381,7 @@
പങ്കിടുക
അയയ്ക്കുക
%1$s,
+ ഒന്നിലധികം ചാറ്റുകളിലേക്ക് പങ്കിടുന്നത് Signal സന്ദേശങ്ങൾക്ക് മാത്രമേ പിന്തുണക്കൂ
ചില ഉപയോക്താക്കള്ക്ക് അയയ്ക്കാന് കഴിഞ്ഞില്ല
നിങ്ങൾക്ക് %1$d ചാറ്റുകൾക്ക് മാത്രമേ പങ്കിടാനാകൂ
diff --git a/app/src/main/res/values-mr/strings.xml b/app/src/main/res/values-mr/strings.xml
index 790b683415a..adb1723dacb 100644
--- a/app/src/main/res/values-mr/strings.xml
+++ b/app/src/main/res/values-mr/strings.xml
@@ -472,8 +472,8 @@
गट अद्यतनित करा
- - %1$d सदस्य गटात जोडणे शक्य झाले नाही. आपल्याला त्याला आता जोडायचे आहे का?
- - %1$d सदस्य गटात जोडणे शक्य झाले नाहीत. आपल्याला त्यांना आता जोडायचे आहे का?
+ - %1$d सदस्य नवीन गटात पुन्हा जोडणे शक्य झाले नाही. आपल्याला त्याला आता जोडायचे आहे का?
+ - %1$d सदस्य नवीन गटात पुन्हा जोडणे शक्य झाले नाहीत. आपल्याला त्यांना आता जोडायचे आहे का?
- सदस्य जोडा
@@ -625,7 +625,7 @@
लेगसी गट
हा एक लेगसी गट आहे. गट अॅडमिन सारखे वैशिष्ट्ये फक्त नवीन गटांसाठी उपलब्ध आहेत.
हा एक लेगसी गट आहे. @उल्लेख आणि अॅडमिन सारखे वैशिष्ट्ये अॅक्सेस करण्यासाठी,
- हा गट नवीन गटामध्ये श्रेणीसुधारित केला जाऊ शकत नाही कारण तो खूप मोठा आहे. गटाचा कमाल आकार %1$d आहे.
+ हा लेगसी गट नवीन गटामध्ये श्रेणीसुधारित केला जाऊ शकत नाही कारण तो खूप मोठा आहे. गटाचा कमाल आकार %1$d आहे.
हा गट श्रेणीसुधारित करा.
हा एक असुरक्षित MMS गट आहे. खाजगीरीत्या चॅट करण्यासाठी, आपल्या संपर्कांना Signal वर आमंत्रित करा.
आता आमंत्रित करा
@@ -1698,6 +1698,8 @@
याबद्दल
स्वतःबद्दल काही शब्द लिहा…
+ %1$d/%2$d
+ मोकळेपणाने बोला
एन्क्रिप्टेड
दयाळू रहा
कॉफी प्रेमी
@@ -1712,6 +1714,7 @@
नाव
शेवटचे नाव (पर्यायी)
जतन करा
+ नेटवर्क त्रुटीमुळे जतन करण्यात अयशस्वी. नंतर पुन्हा प्रयत्न करा.
सामायिक केलेली मिडिया
@@ -2382,6 +2385,7 @@
सामायिक करा
पाठवा
%1$s,
+ अनेक चॅट यांना शेअर करणे फक्त Signal संदेशांसाठी समर्थित आहे
काही वापरकर्त्यांना पाठवण्यात अयशस्वी
आपण फक्त %1$d पर्यंत चॅट सोबत शेअर करू शकता
@@ -2393,7 +2397,10 @@
वॉलपेपर सेट करा
गडद थीम वॉलपेपर अस्पष्ट बनवतो
वॉलपेपर साफ करा
+ या चॅटसाठी वॉलपेपर साफ करायचा?
+ वॉलपेपर साफ करायचा? आपण आपल्या चॅटसाठी सेट केलेले सानुकूलित वॉलपेपर हे साफ करणार नाही.
सर्व वॉलपेपर रीसेट करा
+ सर्व वॉलपेपर रीसेट करा. आपण आपल्या चॅटसाठी सेट केलेले सानुकूलित वॉलपेपर देखील?
संपर्क नाव
रीसेट करा
साफ करा
@@ -2415,5 +2422,6 @@
सर्व चॅटसाठी वॉलपेपर सेट करा.
%s साठी वॉलपेपर सेट करा.
वॉलपेपर सेट करण्यात त्रुटी.
+ फोटो अस्पष्ट करा
diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml
index a7c5f200ec4..36ecf2d3623 100644
--- a/app/src/main/res/values-sk/strings.xml
+++ b/app/src/main/res/values-sk/strings.xml
@@ -1844,6 +1844,7 @@ Bola prijatá správa výmeny kľúčov s neplatnou verziou protokolu.
Informácie
Napíšte pár slov o sebe…
+ %1$d/%2$d
Hovorte slobodne
Šifrovaný
Buďte milí
diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml
index 429bf69d210..a63f7757862 100644
--- a/app/src/main/res/values-sr/strings.xml
+++ b/app/src/main/res/values-sr/strings.xml
@@ -230,7 +230,7 @@
Обриши
Избриши и напусти
Да бисте позвали контакт %1$s, апликацији Signal је потребан приступ Вашем микрофону.
- Сада више опција у „Подешавања групе“
+ Сада више опција у „Поставке групе“
Придружи се
Пуно
@@ -329,7 +329,7 @@
Профил
Грешка постављања профилне слике
Проблем у поставци профила
- Поставите вашу слику
+ Поставите ваш профил
Ваш профил је потпуно шифрован. Профил и промене на њему ће бити видљиве Вашим контактима, када започнете или прихватите нове конверзације и када се придружите новим групама.
Постави аватар
@@ -394,7 +394,7 @@
Датотека без имена
Донирајте услузи Signal
- Signal покрећу људи попут Вас. Покажите своју подршку већ данас!
+ Signal покрећу особе попут Вас. Покажите своју подршку већ данас!
Донирај
Не, хвала
@@ -538,7 +538,7 @@
Сада можете да видите преглед линка са сваког веб сајта у порукама које шаљете.
Преглед веза није доступан
- Веза групе није активна
+ Веза до групе није активна
%1$s · %2$s
@@ -570,7 +570,7 @@
Захтеви за чланство на чекању
Нема захтева за чланство.
- Људи са ове листе покушавају да се придруже овој групи путем везе до групе.
+ Особе са ове листе покушавају да се придруже овој групи путем везе до групе.
Додат је контакт „%1$s“
Одбијен је контакт „%1$s“
@@ -598,16 +598,36 @@
Уклони
SMS контакт
Желите да уклоните %1$s из ове групе?
+
+ - %dчлан не подржава Нове Групе, тако да ће ова група бити Стара Група.
+ - %d чланова не подржава Нове Групе, тако да ће ова група бити Стара Група.
+ - %d чланова не подржава Нове Групе, тако да ће ова група бити Стара Група.
+
+
+ - %d члана не подржава Нове Групе, тако да ова група не може да се креира.
+ - %d чланова не подржава Нове Групе, тако да ова група не може да се креира.
+ - %d чланова не подржава Нове Групе, тако да ова група не може да се креира.
+
- Наслеђена група ће бити створена јер „%1$s“ користи стару верзију Signal-а. Можете да направите Нову Групу Стилова са њима након што ажурирају Signal или да их уклоните пре креирања групе.
+ Стара група ће бити створена јер „%1$s“ користи стару верзију Signal-а. Можете да направите Нову Групу Стилова са њима након што ажурирају Signal или да их уклоните пре креирања групе.
+
+ - Стара Група ће бити креирана јер %1$d члан користи стару верзију Signal-а. Можете да направите Нову Групу Стилова са њим након што ажурира Signal, или да га уклоните пре креирања групе.
+ - Стара Група ће бити креирана јер %1$d чланова користе стару верзију Signal-а. Можете да направите Нову Групу Стилова са њима након што ажурирају Signal, или да их уклоните пре креирања групе.
+ - Стара Група ће бити креирана јер %1$d чланова користе стару верзију Signal-а. Можете да направите Нову Групу Стилова са њима након што ажурирају Signal, или да их уклоните пре креирања групе.
+
Ову групу није могуће креирати јер „%1$s“ користи стару верзију Signal-а. Морате их уклонити пре стварања групе.
+
+ - Ову групу није могуће креирати јер %1$d члан користи стару верзију Signal-а. Морате га уклонити пре стварања групе.
+ - Ову групу није могуће креирати јер %1$d чланова користи стару верзију Signal-а. Морате их уклонити пре стварања групе.
+ - Ову групу није могуће креирати јер %1$d чланова користи стару верзију Signal-а. Морате их уклонити пре стварања групе.
+
Захтеви за чланство & позива
Додајте чланове
Уреди информације о групи
Ко може додати нове чланове?
Ко може мењати информације о групи?
- Веза групе
+ Веза до групе
Блокирај групу
Одблокирај групу
Напусти групу
@@ -637,6 +657,8 @@
Уредите име и слику
Застарела група
Ово је застарела група. Функције попут администратора групе доступне су само за нове групе.
+ Ово је Наледна Група. Да бисте приступили новим функцијама попут @помињања и администратора,
+ Ова Стара Група не може да се надогради на нову групу јер је превелика. Максимална величина групе је %1$d.
надогради ову групу.
Ово је несигурна ММС група. Да бисте приватно ћаскали, позовите своје контакте у Signal.
Позови сада
@@ -699,16 +721,16 @@
Линк за дељење групе
Управи & дели
- Веза групе
+ Веза до групе
Поделите
Ресетуј линк
Захтеви за чланство
Одобри нове чланове
- Захтева да администратор одобри придруживање нових чланова путем везе групе.
- Да ли сте сигурни да желите да укинете везу према групи? Људи више неће моћи да се придруже групи помоћу тренутне везе.
+ Захтева да администратор одобри придруживање нових чланова путем везе до групе.
+ Да ли сте сигурни да желите да укинете везу према групи? Особе више неће моћи да се придруже групи помоћу тренутне везе.
QR кôд
- Људи који скенирају овај код моћи ће да се придруже Вашој групи. Администратори ће и даље морати да одобравају нове захтеве за чланство ако је та поставка укључена.
+ Особе који скенирају овај код моћи ће да се придруже Вашој групи. Администратори ће и даље морати да одобравају нове захтеве за чланство ако је та поставка укључена.
Подели код
Да се опозове позив који сте послали %1$s?
@@ -723,27 +745,29 @@
Захтев за придруживање
Није могуће придружити се групи. Пробајте поново касније.
Дошло је до грешке мреже.
- Веза групе није активна
+ Веза до групе није активна
Није могуће прузети информацију о групи, пробајте поново касније.
Да ли желите да се придружите овој групи и да са њеним члановима поделите ваше име и слику?
+ Администратор ове групе мора одобрити ваш захтев пре него што се придружите овој групи. Када затражите придруживање, ваше име и фотографија ће се делити са члановима.
- Група · %1$d члан
- Група · %1$d чланова
- Група · %1$d чланова
- Ажурирај Signal за коришћење групне везе
- Ваша верзија Signal.а не подржава ову групну везу. Ажурирајте на најновију верзију да бисте се придружили овој групи путем везе.
+ Ажурирај Signal за коришћење везе до групе
+ Ваша верзија Signal.а не подржава ову везу до групе. Ажурирајте на најновију верзију да бисте се придружили овој групи путем везе.
Ажурирајте Signal
- Веза групе није исправна
+ На једном или више повезаних уређаја ради верзија Signal-а која не подржава везе до групе. Ажурирајте Signal на повезаним уређајима да бисте се придружили овој групи.
+ Веза до групе није исправна
Позивница пријатељима
Поделите везу са пријатељима како би се брзо придружили овој групи.
Омогући и дели везу
Веза дељења
- Није могуће омогућити везу групе. Пробајте поново касније
+ Није могуће омогућити везу до групе. Пробајте поново касније
Дошло је до грешке мреже.
- Немате право да омогућите везу групе. Питајте администратора.
+ Немате право да омогућите везу до групе. Питајте администратора.
Тренутно нисте члан групе.
Додај “%1$s„ у ову групу?
@@ -799,6 +823,11 @@
- Обриши изабране датотеке?
- Обриши изабране датотеке?
+
+ - Овиме ћете заувек обрисати означену датотеку. Текст поруке повезане са овој ставком такође ће бити избрисане.
+ - Овиме ћете заувек обрисати %1$d означене датотеке. Текст поруке повезане са овим ставкама такође ће бити избрисане.
+ - Овиме ћете заувек обрисати %1$d означене датотеке. Текст поруке повезане са овим ставкама такође ће бити избрисане.
+
Брисање
Брисање порука…
Изабери све
@@ -885,7 +914,7 @@
пропуштен видео позив од · %1$s
%s ажурира групу.
%1$s вас је позвао/ла · %2$s
- %s је на Signalу!
+ %s је на Signal-у!
Искључили сте нестајуће поруке.
%1$s је онемогућио/ла нестајуће поруке.
Поставили сте време нестајућих порука на %1$s.
@@ -899,12 +928,19 @@
- %1$s чланова нису могли да са додају у нову групу и били су позвани да се придруже.
- %1$s чланова нису могли да са додају у нову групу и били су позвани да се придруже.
+
+ - Члан није могао да са дода у нову групу и био је уклоњен.
+ - %1$s чланова нису могли да са додају у нову групу и били су уклоњени.
+ - %1$s чланова нису могли да са додају у нову групу и били су уклоњени.
+
+ %1$s су променили име профила на %2$s.
+ %1$s су променили име профила од %2$s на %3$s.
%1$s су променили профил.
Направили сте групу.
Група ажурирана.
- Позовите пријатеље у ову групу преко везе групе
+ Позовите пријатеље у ову групу преко везе до групе
Додали сте %1$s.
%1$s је додао/ла %2$s.
@@ -933,6 +969,11 @@
Позвали сте %1$s у групу.
%1$s вас је позвао у групу.
+
+ - %1$s је позвао/ла 1 особу у групу.
+ - %1$s је позвао/ла %2$d особа у групу.
+ - %1$s је позвао/ла %2$d особа у групу.
+
Добили сте позивницу за групу.
- 1 особа је позвана у групу.
@@ -940,17 +981,84 @@
- %1$d особа је позвано у групу.
+
+ - Опозвали сте позивницу за групу.
+ - Опозвали сте %1$d позивнице за групу.
+ - Опозвали сте %1$d позивнице за групу.
+
+
+ - %1$s опозвао позивницу за групу.
+ - %1$s опозвао %2$d позивнице за групу.
+ - %1$s опозвао %2$d позивнице за групу.
+
+ Неко одбио позивницу за групу.
+ Одбили сте позивницу за групу.
+ %1$s је опозвао/ла вашу позивницу за групу.
+ Администратор опозвао/ла вашу позивницу за групу.
+
+ - Позивница за групу је опозвана.
+ - %1$d позивнице за групу су опозване.
+ - %1$d позивнице за групу су опозване.
+
+ Прихватили сте позивницу за групу.
+ %1$s је прихватио/ла позивницу за групу.
+ Додали сте позваног члана %1$s.
+ %1$s је додао позваног члана %2$s.
+ Променили сте име групе у „%1$s”.
+ %1$s је променио/ла име групе у „%2$s”.
+ Име групе је промењено у „%1$s”.
+ Променили сте слику групе.
+ %1$s је променио/ла слику групе.
+ Слика групе је промењена.
+ Поставили сте да дозволу промене описа групе имају „%1$s”.
+ %1$s је поставио/ла да дозволу промене описа групе имају „%2$s”.
+ Ко има дозволу промене описа групе је промењено на „%1$s”.
+ Поставили сте да дозволу измене чланова групе имају „%1$s”.
+ %1$s је поставио/ла да дозволу измене чланова групе имају „%2$s”.
+ Ко има дозволу промене чланова групе је промењено на „%1$s”.
+ Укључили сте везу до групе без одобрења администратора.
+ Укључили сте везу до групе са одобрењем администратора.
+ Искључили сте везу до групе.
+ %1$s је укључио/ла везу до групе без одобрења администратора.
+ %1$s је укључио/ла везу до групе са одобрењем администратора.
+ %1$s је искључио/ла везу до групе.
+ Веза до групе је укључена без одобрења админитратора.
+ Веза до групе је укључена са одобрењем админитратора.
+ Веза до групе је искључена.
+ Искључили сте одобрење администратора за везу до групе.
+ %1$s је искључио/ла одобрење администратора за везу до групе.
+ Одобрење администратора за везу до групе је искључено.
+ Укључили сте одобрење администратора за везу до групе.
+ %1$s је укључио/ла одобрење администратора за везу до групе.
+ Одобрење администратора за везу до групе је укључено.
+ Ресетовали сте везу до групе.
+ %1$s је ресетовао/ла везу до групе.
+ Веза до групе је ресетована.
+ Придружили сте се групи преко везе до групе.
+ %1$s се придружио/ла групи преко везе до групе.
+ Послали сте захтев за придруживање групи.
+ %1$s је затражио/ла да се придружи преко везе до групе.
+ %1$s је прихватио/ла Ваш захтев за придруживање групи.
+ %1$s је прихватио/ла захтев од %2$s за придруживање групи.
+ Одобрили сте захтев од %1$s за придруживање групи.
+ Ваш захтев за придруживање групи је одобрен.
+ Захтев за придруживање групи од %1$s је одобрен.
+ Администратор је одбио/ла ваш захтев за придруживање групи.
+ %1$s је одбио/ла захтев од %2$s за придруживање групи.
+ Захтев за придруживање групи од %1$s је одбијен.
+ Отказали сте Ваш захтев за придруживање групи.
+ %1$s је отказао/ла свој захтев за придруживање групи.
Ваш безбедносни број са %s је промењен.
Означили сте ваш сигурносни број са %s као проверен.
@@ -958,14 +1066,60 @@
Означили сте ваш сигурносни број са %s као непроверен.
Означили сте ваш сигурносни број са %s као непроверен на другом уређају.
+ %1$s покренуо/ла групни позив · %2$s
+ %1$s је у групном позиву · %2$s
+ Ви сте у групном пизиву · %1$s
+ %1$s и %2$s су у групном позиву · %3$s
+ Групни позив · %1$s
+ %1$s покренуо/ла групни позив
+ %1$s је у групном позиву
+ Ви сте у групном пизиву
+ %1$s и %2$s су у групном позиву
+ Групни позив
Ви
+
+ - %1$s, %2$s, и још %3$d су у групном позиву · %4$s
+ - %1$s, %2$s, и још %3$d су у групном позиву %4$s
+ - %1$s, %2$s, и још %3$d су у групном позиву · %4$s
+
+
+ - %1$s, %2$s, и још %3$d су у групном позиву
+ - %1$s, %2$s, и још %3$d су у групном позиву
+ - %1$s, %2$s, и још %3$d су у групном позиву
+
Прихвати
Настави
Обриши
Блокирај
Одблокирај
+ Да ли дозвољавате да вам %1$s шаље поруке и да подели ваше име и слику са њима? Неће знати да сте прочитали њихове поруке док не прихватите.
+ Да ли дозвољавате да вам %1$s шаље поруке и да подели ваше име и слику са њима? Нећете примати поруке док их не деблокирате.
+ Наставити разговор са овом групом и поделите своје име и слику са њеним члановима?
+ Надоградите ову групу да бисте активирали нове функције попут @помињања и администратора. Чланови који нису поделили своје име или слику у овој групи биће позвани да се придруже.
+ Ова Стара Група не може више да се користи јер је превелика. Максимална величина групе је %1$d.
+ Наставити разговор са %1$s и поделите своје име и слику са њима?
+ Да ли желите да се придружите овој групи и да са њеним члановима поделите ваше име и слику? Неће знати да сте прочитали поруке док не прихватите.
+ Придружите се групи? Неће знати да сте им прочитали поруке док не прихватите.
Да ли желите да одблокирате ову групу и да са њеним члановима поделите ваше име и слику? Нећете добити ни једну поруку док је не одблокирате.
+ Члан %1$s
+ Члан од %1$s и %2$s
+ Члан од %1$s, %2$s, и %3$s
+
+ - %1$d члан
+ - %1$d члан(ов)а
+ - %1$d члан(ов)а
+
+
+ - %1$d члан (+%2$d позван(и))
+ - %1$d члан(ов)а (+%2$d позван(и))
+ - %1$d члан(ов)а (+%2$d позван(и))
+
+
+ - %d додатна група
+ - %d додатне групе
+ - %d додатне групе
+
Лозинке се не поклапају!
Нетачна стара лозинка!
@@ -988,12 +1142,20 @@
Нажалост, ово није исправан бар-кôд везе уређаја.
Повезати Signal уређај?
Изгледа да покушавате повезати Signal уређај користећи спољашњи читач бар-кôда. Због сопствене заштите, очитајте кôд поново помоћу Signal-a.
+ Signal захтева приступ камери да би скенирао QR кôд, али су му дозволе трајно забрањене. Молимо вас да у апликацији за подешавање телефона Signal-у дозволите пруиступ камери.
+ Није могуће скенирати QR кôд без дозволе камере
Нестајуће поруке
Ваше поруке неће истећи.
Примљене и послате поруке ове преписке ће нестати %s након што су погледане.
Ажурирај сада
+ Ваша верзија Signal-а истиче данас. Ажурирате на најновију верзију.
+
+ - Ваша верзија Signal-а истиче сутра. Ажурирате на најновију верзију.
+ - Ваша верзија Signal-а истиче за %d дана. Ажурирате на најновију верзију.
+ - Ваша верзија Signal-а истиче за %d дана. Ажурирате на најновију верзију.
+
Унесите лозинку
Икона Signal-a
@@ -1008,18 +1170,29 @@
Инсталирано издање Гуглових Плеј сервиса не функционише исправно. Реинсталирајте Гуглове Плеј сервисе и покушајте поново.
Нетачан PIN
+ Прескочите унос PIN-а?
+ Потребна вам је помоћ?
+ Ваш PIN је кôд од %1$d или више цифре или алфанумеричка карактеракоји сте раније креирали.\n\nАко се не сећате вашег PIN-а, можете направити нови. Регистрација новог налогa је дозвољена, али ћете изгубити нека подешавања као што су личне информације профила.
Ако се не сећате вашег PIN-а, можете направити нови. Регистрација новог налога је дозвољена, али ћете изгубити подешавања и ваше информације.
Направи нови PIN
Контактирајте подршку
Одустани
Прескочи
+
+ - Преостао вам %1$d покушај. Уколико искористите све покушаје, моћи ћете да направите нови PIN. Регистрација новог налогa је дозвољена, али ћете изгубити нека подешавања као што су личне информације профила.
+ - Преостао вам %1$d покушаја. Уколико искористите све покушаје, моћи ћете да направите нови PIN. Регистрација новог налогa је дозвољена, али ћете изгубити нека подешавања као што су личне информације профила.
+ - Преостао вам %1$d покушаја. Уколико искористите све покушаје, моћи ћете да направите нови PIN. Регистрација новог налогa је дозвољена, али ћете изгубити нека подешавања као што су личне информације профила.
+
Signal регистрација - Помоћ за PIN na Android-у
Унеси алфанумерички PIN
Унеси нумерички PIN
Направите ваш PIN
+ Искористили сте све покушаје за унос PIN-а, али још увек можете приступити Signal-у креирањем новог PIN-а. Зарад безбедности и приватности, подешавања налогa и ваше информације неће бити враћене.
+ Направи нови PIN
Упозорење
+ Ако онемогућите PIN, изгубићете све податке када поново региструјете Signal, осим ако ручно направите резервне копије и вратите податке. Не можете укључити закључавање регистрације док је PIN онемогућен.
Искључи PIN
Оцените апликацију
@@ -1029,7 +1202,9 @@
Касније
Упс, изгледа да апликација Плеј продавница није инсталирана на вашем уређају.
+ Све · %1$d
+ +%1$d
Ви
@@ -1038,6 +1213,8 @@
Неименована група
+ Јављање…
+ Завршавам позив…
Звони…
Заузето
Прималац није доступан
@@ -1047,18 +1224,43 @@
Важи
Кликните овде да укључите вашу камеру
+ Да бисте позвали %1$s, апликацији Signal је потребан приступ Вашој камери.
+ Signal %1$s
+ Позивам…
+ Signal гласовни позив…
Сигнал видео позив
Започети позив
Придружи се позиву
Позив је пун
+ За овај позив је достигнут максималан број %1$d учесника. Покушајте поново касније.
\"%1$s\" групни позив
+ Погледајте учеснике
Ваш видео је искључен
Обнављање везе…
+ Придружавање…
Прекинуто
+ Нема никога овде
+ %1$s је у овом позиву
+ %1$s и %2$s су у овом позиву
+
+ - %1$s, %2$s, и још %3$d су у овом позиву
+ - %1$s, %2$s, и још %3$d су у овом позиву
+ - %1$s, %2$s, и још %3$d су у овом позиву
+
+
+ - У овом позиву · %1$d особа
+ - У овом позиву · %1$d људи
+ - У овом позиву · %1$d особа
+
+ %1$s је блокиран
Више информација
+ Нећете добити њихов аудио или видео, а ни они ваш.
+ Не може да се прими звук & видео од %1$s
+ Не може да се прими звук и видео од %1$s
+ То је можда зато што нису потврдили промену вашег сигурносног броја, постоји проблем са њиховим уређајем или су вас блокирали.
Изаберите вашу државу
Морате навести позивни број
@@ -1128,6 +1330,12 @@
Слање позивнице за Signal
Порука на Signalу
+ Подсетићемо вас поново касније.
+ Подсетићемо вас поново сутра.
+ Подсетићемо вас поново за пар дана.
+ Подсетићемо вас поново за недељу дана.
+ Подсетићемо вас поново за пар недеља.
+ Подсетићемо вас поново за месец дана.
Слика
Налепница
@@ -1142,6 +1350,7 @@
Ресетовали сте безбедну сесију.
%s ресетова безбедну сесију.
Порука дупликат.
+ Ову поруку није било могуће обрадити јер је послата из новије верзије Signal-а. Можете затражити од свог контакта да поново пошаље ову поруку након што ажурирате.
Грешка у обради поруке.
Налепнице
@@ -1163,11 +1372,20 @@
Уреди
Готово
+ Додирните линију да је обришете
Пошаљи
+ Неуспех слања извештаја
Успех!
+ Копирајте овај УРЛ и додајте га и вашу пријаву или имејл:\n\n%1$s
Дели
+ Филтер:
+ Инфо о уређају:
+ Верзија Андроида:
+ Верзија Signal-а:
+ Signal пакет:
Закључавање регистрације:
+ Локално:
Група ажурирана.
Напусти групу
@@ -1175,6 +1393,8 @@
Нацрт:
Позвали сте
Позвали вас
+ Пропуштен позив
+ Пропуштен видео позив
Мултимедијална порука
Налепница
Једнократна фотографија
@@ -1182,7 +1402,7 @@
Једнократни медиј
Ова порука је избрисана.
Избрисали сте ову поруку.
- %s је на Signalу!
+ %s је на Signal-у!
Нестајуће поруке искључене
Време нестајања поруке постављено на %s
Безбедносни број промењен
@@ -1209,7 +1429,16 @@
Корисничко име
Обриши
+ Корисничко име постављено.
+ Корисничко име уклоњено.
Дошло је до грешке мреже.
+ Ово корисничко име је узето.
+ Ово корисничко име је доступно.
+ Корисничко име може да садржи a-Z, 0-9 и _
+ Корисничко име не може започети бројем.
+ Корисничко име је неважеће.
+ Корисничко име мора садржати између %1$d и %2$d карактера.
+ Корисничка имена на Signal-у нису обавезна. Ако додате ваше корисничко име, други корисници Signal-а ће моћи да вас по њему пронађу и контактирају, без да знају ваш број телефона.
- %d особа је на Сигналу!
- %d особе су на Сигналу!
@@ -1222,7 +1451,8 @@
Поделите сигурносни број преко…
Наш Signal безбедносни број:
Изгледа да немате ниједну апликацију преко које бисте могли да делите.
- У клипборду нема безбедносног броја за упоређивање.
+ У остави нема безбедносног броја за упоређивање.
+ Signal захтева приступ камери да би скенирао QR кôд, али су му дозволе трајно забрањене. Молимо вас да у апликацији за подешавање телефона Signal-у дозволите пруиступ камери.
Није могуће скенирати QR кôд без дозволе камере
Прво морате разменити поруке са %1$s да бисте видели његов сигурносни број.
@@ -1247,6 +1477,8 @@
Ви
Неподржан тип медијума
Нацрт
+ Апликација Signal захтева дозволу меморији да би сачувавала на спољној меморији, али је трајно одбијено. Наставите до подешавања апликације, одаберите „Дозволе“ и омогућите ставку „Меморија“.
+ Није могуће сачувати у спољном меморији без дозвола
Обрисати поруку?
Ово ће трајно да обрише ову поруку.
%1$s за %2$s
@@ -1279,6 +1511,7 @@
Реакција %1$s на ваш једнократни медиј.
Реакција %1$s на вашу налепницу.
Ова порука је избрисана.
+ Искључити нотификације контакта који се придружио Signal-у? Можете их поново омогућити у Signal > Подешавања > Нотификације.
Подразумеван
Позиви
@@ -1319,9 +1552,11 @@
Могуће је да је ваш број телефона регистрован на Signal на другом уређају. Кликнути овде за понављање регистрације.
Да бисте одговорили на позив од %s, „Signal“-у је потребан приступ Вашем микрофону.
+ Signal захтева приступ микрофону и камери да би успоставио или примио позив, али су му дозволе трајно забрањене. Молимо вас да у апликацији за подешавање телефона Signal-у дозволите пруиступ микрофону и камери.
Одговорено на другом уређају.
Одбијено на другом уређају.
У току на другом уређају.
+ Неко се придружио овом позиву са сигурноснем бројем који се променио.
Превуците ка горе за промену изгледа
@@ -1353,6 +1588,7 @@
Фотографија контакта
+ Signal захтева приступ вашим контактима да би их показао, али му је дозвола трајно забрањена. Молимо вас да у апликацији за подешавање телефона Signal-у дозволите пруиступ контактима.
Грешка при преузимања контакта, проверите вашу везу са мрежом.
Корисник није пронађен
„%1$s“ није корисник Signal-а. Проверите корисничко име и поновите.
@@ -1360,6 +1596,7 @@
Достигнута максимална величина групе
Групе „Signal“-а могу имати највише %1$d чланова.
Достигнуто је препоручено ограничење чланова
+ Групе имају најбољи учинак са %1$d чланова или мање. Додавање више чланова проузроковаће кашњења у слању и примању порука.
- %1$d члан
- %1$d чланова
@@ -1387,7 +1624,9 @@
Сличица прилога
Фиока брзог прилога камере
Сними и пошаљи звук
+ Закључај снимање аудио прилога
Укључите Signal за СМС
+ Порука не може да се пошаље. Проверите да ли сте повезани на интернет и покушајте поново.
Превуцте да бисте отказали
Одустани
@@ -1414,7 +1653,7 @@
Звук
Видео
- Слика
+ Фотографија
Једнократни медиј
Налепница
Ви
@@ -1495,6 +1734,7 @@
Нема резултата
+ Овај извештај ће бити објављен и видљив пројектантима. Можете га прегледати и изменити пре објаве.
Желите ли да увезете постојеће текстуалне поруке у Signalову шифровану базу података?
Подразумевана системска база података неће бити измењена или преуређена на било који начин.
@@ -1537,6 +1777,7 @@
Шифровано
Будите љубазни
Љубитељ кафе
+ Доступан за ћаскање
Правим паузу
Радим на нешто ново
@@ -1592,6 +1833,7 @@
Уклони слику
Захтеви за разговор
+ Од сада корисници могу да прихвате нове разговоре. Имена профила олакшавају људима да знају ко их је контактирао.
Додајте име на профил
Јесте ли већ прочитали FAQ?
@@ -1602,12 +1844,14 @@
Шта је ово?
Како се осећате? (Опционо)
Инфо о подршци
+ Signal Андроид захтев за подржку
Извештај о грешкама:
Грешка при отпремању извештаја
+ Будите што описнији како бисте нам помогли да разумемо проблем.
Ова порука
Недавно коришћено
- Емотикони & људи
+ Емотикони & Особе
Природа
Јело
Активности
@@ -1658,6 +1902,7 @@
Користите фотографије из адресара
Користите фотографије контакта из Вашег адресара ако је доступна
Креирати приказ линка
+ Видите преглед линка са сваког веб сајта у порукама које шаљете.
Изаберите идентитет
Изаберите унос вашег контакта са списка контаката.
Измени лозинку
@@ -1720,6 +1965,8 @@
Позадина за ћаскање
Искључи PIN
Укључи PIN
+ Ако онемогућите PIN, изгубићете све податке када поново региструјете Signal, осим ако ручно направите резервне копије и вратите податке. Не можете укључити закључавање регистрације док је PIN онемогућен.
+ Употребом PIN-а се подаци које Signal складишти енкриптују, тако да им само ви можете приступити. Ваш налог, подешавања и контакти ће бити враћени када реинсталирате Signal. За отварање апликације неће вам требати PIN.
Подразумевана поставка
Језик
Поруке и позиви преко Signal-a
@@ -1734,6 +1981,7 @@
Уколико су извештаји читања онемогућени, нећете видети када неко прочита ваше поруке.
Показатељи куцања
Уколико су показатељи куцања онемогућени, нећете видети када вам неко пише поруку.
+ Затражите да тастатура онемогући персонализовано учење. Ово подешавање није гаранција и ваша тастатура га може занемарити.
Блокирани корисници
На мобилном интернету
На бежичној
@@ -1748,6 +1996,11 @@
Прегледајте складиште
Брисати старе поруке?
Очистити историју поруке?
+ Овим ћете трајно избрисати сву историју порука и медије са уређаја старије од %1$s.
+ Ово ће трајно да скрати све преписке на %1$s најскоријих порука.
+ Овим ћете трајно избрисати сву историју порука и медије са уређаја.
+ Да ли заиста желите да избришете сву историју порука?
+ Сва историја порука биће трајно уклоњена. Ова радња се не може опозвати.
Избриши све сада
Заувек
1 година
@@ -1896,13 +2149,18 @@
Signal протокол је аутоматски заштитио %1$d%% ваших одлазних порука у последњих %2$d дана. Разговори између корисника Signal-а су увек потпуно енкриптовани.
Осигурај више разговора
Недовољно података
+ Ваш проценат увида израчунава се на основу одлазних порука које нису нестале или избрисане у протеклих %1$d дана.
Започните разговор
+ Почните безбедно да комуницирате и омогућите нове функције које превазилазе ограничења нешифрованих СМС порука тако што ћете позвати још контаката да се придруже Signal-у.
+ Ове статистике су генерисане локално на вашем уређају и можете их видети само ви. Никада се не преносе нигде.
Шифроване поруке
Одустани
Пошаљи
Представљамо увид
+ Откријте колико је одлазних порука сигурно послато, а затим брзо позовите нове контакте да повећате проценат Signal-а.
Преглед увида
Позивница за Signal
+ Можете да повећате број шифрованих порука које шаљете за %1$d%%
Осигурај више чета
Пошаљите позивницу %1$s
Преглед увида
@@ -1926,6 +2184,7 @@
Направи нови PIN
Ваш PIN можете променити док год је овај уређај регистрован.
Направите ваш PIN
+ Употребом PIN-а се подаци које Signal складишти енкриптују, тако да им само ви можете приступити. Ваш налог, подешавања и контакти ће бити враћени када реинсталирате Signal. За отварање апликације неће вам требати PIN.
Унесите компликованији PIN
PIN-ови се не подударају. Покушајте поново.
@@ -1937,6 +2196,7 @@
Постављање PIN\'а…
Представљамо PIN-ове
+ Употребом PIN-а се подаци које Signal складишти енкриптују, тако да им само ви можете приступити. Ваш налог, подешавања и контакти ће бити враћени када реинсталирате Signal. За отварање апликације неће вам требати PIN.
Сазнај више
Закључавање регистрације = PIN
Закључавање регистрације се од сада зове PIN и има више примена. Ажурирајте га сада.
@@ -1946,12 +2206,14 @@
Искључи PIN
Унесите ваш Signal PIN
+ Да бисмо вам помогли да запамтите ваш PIN, повремено ћемо тражити од вас да га потврдите. Временом ће то бити све ређе.
Прескочи
Пошаљи
Заборавили сте ПИН?
Нетачан PIN. Покушајте поново.
Налог је закључан
+ Ваш рачун је закључан ради заштите ваше приватности и сигурности. Након %1$d дана неактивности, биће вам омогућена поновна регистрација овог броја телефона без потребе да унесете стари PIN. Сви подаци ће бити избрисани.
Даље
Сазнај више
@@ -1990,20 +2252,27 @@
- %1$d преостала покушаја.
+ %1$s ће добити ваш захтев за разговор. Моћи ћете да позовете након што је ваш захтев одобрен.
Направите PIN
+ Употребом PIN-а се подаци које Signal складишти енкриптују
Направите PIN
Реците Signal-у шта мислите
Да бисмо Signal учинили најбољом апликацијом за размену порука на планети, волели бисмо да чујемо ваше повратне информације.
Сазнај више
Одбаци
+ Signal Истраживање
+ Верујемо у приватност.Signal вас не прати нити прикупља ваше податке. Да бисмо побољшали Signal за све, ослањамо се на повратне информације корисника, и ми бисмо волели Ваше.
Водимо анкету како бисмо разумели како користите Signal. Наша анкета не прикупља податке који ће вас идентификовати. Ако сте заинтересовани за дељење повратних информација, имаћете могућност да наведете контакт информације.
Ако имате неколико минута и повратне информације да поделите, желели бисмо да нам пишете.
]]>
+ Попуните анкету
Не, хвала
+ Домаћин анкете је Alchemer на сигурносном домену surveys.signalusers.org
Икона преноса
Учитавање…
Повезује се…
Дозвола је потребна
+ Signal захтева приступ СМС да би послао СМС, али су му дозволе трајно забрањене. Молимо вас да у апликацији за подешавање телефона Signal-у омогућите СМС.
Настави
Не сада
ОМОГУЋИТЕ ПОРУКЕ ПРЕКО SIGNAL-A
@@ -2011,6 +2280,8 @@
Нова закључана порука
Откључајте за преглед порука на чекању
Лозинка резерве
+ Резервне копије ће бити сачуване у спољној меморији и шифроване доњом лозинком. Морате имати ову лозинку да бисте вратили резервну копију.
+ Морате имати ову лозинку да бисте вратили резервну копију.
Фасцикла
Записао сам ову фразу. Без тога нећу моћи да вратим резервну копију.
Поврати резерву
@@ -2035,7 +2306,8 @@
Обриши резерве
Да бисте омогућили резервне копије, изаберите фасциклу. Резервне копије ће бити сачуване на изабраној локацији.
Одаберите фолдер
- Копирано на клипборд
+ Копирано у остави
+ Није доступан бирач датотека.
Верификујте фразу за резервну копију
Овери
Успешно сте унели фразу за резервну копију
@@ -2058,7 +2330,7 @@
Сви
Моји контакти
никога
- Ваш телефонски број ће бити видљив свим људима и групама којима пошаљете поруку.
+ Ваш телефонски број ће бити видљив свим особама и групама којима пошаљете поруку.
Свако ко у контактима има ваш телефонски број видеће вас као контакт на Signal. Други ће вас моћи пронаћи у претрази.
Закључавање екрана
Ограничите приступ Сигналу помоћу Андроид екрана за закључавање или отиском прста
@@ -2068,9 +2340,12 @@
Промените ваш PIN
PIN подсетници
Употребом PIN-а се подаци које Signal складишти енкриптују, тако да им само ви можете приступити. Ваш налог, подешавања и контакти ће бити повраћени када реинсталирате Signal.
+ Додајте додатну безбедност тако да при поновној регистрацији вашег броја телефона на Signal, биће потребно унети ваш Signal PIN.
+ Подсетници вам помажу да запамтите ваш PIN, јер он ме може бити повраћен. Временом ћемо вас питати све ређе да га проверите.
Искључи
Потврдите ПИН
Потврдите ваш Signal PIN
+ Будите сигурни да сте запамтили или безбедно сачували ваш PIN, јер он не може бити повраћен. Уколико заборавите ваш PIN, могућ је губитак података када поново региструјете ваш Signal налог.
Нетачан PIN. Покушајте поново.
Грешка при омогућавању закључавања регистрације.
Грешка при онемогућавању закључавања регистрације.
@@ -2101,19 +2376,26 @@
Несигуран гласовни позив
Видео позив
Одузми администраторске дозволе групе од %1$s?
+ „%1$s“ моћи ће да уређује ову групу и њене чланове.
Уклонити %1$s из групе?
Уклони
- Копирано на клипборд
+ Копирано у остави
Администратор
Одобри
Одбиј
+ Старе против нових група
+ Шта су старе групе?
+ Старе групе су групе које нису компатибилне са функцијама Нове групе, као што су администратори и описнија ажурирања група.
+ Могу ли могу да надоградим стару групу?
+ Старе групе још увек не могу да се надограде на Нове групе, али можете да направите нову групу са истим члановима ако су на најновијој верзији Signal-а.
+ Signal ће убудуће понудити начин за надоградњу старих група.
Поделите Signal-ом
Копирај
QR кôд
Поделите
- Копирано на клипборд
+ Копирано у остави
Веза није тренутно активна
@@ -2127,6 +2409,8 @@
Прегледајте чланове
Прегледајте захтев
+ %1$d чланова групе имају исто име, прегледајте чланове у наставку и одаберите акцију.
+ Ако нисте сигурни од кога потиче захтев, прегледајте доње контакте и предузмите мере.
Нема других заједничких група
Нема заједничких група.
@@ -2175,6 +2459,8 @@
Да ли заиста желите да избришете ваш налог?
Ово ће избрисати ваш Signal налог и ресетовати апликацију. Апликација ће се затворити након завршетка поступка.
Брисање налога није успело. Да ли имате мрежну везу?
+ Брисање локалних података није успело. Можете их ручно обрисати у поставкама апликације система.
+ Поставке покретања апликације
Претражити државе
@@ -2183,23 +2469,33 @@
Поделите
Пошаљи
%1$s,
+ Дељење у више ћаскања је подржано само за Signal поруке
+ Слање неким корисницима није успело
+ Могуће је делити највише %1$d ћаскања
Проследи поруку
Позадина за ћаскање
Поставити позадину
+ Тамна тема затамњује позадину
+ Уклони позадину
+ Уклонити позадину за овај чет?
+ Уклонити позадину? Ово неће уклонити посебне позадине које сте поставили за ћаскање.
Рисетуј све позадине
+ Ресетујте све позадине, укључујући посебне позадине које сте поставили за ћаскање?
Име контакта
Ресетуј
Очистити
Преглед позадине
Одабери из фотографије
+ Предефинисано
Преглед
Поставити позадину
+ Превуците да бисте прегледали још позадина
Поставити позадину за све четове
Постави позадину за %1$s
За приказивање Ваше галерије потребна је дозвола за меморију.
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index 6cc35d0dd7a..31781485b69 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -644,7 +644,7 @@
Misslyckades med att ställa in avatar
Lägg till i systemkontakter
- Denna person är i dina kontakter
+ Denna person finns i dina kontakter
Försvinnande meddelanden
Chattfärg
Chattbakgrund
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index ee5dc272d16..d2ba291fbd4 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -1699,9 +1699,10 @@ Geçersiz protokol sürümünde anahtar değişim iletisi alındı.
Hakkında
Hakkınızda bir kaç kelime yazın…
+ %1$d/%2$d
Özgürce konuşun
Şifreli
- Türk; öğün, çalış, güven
+ Nazik ol
Kahve sevdalısı
Konuşmaya açık
Dinleniyor
From 213ffdab6292520bb06e89ebaaf0481b8a98c71f Mon Sep 17 00:00:00 2001
From: Greyson Parrelli
Date: Tue, 2 Feb 2021 20:13:58 -0500
Subject: [PATCH 17/17] Bump version to 5.3.11
---
app/build.gradle | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/build.gradle b/app/build.gradle
index aa136cbe4cd..0727ff12a6e 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -61,8 +61,8 @@ protobuf {
}
}
-def canonicalVersionCode = 783
-def canonicalVersionName = "5.3.10"
+def canonicalVersionCode = 784
+def canonicalVersionName = "5.3.11"
def postFixSize = 100
def abiPostFix = ['universal' : 0,