From 226eb739bdd5d1b27feaaf52cb2a206236f1e74e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 31 Oct 2021 08:35:39 +0100 Subject: [PATCH 001/394] make custom 'xmpp' protocol in address book case insensitve fixes #4215 --- .../java/eu/siacs/conversations/android/JabberIdContact.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/android/JabberIdContact.java b/src/main/java/eu/siacs/conversations/android/JabberIdContact.java index 0fd46148b..0b701d27a 100644 --- a/src/main/java/eu/siacs/conversations/android/JabberIdContact.java +++ b/src/main/java/eu/siacs/conversations/android/JabberIdContact.java @@ -23,13 +23,13 @@ public class JabberIdContact extends AbstractPhoneContact { ContactsContract.Data.LOOKUP_KEY, ContactsContract.CommonDataKinds.Im.DATA }; - private static final String SELECTION = ContactsContract.Data.MIMETYPE + "=? AND (" + ContactsContract.CommonDataKinds.Im.PROTOCOL + "=? or (" + ContactsContract.CommonDataKinds.Im.PROTOCOL + "=? and " + ContactsContract.CommonDataKinds.Im.CUSTOM_PROTOCOL + "=?))"; + private static final String SELECTION = ContactsContract.Data.MIMETYPE + "=? AND (" + ContactsContract.CommonDataKinds.Im.PROTOCOL + "=? or (" + ContactsContract.CommonDataKinds.Im.PROTOCOL + "=? and lower(" + ContactsContract.CommonDataKinds.Im.CUSTOM_PROTOCOL + ")=?))"; private static final String[] SELECTION_ARGS = { ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE, String.valueOf(ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER), String.valueOf(ContactsContract.CommonDataKinds.Im.PROTOCOL_CUSTOM), - "XMPP" + "xmpp" }; private final Jid jid; From ba4a47204b1d9e76a5d48656ac2e4b0f9f8f6a42 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 31 Oct 2021 10:20:34 +0100 Subject: [PATCH 002/394] fixed IndexOutOfBounds when rendering quotes --- .../ui/adapter/MessageAdapter.java | 16 ++++----- .../conversations/ui/util/QuoteHelper.java | 34 +++++++++---------- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index 0eb8a10fb..ccb40418a 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -370,9 +370,7 @@ private boolean handleTextQuotes(SpannableStringBuilder body, boolean darkBackgr char current = body.length() > i ? body.charAt(i) : '\n'; if (lineStart == -1) { if (previous == '\n') { - if ( - QuoteHelper.isPositionQuoteStart(body, i) - ) { + if (i < body.length() && QuoteHelper.isPositionQuoteStart(body, i)) { // Line start with quote lineStart = i; if (quoteStart == -1) quoteStart = i; @@ -806,12 +804,12 @@ public View getView(int position, View view, ViewGroup parent) { } else if (message.treatAsDownloadable()) { try { final URI uri = new URI(message.getBody()); - displayDownloadableMessage(viewHolder, - message, - activity.getString(R.string.check_x_filesize_on_host, - UIHelper.getFileDescriptionString(activity, message), - uri.getHost()), - darkBackground); + displayDownloadableMessage(viewHolder, + message, + activity.getString(R.string.check_x_filesize_on_host, + UIHelper.getFileDescriptionString(activity, message), + uri.getHost()), + darkBackground); } catch (Exception e) { displayDownloadableMessage(viewHolder, message, diff --git a/src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java b/src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java index ac2913037..4beee8a22 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java +++ b/src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java @@ -11,47 +11,47 @@ public class QuoteHelper { public static final char QUOTE_ALT_CHAR = '»'; public static final char QUOTE_ALT_END_CHAR = '«'; - public static boolean isPositionQuoteCharacter(CharSequence body, int pos){ + public static boolean isPositionQuoteCharacter(CharSequence body, int pos) { // second part of logical check actually goes against the logic indicated in the method name, since it also checks for context // but it's very useful return body.charAt(pos) == QUOTE_CHAR || isPositionAltQuoteStart(body, pos); } - public static boolean isPositionQuoteEndCharacter(CharSequence body, int pos){ + public static boolean isPositionQuoteEndCharacter(CharSequence body, int pos) { return body.charAt(pos) == QUOTE_END_CHAR; } - public static boolean isPositionAltQuoteCharacter (CharSequence body, int pos){ + public static boolean isPositionAltQuoteCharacter(CharSequence body, int pos) { return body.charAt(pos) == QUOTE_ALT_CHAR; } - public static boolean isPositionAltQuoteEndCharacter(CharSequence body, int pos){ + public static boolean isPositionAltQuoteEndCharacter(CharSequence body, int pos) { return body.charAt(pos) == QUOTE_ALT_END_CHAR; } - public static boolean isPositionAltQuoteStart(CharSequence body, int pos){ + public static boolean isPositionAltQuoteStart(CharSequence body, int pos) { return isPositionAltQuoteCharacter(body, pos) && !isPositionFollowedByAltQuoteEnd(body, pos); } public static boolean isPositionFollowedByQuoteChar(CharSequence body, int pos) { - return body.length() > pos + 1 && isPositionQuoteCharacter(body, pos +1 ); + return body.length() > pos + 1 && isPositionQuoteCharacter(body, pos + 1); } // 'Prequote' means anything we require or can accept in front of a QuoteChar - public static boolean isPositionPrecededByPrequote(CharSequence body, int pos){ + public static boolean isPositionPrecededByPreQuote(CharSequence body, int pos) { return UIHelper.isPositionPrecededByLineStart(body, pos); } - public static boolean isPositionQuoteStart (CharSequence body, int pos){ + public static boolean isPositionQuoteStart(CharSequence body, int pos) { return (isPositionQuoteCharacter(body, pos) - && isPositionPrecededByPrequote(body, pos) + && isPositionPrecededByPreQuote(body, pos) && (UIHelper.isPositionFollowedByQuoteableCharacter(body, pos) - || isPositionFollowedByQuoteChar(body, pos))); + || isPositionFollowedByQuoteChar(body, pos))); } - public static boolean bodyContainsQuoteStart (CharSequence body){ - for (int i = 0; i < body.length(); i++){ - if (isPositionQuoteStart(body, i)){ + public static boolean bodyContainsQuoteStart(CharSequence body) { + for (int i = 0; i < body.length(); i++) { + if (isPositionQuoteStart(body, i)) { return true; } } @@ -76,7 +76,7 @@ public static boolean isPositionFollowedByAltQuoteEnd(CharSequence body, int pos return false; } - public static boolean isNestedTooDeeply (CharSequence line){ + public static boolean isNestedTooDeeply(CharSequence line) { if (isPositionQuoteStart(line, 0)) { int nestingDepth = 1; for (int i = 1; i < line.length(); i++) { @@ -91,9 +91,9 @@ public static boolean isNestedTooDeeply (CharSequence line){ return false; } - public static String replaceAltQuoteCharsInText(String text){ - for (int i = 0; i < text.length(); i++){ - if (isPositionAltQuoteStart(text, i)){ + public static String replaceAltQuoteCharsInText(String text) { + for (int i = 0; i < text.length(); i++) { + if (isPositionAltQuoteStart(text, i)) { text = text.substring(0, i) + QUOTE_CHAR + text.substring(i + 1); } } From bae9fc45c22ca959fae3c97b5ca515dbbc9b8591 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 31 Oct 2021 10:20:58 +0100 Subject: [PATCH 003/394] make rtcpMux optional --- .../java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 7b9caa66c..751fa66f4 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -255,6 +255,7 @@ synchronized void initializePeerConnection(final Set media, final List Date: Wed, 3 Nov 2021 15:59:05 +0100 Subject: [PATCH 004/394] version bump to 2.10.2 + changelog --- CHANGELOG.md | 5 +++++ build.gradle | 8 +++++--- fastlane/metadata/android/en-US/changelogs/42023.txt | 2 ++ 3 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/42023.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index b9227af42..c2bb0a938 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +### Version 2.10.2 + +* Fix crash when rendering some quotes +* Fix crash in welcome screen + ### Version 2.10.1 * Fix issue with some videos not being compressed diff --git a/build.gradle b/build.gradle index 442b2a500..8edf10065 100644 --- a/build.gradle +++ b/build.gradle @@ -92,8 +92,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 29 - versionCode 42022 - versionName "2.10.1" + versionCode 42023 + versionName "2.10.2" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId @@ -277,7 +277,9 @@ android { variant.outputs.each { output -> def baseAbiVersionCode = project.ext.abiCodes.get(output.getFilter(com.android.build.OutputFile.ABI)) if (baseAbiVersionCode != null) { - output.versionCodeOverride = (100 * variant.versionCode) + baseAbiVersionCode + output.versionCodeOverride = (100 * project.android.defaultConfig.versionCode) + baseAbiVersionCode + } else { + output.versionCodeOverride = 100 * project.android.defaultConfig.versionCode } } diff --git a/fastlane/metadata/android/en-US/changelogs/42023.txt b/fastlane/metadata/android/en-US/changelogs/42023.txt new file mode 100644 index 000000000..ed3c25380 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/42023.txt @@ -0,0 +1,2 @@ +* Fix crash when rendering some quotes +* Fix crash in welcome screen From 7d7e158fd75dd447e16ec8c5e9b96e5594a82fee Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 3 Nov 2021 16:00:26 +0100 Subject: [PATCH 005/394] code clean up for LocationProvider --- .../conversations/utils/LocationProvider.java | 42 +++++++++++-------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/utils/LocationProvider.java b/src/main/java/eu/siacs/conversations/utils/LocationProvider.java index afb39a008..3eb786e39 100644 --- a/src/main/java/eu/siacs/conversations/utils/LocationProvider.java +++ b/src/main/java/eu/siacs/conversations/utils/LocationProvider.java @@ -4,6 +4,8 @@ import android.telephony.TelephonyManager; import android.util.Log; +import androidx.core.content.ContextCompat; + import org.osmdroid.util.GeoPoint; import java.io.BufferedReader; @@ -16,11 +18,14 @@ public class LocationProvider { - public static final GeoPoint FALLBACK = new GeoPoint(0.0,0.0); + public static final GeoPoint FALLBACK = new GeoPoint(0.0, 0.0); - public static String getUserCountry(Context context) { + public static String getUserCountry(final Context context) { try { - final TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + final TelephonyManager tm = ContextCompat.getSystemService(context, TelephonyManager.class); + if (tm == null) { + return getUserCountryFallback(); + } final String simCountry = tm.getSimCountryIso(); if (simCountry != null && simCountry.length() == 2) { // SIM country code is available return simCountry.toUpperCase(Locale.US); @@ -30,40 +35,41 @@ public static String getUserCountry(Context context) { return networkCountry.toUpperCase(Locale.US); } } - } catch (Exception e) { - // fallthrough + return getUserCountryFallback(); + } catch (final Exception e) { + return getUserCountryFallback(); } - Locale locale = Locale.getDefault(); + } + + private static String getUserCountryFallback() { + final Locale locale = Locale.getDefault(); return locale.getCountry(); } - public static GeoPoint getGeoPoint(Context context) { + public static GeoPoint getGeoPoint(final Context context) { return getGeoPoint(context, getUserCountry(context)); } - public static synchronized GeoPoint getGeoPoint(Context context, String country) { - try { - BufferedReader reader = new BufferedReader(new InputStreamReader(context.getResources().openRawResource(R.raw.countries))); + public static synchronized GeoPoint getGeoPoint(final Context context, final String country) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(context.getResources().openRawResource(R.raw.countries)))) { String line; - while((line = reader.readLine()) != null) { - String[] parts = line.split("\\s+",4); + while ((line = reader.readLine()) != null) { + final String[] parts = line.split("\\s+", 4); if (parts.length == 4) { if (country.equalsIgnoreCase(parts[0])) { try { return new GeoPoint(Double.parseDouble(parts[1]), Double.parseDouble(parts[2])); - } catch (NumberFormatException e) { + } catch (final NumberFormatException e) { return FALLBACK; } } - } else { - Log.d(Config.LOGTAG,"unable to parse line="+line); } } - } catch (IOException e) { - Log.d(Config.LOGTAG,e.getMessage()); + } catch (final IOException e) { + Log.d(Config.LOGTAG, "unable to parse country->geo map", e); } return FALLBACK; } -} +} \ No newline at end of file From d4cbf2e11e6a2883c4583a0ab598be0433f7fcad Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 7 Nov 2021 11:35:00 +0100 Subject: [PATCH 006/394] take intent type into account when sharing with conversations --- .../conversations/services/XmppConnectionService.java | 4 ++-- .../eu/siacs/conversations/ui/ConversationFragment.java | 9 +++++---- .../eu/siacs/conversations/ui/ConversationsActivity.java | 1 + .../eu/siacs/conversations/ui/ShareWithActivity.java | 8 +++++++- .../java/eu/siacs/conversations/ui/util/Attachment.java | 8 ++++---- 5 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 4222b9faa..815182680 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -572,8 +572,8 @@ public void attachFileToConversation(final Conversation conversation, final Uri } } - public void attachImageToConversation(final Conversation conversation, final Uri uri, final UiCallback callback) { - final String mimeType = MimeUtils.guessMimeTypeFromUri(this, uri); + public void attachImageToConversation(final Conversation conversation, final Uri uri, final String type, final UiCallback callback) { + final String mimeType = MimeUtils.guessMimeTypeFromUriAndMime(this, uri, type); final String compressPictures = getCompressPicturesPreference(); if ("never".equals(compressPictures) diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index a019f282a..0077684a5 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -688,14 +688,14 @@ public void attachEditorContentToConversation(Uri uri) { toggleInputMethod(); } - private void attachImageToConversation(Conversation conversation, Uri uri) { + private void attachImageToConversation(Conversation conversation, Uri uri, String type) { if (conversation == null) { return; } final Toast prepareFileToast = Toast.makeText(getActivity(), getText(R.string.preparing_image), Toast.LENGTH_LONG); prepareFileToast.show(); activity.delegateUriPermissionsToService(uri); - activity.xmppConnectionService.attachImageToConversation(conversation, uri, + activity.xmppConnectionService.attachImageToConversation(conversation, uri, type, new UiCallback() { @Override @@ -889,7 +889,7 @@ private void commitAttachments() { attachLocationToConversation(conversation, attachment.getUri()); } else if (attachment.getType() == Attachment.Type.IMAGE) { Log.d(Config.LOGTAG, "ConversationsActivity.commitAttachments() - attaching image to conversations. CHOOSE_IMAGE"); - attachImageToConversation(conversation, attachment.getUri()); + attachImageToConversation(conversation, attachment.getUri(), attachment.getMime()); } else { Log.d(Config.LOGTAG, "ConversationsActivity.commitAttachments() - attaching file to conversations. CHOOSE_FILE/RECORD_VOICE/RECORD_VIDEO"); attachFileToConversation(conversation, attachment.getUri(), attachment.getMime()); @@ -2186,13 +2186,14 @@ private void processExtras(final Bundle extras) { final boolean asQuote = extras.getBoolean(ConversationsActivity.EXTRA_AS_QUOTE); final boolean pm = extras.getBoolean(ConversationsActivity.EXTRA_IS_PRIVATE_MESSAGE, false); final boolean doNotAppend = extras.getBoolean(ConversationsActivity.EXTRA_DO_NOT_APPEND, false); + final String type = extras.getString(ConversationsActivity.EXTRA_TYPE); final List uris = extractUris(extras); if (uris != null && uris.size() > 0) { if (uris.size() == 1 && "geo".equals(uris.get(0).getScheme())) { mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), uris.get(0), Attachment.Type.LOCATION)); } else { final List cleanedUris = cleanUris(new ArrayList<>(uris)); - mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), cleanedUris)); + mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), cleanedUris, type)); } toggleInputMethod(); return; diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java index fbdba5724..cc46ed33f 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java @@ -99,6 +99,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio public static final String EXTRA_DO_NOT_APPEND = "do_not_append"; public static final String EXTRA_POST_INIT_ACTION = "post_init_action"; public static final String POST_ACTION_RECORD_VOICE = "record_voice"; + public static final String EXTRA_TYPE = "type"; private static final List VIEW_AND_SHARE_ACTIONS = Arrays.asList( ACTION_VIEW_CONVERSATION, diff --git a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java index cb698691e..d03928c8c 100644 --- a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java @@ -33,7 +33,8 @@ public void onConversationUpdate() { refreshUi(); } - private class Share { + private static class Share { + public String type; ArrayList uris = new ArrayList<>(); public String account; public String contact; @@ -65,6 +66,7 @@ protected void onActivityResult(int requestCode, int resultCode, final Intent da @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (grantResults.length > 0) if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { if (requestCode == REQUEST_STORAGE_PERMISSION) { @@ -139,6 +141,7 @@ public void onStart() { } else if (type != null && uri != null) { this.share.uris.clear(); this.share.uris.add(uri); + this.share.type = type; } else { this.share.text = text; this.share.asQuote = asQuote; @@ -193,6 +196,9 @@ private void share(final Conversation conversation) { intent.setAction(Intent.ACTION_SEND_MULTIPLE); intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, share.uris); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + if (share.type != null) { + intent.putExtra(ConversationsActivity.EXTRA_TYPE, share.type); + } } else if (share.text != null) { intent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION); intent.putExtra(Intent.EXTRA_TEXT, share.text); diff --git a/src/main/java/eu/siacs/conversations/ui/util/Attachment.java b/src/main/java/eu/siacs/conversations/ui/util/Attachment.java index 4083d5b04..b539c70ef 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/Attachment.java +++ b/src/main/java/eu/siacs/conversations/ui/util/Attachment.java @@ -136,10 +136,10 @@ public static List of(final Context context, Uri uri, Type type) { return Collections.singletonList(new Attachment(uri, type, mime)); } - public static List of(final Context context, List uris) { - List attachments = new ArrayList<>(); - for (Uri uri : uris) { - final String mime = MimeUtils.guessMimeTypeFromUri(context, uri); + public static List of(final Context context, List uris, final String type) { + final List attachments = new ArrayList<>(); + for (final Uri uri : uris) { + final String mime = MimeUtils.guessMimeTypeFromUriAndMime(context, uri, type); attachments.add(new Attachment(uri, mime != null && isImage(mime) ? Type.IMAGE : Type.FILE, mime)); } return attachments; From b5786787f011607b2aacc869c2a9d1c83ffc871f Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 9 Nov 2021 14:27:26 +0100 Subject: [PATCH 007/394] bump libphone number library --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 8edf10065..a95b2ff8e 100644 --- a/build.gradle +++ b/build.gradle @@ -76,7 +76,7 @@ dependencies { implementation "com.squareup.okhttp3:okhttp:4.9.2" implementation 'com.google.guava:guava:30.1.1-android' - quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.18' + quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.36' implementation fileTree(include: ['libwebrtc-m92.aar'], dir: 'libs') } From fda45a7c86004dfee6e05dd748819fe195fd147f Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 10 Nov 2021 16:40:16 +0100 Subject: [PATCH 008/394] use implicit descriptions for WebRTC using the parameter-free form of setLocalDescription() solves some potential race conditions that can occur - especially once we introduce restartIce() --- .../xmpp/jingle/JingleRtpConnection.java | 24 +++---- .../xmpp/jingle/WebRTCWrapper.java | 69 ++++++------------- 2 files changed, 28 insertions(+), 65 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 8c4d14843..72d2a4837 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -537,9 +537,10 @@ private synchronized void sendSessionAccept(final Set media, final Sessio try { this.webRTCWrapper.setRemoteDescription(sdp).get(); addIceCandidatesFromBlackLog(); - org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createAnswer().get(); + org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.setLocalDescription().get(); prepareSessionAccept(webRTCSessionDescription); } catch (final Exception e) { + //TODO sending the error text is worthwhile as well. Especially for FailureToSet exceptions failureToAcceptSession(e); } } @@ -569,7 +570,7 @@ private void prepareSessionAccept(final org.webrtc.SessionDescription webRTCSess new FutureCallback() { @Override public void onSuccess(final RtpContentMap outgoingContentMap) { - sendSessionAccept(outgoingContentMap, webRTCSessionDescription); + sendSessionAccept(outgoingContentMap); } @Override @@ -581,7 +582,7 @@ public void onFailure(@NonNull Throwable throwable) { ); } - private void sendSessionAccept(final RtpContentMap rtpContentMap, final org.webrtc.SessionDescription webRTCSessionDescription) { + private void sendSessionAccept(final RtpContentMap rtpContentMap) { if (isTerminated()) { Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": preparing session accept was too slow. already terminated. nothing to do."); return; @@ -589,11 +590,6 @@ private void sendSessionAccept(final RtpContentMap rtpContentMap, final org.webr transitionOrThrow(State.SESSION_ACCEPTED); final JinglePacket sessionAccept = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId); send(sessionAccept); - try { - webRTCWrapper.setLocalDescription(webRTCSessionDescription).get(); - } catch (Exception e) { - failureToAcceptSession(e); - } } private ListenableFuture prepareOutgoingContentMap(final RtpContentMap rtpContentMap) { @@ -841,9 +837,10 @@ private synchronized void sendSessionInitiate(final Set media, final Stat return; } try { - org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createOffer().get(); + org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.setLocalDescription().get(); prepareSessionInitiate(webRTCSessionDescription, targetState); } catch (final Exception e) { + //TODO sending the error text is worthwhile as well. Especially for FailureToSet exceptions failureToInitiateSession(e, targetState); } } @@ -877,7 +874,7 @@ private void prepareSessionInitiate(final org.webrtc.SessionDescription webRTCSe Futures.addCallback(outgoingContentMapFuture, new FutureCallback() { @Override public void onSuccess(final RtpContentMap outgoingContentMap) { - sendSessionInitiate(outgoingContentMap, webRTCSessionDescription, targetState); + sendSessionInitiate(outgoingContentMap, targetState); } @Override @@ -887,7 +884,7 @@ public void onFailure(@NonNull final Throwable throwable) { }, MoreExecutors.directExecutor()); } - private void sendSessionInitiate(final RtpContentMap rtpContentMap, final org.webrtc.SessionDescription webRTCSessionDescription, final State targetState) { + private void sendSessionInitiate(final RtpContentMap rtpContentMap, final State targetState) { if (isTerminated()) { Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": preparing session was too slow. already terminated. nothing to do."); return; @@ -895,11 +892,6 @@ private void sendSessionInitiate(final RtpContentMap rtpContentMap, final org.we this.transitionOrThrow(targetState); final JinglePacket sessionInitiate = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId); send(sessionInitiate); - try { - this.webRTCWrapper.setLocalDescription(webRTCSessionDescription).get(); - } catch (Exception e) { - failureToInitiateSession(e, targetState); - } } private ListenableFuture encryptSessionInitiate(final RtpContentMap rtpContentMap) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 751fa66f4..368b13913 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -403,70 +403,36 @@ void setVideoEnabled(final boolean enabled) { videoTrack.setEnabled(enabled); } - ListenableFuture createOffer() { + ListenableFuture setLocalDescription() { return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { final SettableFuture future = SettableFuture.create(); - peerConnection.createOffer(new CreateSdpObserver() { - @Override - public void onCreateSuccess(SessionDescription sessionDescription) { - future.set(sessionDescription); - } - - @Override - public void onCreateFailure(String s) { - future.setException(new IllegalStateException("Unable to create offer: " + s)); - } - }, new MediaConstraints()); - return future; - }, MoreExecutors.directExecutor()); - } - - ListenableFuture createAnswer() { - return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { - final SettableFuture future = SettableFuture.create(); - peerConnection.createAnswer(new CreateSdpObserver() { + peerConnection.setLocalDescription(new SetSdpObserver() { @Override - public void onCreateSuccess(SessionDescription sessionDescription) { - future.set(sessionDescription); + public void onSetSuccess() { + final SessionDescription description = peerConnection.getLocalDescription(); + Log.d(EXTENDED_LOGGING_TAG, "set local description:"); + logDescription(description); + future.set(description); } @Override - public void onCreateFailure(String s) { - future.setException(new IllegalStateException("Unable to create answer: " + s)); + public void onSetFailure(final String message) { + future.setException(new FailureToSetDescriptionException(message)); } - }, new MediaConstraints()); + }); return future; }, MoreExecutors.directExecutor()); } - ListenableFuture setLocalDescription(final SessionDescription sessionDescription) { - Log.d(EXTENDED_LOGGING_TAG, "setting local description:"); + private static void logDescription(final SessionDescription sessionDescription) { for (final String line : sessionDescription.description.split(eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) { Log.d(EXTENDED_LOGGING_TAG, line); } - return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { - final SettableFuture future = SettableFuture.create(); - peerConnection.setLocalDescription(new SetSdpObserver() { - @Override - public void onSetSuccess() { - future.set(null); - } - - @Override - public void onSetFailure(final String s) { - future.setException(new IllegalArgumentException("unable to set local session description: " + s)); - - } - }, sessionDescription); - return future; - }, MoreExecutors.directExecutor()); } ListenableFuture setRemoteDescription(final SessionDescription sessionDescription) { Log.d(EXTENDED_LOGGING_TAG, "setting remote description:"); - for (final String line : sessionDescription.description.split(eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) { - Log.d(EXTENDED_LOGGING_TAG, line); - } + logDescription(sessionDescription); return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { final SettableFuture future = SettableFuture.create(); peerConnection.setRemoteDescription(new SetSdpObserver() { @@ -476,9 +442,8 @@ public void onSetSuccess() { } @Override - public void onSetFailure(String s) { - future.setException(new IllegalArgumentException("unable to set remote session description: " + s)); - + public void onSetFailure(final String message) { + future.setException(new FailureToSetDescriptionException(message)); } }, sessionDescription); return future; @@ -619,6 +584,12 @@ private PeerConnectionNotInitialized() { } + private static class FailureToSetDescriptionException extends IllegalArgumentException { + public FailureToSetDescriptionException(String message) { + super(message); + } + } + private static class CapturerChoice { private final CameraVideoCapturer cameraVideoCapturer; private final CameraEnumerationAndroid.CaptureFormat captureFormat; From 4ec0996dffa2381dc7399a7dc8bfa67e2cdf24ed Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 11 Nov 2021 11:19:37 +0100 Subject: [PATCH 009/394] webrtc: include oldState in onConnectionChange --- .../xmpp/jingle/JingleRtpConnection.java | 4 ++-- .../conversations/xmpp/jingle/WebRTCWrapper.java | 12 ++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 72d2a4837..af4e05ba0 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -1330,8 +1330,8 @@ public void onIceCandidate(final IceCandidate iceCandidate) { } @Override - public void onConnectionChange(final PeerConnection.PeerConnectionState newState) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnectionState changed to " + newState); + public void onConnectionChange(final PeerConnection.PeerConnectionState oldState, final PeerConnection.PeerConnectionState newState) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnectionState changed: "+oldState+"->" + newState); if (newState == PeerConnection.PeerConnectionState.CONNECTED && this.rtpConnectionStarted == 0) { this.rtpConnectionStarted = SystemClock.elapsedRealtime(); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 368b13913..f5b2c0b87 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -88,6 +88,7 @@ public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDev private final Handler mainHandler = new Handler(Looper.getMainLooper()); private VideoTrack localVideoTrack = null; private VideoTrack remoteVideoTrack = null; + private PeerConnection.PeerConnectionState currentState; private final PeerConnection.Observer peerConnectionObserver = new PeerConnection.Observer() { @Override public void onSignalingChange(PeerConnection.SignalingState signalingState) { @@ -98,8 +99,9 @@ public void onSignalingChange(PeerConnection.SignalingState signalingState) { } @Override - public void onConnectionChange(PeerConnection.PeerConnectionState newState) { - eventCallback.onConnectionChange(newState); + public void onConnectionChange(final PeerConnection.PeerConnectionState newState) { + eventCallback.onConnectionChange(currentState, newState); + currentState = newState; } @Override @@ -150,7 +152,7 @@ public void onDataChannel(DataChannel dataChannel) { @Override public void onRenegotiationNeeded() { - + Log.d(EXTENDED_LOGGING_TAG,"onRenegotiationNeeded - current state: "+currentState); } @Override @@ -261,6 +263,8 @@ synchronized void initializePeerConnection(final Set media, final List optionalCapturerChoice = media.contains(Media.VIDEO) ? getVideoCapturer() : Optional.absent(); if (optionalCapturerChoice.isPresent()) { @@ -531,7 +535,7 @@ AppRTCAudioManager getAudioManager() { public interface EventCallback { void onIceCandidate(IceCandidate iceCandidate); - void onConnectionChange(PeerConnection.PeerConnectionState newState); + void onConnectionChange(PeerConnection.PeerConnectionState oldState, PeerConnection.PeerConnectionState newState); void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices); } From 61851e5f843307f2f90de892fd4fefbff5b19d46 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 11 Nov 2021 14:40:15 +0100 Subject: [PATCH 010/394] do not automacially hang up failed webrtc sessions --- .../conversations/ui/RtpSessionActivity.java | 16 +++-- .../xmpp/jingle/JingleRtpConnection.java | 66 +++++++++++-------- .../xmpp/jingle/RtpEndUserState.java | 1 + .../xmpp/jingle/WebRTCWrapper.java | 16 ++++- src/main/res/values/strings.xml | 1 + 5 files changed, 64 insertions(+), 36 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 96aa00db0..b2cf583b5 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -96,7 +96,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe ); private static final List STATES_SHOWING_SWITCH_TO_CHAT = Arrays.asList( RtpEndUserState.CONNECTING, - RtpEndUserState.CONNECTED + RtpEndUserState.CONNECTED, + RtpEndUserState.RECONNECTING + ); + private static final List STATES_CONSIDERED_CONNECTED = Arrays.asList( + RtpEndUserState.CONNECTED, + RtpEndUserState.RECONNECTING ); private static final String PROXIMITY_WAKE_LOCK_TAG = "conversations:in-rtp-session"; private static final int REQUEST_ACCEPT_CALL = 0x1111; @@ -502,7 +507,7 @@ public void onUserLeaveHint() { private boolean isConnected() { final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; - return connection != null && connection.getEndUserState() == RtpEndUserState.CONNECTED; + return connection != null && STATES_CONSIDERED_CONNECTED.contains(connection.getEndUserState()); } private boolean switchToPictureInPicture() { @@ -661,6 +666,9 @@ private void updateStateDisplay(final RtpEndUserState state, final Set me case CONNECTED: setTitle(R.string.rtp_state_connected); break; + case RECONNECTING: + setTitle(R.string.rtp_state_reconnecting); + break; case ACCEPTING_CALL: setTitle(R.string.rtp_state_accepting_call); break; @@ -803,7 +811,7 @@ private void updateInCallButtonConfiguration() { @SuppressLint("RestrictedApi") private void updateInCallButtonConfiguration(final RtpEndUserState state, final Set media) { - if (state == RtpEndUserState.CONNECTED && !isPictureInPicture()) { + if (STATES_CONSIDERED_CONNECTED.contains(state) && !isPictureInPicture()) { Preconditions.checkArgument(media.size() > 0, "Media must not be empty"); if (media.contains(Media.VIDEO)) { final JingleRtpConnection rtpConnection = requireRtpConnection(); @@ -998,7 +1006,7 @@ private void updateVideoViews(final RtpEndUserState state) { RendererCommon.ScalingType.SCALE_ASPECT_FILL, RendererCommon.ScalingType.SCALE_ASPECT_FIT ); - if (state == RtpEndUserState.CONNECTED) { + if (STATES_CONSIDERED_CONNECTED.contains(state)) { binding.appBarLayout.setVisibility(View.GONE); getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); binding.remoteVideoWrapper.setVisibility(View.VISIBLE); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index af4e05ba0..2d322ead9 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -1035,24 +1035,7 @@ public RtpEndUserState getEndUserState() { return RtpEndUserState.CONNECTING; } case SESSION_ACCEPTED: - //TODO refactor this out into separate method (that uses switch for better readability) - final PeerConnection.PeerConnectionState state; - try { - state = webRTCWrapper.getState(); - } catch (final WebRTCWrapper.PeerConnectionNotInitialized e) { - //We usually close the WebRTCWrapper *before* transitioning so we might still - //be in SESSION_ACCEPTED even though the peerConnection has been torn down - return RtpEndUserState.ENDING_CALL; - } - if (state == PeerConnection.PeerConnectionState.CONNECTED) { - return RtpEndUserState.CONNECTED; - } else if (state == PeerConnection.PeerConnectionState.NEW || state == PeerConnection.PeerConnectionState.CONNECTING) { - return RtpEndUserState.CONNECTING; - } else if (state == PeerConnection.PeerConnectionState.CLOSED) { - return RtpEndUserState.ENDING_CALL; - } else { - return rtpConnectionStarted == 0 ? RtpEndUserState.CONNECTIVITY_ERROR : RtpEndUserState.CONNECTIVITY_LOST_ERROR; - } + return getPeerConnectionStateAsEndUserState(); case REJECTED: case REJECTED_RACED: case TERMINATED_DECLINED_OR_BUSY: @@ -1082,6 +1065,29 @@ public RtpEndUserState getEndUserState() { throw new IllegalStateException(String.format("%s has no equivalent EndUserState", this.state)); } + + private RtpEndUserState getPeerConnectionStateAsEndUserState() { + final PeerConnection.PeerConnectionState state; + try { + state = webRTCWrapper.getState(); + } catch (final WebRTCWrapper.PeerConnectionNotInitialized e) { + //We usually close the WebRTCWrapper *before* transitioning so we might still + //be in SESSION_ACCEPTED even though the peerConnection has been torn down + return RtpEndUserState.ENDING_CALL; + } + switch (state) { + case CONNECTED: + return RtpEndUserState.CONNECTED; + case NEW: + case CONNECTING: + return RtpEndUserState.CONNECTING; + case CLOSED: + return RtpEndUserState.ENDING_CALL; + default: + return rtpConnectionStarted == 0 ? RtpEndUserState.CONNECTIVITY_ERROR : RtpEndUserState.RECONNECTING; + } + } + public Set getMedia() { final State current = getState(); if (current == State.NULL) { @@ -1331,29 +1337,31 @@ public void onIceCandidate(final IceCandidate iceCandidate) { @Override public void onConnectionChange(final PeerConnection.PeerConnectionState oldState, final PeerConnection.PeerConnectionState newState) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnectionState changed: "+oldState+"->" + newState); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnectionState changed: " + oldState + "->" + newState); + final boolean neverConnected = this.rtpConnectionStarted == 0; if (newState == PeerConnection.PeerConnectionState.CONNECTED && this.rtpConnectionStarted == 0) { this.rtpConnectionStarted = SystemClock.elapsedRealtime(); } if (newState == PeerConnection.PeerConnectionState.CLOSED && this.rtpConnectionEnded == 0) { this.rtpConnectionEnded = SystemClock.elapsedRealtime(); } - //TODO 'failed' means we need to restart ICE - // - //TODO 'disconnected' can probably be ignored as "This is a less stringent test than failed - // and may trigger intermittently and resolve just as spontaneously on less reliable networks, - // or during temporary disconnections. When the problem resolves, the connection may return - // to the connected state." - // Obviously the UI needs to reflect this new state with a 'reconnecting' display or something - if (Arrays.asList(PeerConnection.PeerConnectionState.FAILED, PeerConnection.PeerConnectionState.DISCONNECTED).contains(newState)) { + + if (neverConnected && Arrays.asList(PeerConnection.PeerConnectionState.FAILED, PeerConnection.PeerConnectionState.DISCONNECTED).contains(newState)) { if (isTerminated()) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": not sending session-terminate after connectivity error because session is already in state " + this.state); return; } new Thread(this::closeWebRTCSessionAfterFailedConnection).start(); - } else { - updateEndUserState(); + } else if (newState == PeerConnection.PeerConnectionState.FAILED) { + Log.d(Config.LOGTAG, "attempting to restart ICE"); + webRTCWrapper.restartIce(); } + updateEndUserState(); + } + + @Override + public void onRenegotiationNeeded() { + Log.d(Config.LOGTAG, "onRenegotiationNeeded()"); } private void closeWebRTCSessionAfterFailedConnection() { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java index 61536bb7c..9a431bc01 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java @@ -4,6 +4,7 @@ public enum RtpEndUserState { INCOMING_CALL, //received a 'propose' message CONNECTING, //session-initiate or session-accepted but no webrtc peer connection yet CONNECTED, //session-accepted and webrtc peer connection is connected + RECONNECTING, //session-accepted and webrtc peer connection was connected once but is currently disconnected or failed FINDING_DEVICE, //'propose' has been sent out; no 184 ack yet RINGING, //'propose' has been sent out and it has been 184 acked ACCEPTING_CALL, //'proceed' message has been sent; but no session-initiate has been received diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index f5b2c0b87..6e4a94539 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -100,13 +100,14 @@ public void onSignalingChange(PeerConnection.SignalingState signalingState) { @Override public void onConnectionChange(final PeerConnection.PeerConnectionState newState) { - eventCallback.onConnectionChange(currentState, newState); + final PeerConnection.PeerConnectionState oldState = currentState; currentState = newState; + eventCallback.onConnectionChange(oldState, newState); } @Override public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) { - + Log.d(EXTENDED_LOGGING_TAG, "onIceConnectionChange(" + iceConnectionState + ")"); } @Override @@ -152,7 +153,10 @@ public void onDataChannel(DataChannel dataChannel) { @Override public void onRenegotiationNeeded() { - Log.d(EXTENDED_LOGGING_TAG,"onRenegotiationNeeded - current state: "+currentState); + Log.d(EXTENDED_LOGGING_TAG, "onRenegotiationNeeded()"); + if (currentState != null && currentState != PeerConnection.PeerConnectionState.NEW) { + eventCallback.onRenegotiationNeeded(); + } } @Override @@ -293,6 +297,10 @@ synchronized void initializePeerConnection(final Set media, final List availableAudioDevices); + + void onRenegotiationNeeded(); } private static abstract class SetSdpObserver implements SdpObserver { diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 4e520e856..dae88c606 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -904,6 +904,7 @@ Incoming video call Connecting Connected + Reconnecting Accepting call Ending call Answer From 9843b72f6fc3993313c404c2ab0e5aa70b4c6a77 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 11 Nov 2021 15:23:45 +0100 Subject: [PATCH 011/394] always use Camera2Enumerator --- .../siacs/conversations/xmpp/jingle/WebRTCWrapper.java | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 6e4a94539..c1201206a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -476,16 +476,8 @@ void addIceCandidate(IceCandidate iceCandidate) { requirePeerConnection().addIceCandidate(iceCandidate); } - private CameraEnumerator getCameraEnumerator() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - return new Camera2Enumerator(requireContext()); - } else { - return new Camera1Enumerator(); - } - } - private Optional getVideoCapturer() { - final CameraEnumerator enumerator = getCameraEnumerator(); + final CameraEnumerator enumerator = new Camera2Enumerator(requireContext()); final Set deviceNames = ImmutableSet.copyOf(enumerator.getDeviceNames()); for (final String deviceName : deviceNames) { if (isFrontFacing(enumerator, deviceName)) { From 9c3f55bef220351e5eff5790b38ccec93aa6c573 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 11 Nov 2021 16:52:18 +0100 Subject: [PATCH 012/394] use stopwatch to keep track of jingle rtp session duration --- .../conversations/ui/RtpSessionActivity.java | 11 ++--- .../conversations/utils/TimeFrameUtils.java | 12 ++++-- .../xmpp/jingle/JingleRtpConnection.java | 40 ++++++++++--------- 3 files changed, 34 insertions(+), 29 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index b2cf583b5..d0bdbb788 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -939,14 +939,11 @@ private void updateCallDuration() { this.binding.duration.setVisibility(View.GONE); return; } - final long rtpConnectionStarted = connection.getRtpConnectionStarted(); - final long rtpConnectionEnded = connection.getRtpConnectionEnded(); - if (rtpConnectionStarted != 0) { - final long ended = rtpConnectionEnded == 0 ? SystemClock.elapsedRealtime() : rtpConnectionEnded; - this.binding.duration.setText(TimeFrameUtils.formatTimePassed(rtpConnectionStarted, ended, false)); - this.binding.duration.setVisibility(View.VISIBLE); - } else { + if (connection.zeroDuration()) { this.binding.duration.setVisibility(View.GONE); + } else { + this.binding.duration.setText(TimeFrameUtils.formatElapsedTime(connection.getCallDuration(), false)); + this.binding.duration.setVisibility(View.VISIBLE); } } diff --git a/src/main/java/eu/siacs/conversations/utils/TimeFrameUtils.java b/src/main/java/eu/siacs/conversations/utils/TimeFrameUtils.java index 1cb78db0c..9e7946d57 100644 --- a/src/main/java/eu/siacs/conversations/utils/TimeFrameUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/TimeFrameUtils.java @@ -71,10 +71,14 @@ public static String formatTimePassed(final long since, final boolean withMillis public static String formatTimePassed(final long since, final long to, final boolean withMilliseconds) { final long passed = (since < 0) ? 0 : (to - since); - final int hours = (int) (passed / 3600000); - final int minutes = (int) (passed / 60000) % 60; - final int seconds = (int) (passed / 1000) % 60; - final int milliseconds = (int) (passed / 100) % 10; + return formatElapsedTime(passed, withMilliseconds); + } + + public static String formatElapsedTime(final long elapsed, final boolean withMilliseconds) { + final int hours = (int) (elapsed / 3600000); + final int minutes = (int) (elapsed / 60000) % 60; + final int seconds = (int) (elapsed / 1000) % 60; + final int milliseconds = (int) (elapsed / 100) % 10; if (hours > 0) { return String.format(Locale.ENGLISH, "%d:%02d:%02d", hours, minutes, seconds); } else if (withMilliseconds) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 2d322ead9..ed1f35dd2 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -1,6 +1,5 @@ package eu.siacs.conversations.xmpp.jingle; -import android.os.SystemClock; import android.util.Log; import androidx.annotation.NonNull; @@ -8,6 +7,7 @@ import com.google.common.base.Optional; import com.google.common.base.Preconditions; +import com.google.common.base.Stopwatch; import com.google.common.base.Strings; import com.google.common.base.Throwables; import com.google.common.collect.Collections2; @@ -29,8 +29,10 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Queue; import java.util.Set; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -147,8 +149,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private Set proposedMedia; private RtpContentMap initiatorRtpContentMap; private RtpContentMap responderRtpContentMap; - private long rtpConnectionStarted = 0; //time of 'connected' - private long rtpConnectionEnded = 0; + private final Stopwatch sessionDuration = Stopwatch.createUnstarted(); + private final Queue stateHistory = new LinkedList<>(); private ScheduledFuture ringingTimeoutFuture; JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) { @@ -1056,7 +1058,7 @@ public RtpEndUserState getEndUserState() { return RtpEndUserState.RETRACTED; } case TERMINATED_CONNECTIVITY_ERROR: - return rtpConnectionStarted == 0 ? RtpEndUserState.CONNECTIVITY_ERROR : RtpEndUserState.CONNECTIVITY_LOST_ERROR; + return zeroDuration() ? RtpEndUserState.CONNECTIVITY_ERROR : RtpEndUserState.CONNECTIVITY_LOST_ERROR; case TERMINATED_APPLICATION_FAILURE: return RtpEndUserState.APPLICATION_ERROR; case TERMINATED_SECURITY_ERROR: @@ -1084,7 +1086,7 @@ private RtpEndUserState getPeerConnectionStateAsEndUserState() { case CLOSED: return RtpEndUserState.ENDING_CALL; default: - return rtpConnectionStarted == 0 ? RtpEndUserState.CONNECTIVITY_ERROR : RtpEndUserState.RECONNECTING; + return zeroDuration() ? RtpEndUserState.CONNECTIVITY_ERROR : RtpEndUserState.RECONNECTING; } } @@ -1338,15 +1340,18 @@ public void onIceCandidate(final IceCandidate iceCandidate) { @Override public void onConnectionChange(final PeerConnection.PeerConnectionState oldState, final PeerConnection.PeerConnectionState newState) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnectionState changed: " + oldState + "->" + newState); - final boolean neverConnected = this.rtpConnectionStarted == 0; - if (newState == PeerConnection.PeerConnectionState.CONNECTED && this.rtpConnectionStarted == 0) { - this.rtpConnectionStarted = SystemClock.elapsedRealtime(); - } - if (newState == PeerConnection.PeerConnectionState.CLOSED && this.rtpConnectionEnded == 0) { - this.rtpConnectionEnded = SystemClock.elapsedRealtime(); + this.stateHistory.add(newState); + if (newState == PeerConnection.PeerConnectionState.CONNECTED) { + this.sessionDuration.start(); + } else if (this.sessionDuration.isRunning()) { + this.sessionDuration.stop(); } - if (neverConnected && Arrays.asList(PeerConnection.PeerConnectionState.FAILED, PeerConnection.PeerConnectionState.DISCONNECTED).contains(newState)) { + final boolean neverConnected = !this.stateHistory.contains(PeerConnection.PeerConnectionState.CONNECTED); + final boolean failedOrDisconnected = Arrays.asList(PeerConnection.PeerConnectionState.FAILED, PeerConnection.PeerConnectionState.DISCONNECTED).contains(newState); + + + if (neverConnected && failedOrDisconnected) { if (isTerminated()) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": not sending session-terminate after connectivity error because session is already in state " + this.state); return; @@ -1375,12 +1380,12 @@ private void closeWebRTCSessionAfterFailedConnection() { } } - public long getRtpConnectionStarted() { - return this.rtpConnectionStarted; + public boolean zeroDuration() { + return this.sessionDuration.elapsed(TimeUnit.NANOSECONDS) <= 0; } - public long getRtpConnectionEnded() { - return this.rtpConnectionEnded; + public long getCallDuration() { + return this.sessionDuration.elapsed(TimeUnit.MILLISECONDS); } public AppRTCAudioManager getAudioManager() { @@ -1507,8 +1512,7 @@ private void finish() { } private void writeLogMessage(final State state) { - final long started = this.rtpConnectionStarted; - long duration = started <= 0 ? 0 : SystemClock.elapsedRealtime() - started; + final long duration = getCallDuration(); if (state == State.TERMINATED_SUCCESS || (state == State.TERMINATED_CONNECTIVITY_ERROR && duration > 0)) { writeLogMessageSuccess(duration); } else { From b6dee6da6a16a383ca1cac097719e5aebafd77b5 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 11 Nov 2021 17:05:32 +0100 Subject: [PATCH 013/394] reverse: webrtc: include oldState in onConnectionChange MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit turns out we don’t need it and a better way is for RtpConnection to keep track of *all* states in the current generation --- .../conversations/xmpp/jingle/JingleRtpConnection.java | 4 ++-- .../siacs/conversations/xmpp/jingle/WebRTCWrapper.java | 10 +++------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index ed1f35dd2..37c51bfdc 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -1338,8 +1338,8 @@ public void onIceCandidate(final IceCandidate iceCandidate) { } @Override - public void onConnectionChange(final PeerConnection.PeerConnectionState oldState, final PeerConnection.PeerConnectionState newState) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnectionState changed: " + oldState + "->" + newState); + public void onConnectionChange(final PeerConnection.PeerConnectionState newState) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnectionState changed to" + newState); this.stateHistory.add(newState); if (newState == PeerConnection.PeerConnectionState.CONNECTED) { this.sessionDuration.start(); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index c1201206a..5ee6c5d5e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -88,7 +88,6 @@ public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDev private final Handler mainHandler = new Handler(Looper.getMainLooper()); private VideoTrack localVideoTrack = null; private VideoTrack remoteVideoTrack = null; - private PeerConnection.PeerConnectionState currentState; private final PeerConnection.Observer peerConnectionObserver = new PeerConnection.Observer() { @Override public void onSignalingChange(PeerConnection.SignalingState signalingState) { @@ -100,9 +99,7 @@ public void onSignalingChange(PeerConnection.SignalingState signalingState) { @Override public void onConnectionChange(final PeerConnection.PeerConnectionState newState) { - final PeerConnection.PeerConnectionState oldState = currentState; - currentState = newState; - eventCallback.onConnectionChange(oldState, newState); + eventCallback.onConnectionChange(newState); } @Override @@ -154,6 +151,7 @@ public void onDataChannel(DataChannel dataChannel) { @Override public void onRenegotiationNeeded() { Log.d(EXTENDED_LOGGING_TAG, "onRenegotiationNeeded()"); + final PeerConnection.PeerConnectionState currentState = peerConnection == null ? null : peerConnection.connectionState(); if (currentState != null && currentState != PeerConnection.PeerConnectionState.NEW) { eventCallback.onRenegotiationNeeded(); } @@ -267,8 +265,6 @@ synchronized void initializePeerConnection(final Set media, final List optionalCapturerChoice = media.contains(Media.VIDEO) ? getVideoCapturer() : Optional.absent(); if (optionalCapturerChoice.isPresent()) { @@ -535,7 +531,7 @@ AppRTCAudioManager getAudioManager() { public interface EventCallback { void onIceCandidate(IceCandidate iceCandidate); - void onConnectionChange(PeerConnection.PeerConnectionState oldState, PeerConnection.PeerConnectionState newState); + void onConnectionChange(PeerConnection.PeerConnectionState newState); void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices); From 717c83753f24ffbccabe17c23100268f9146040a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 11 Nov 2021 21:02:15 +0100 Subject: [PATCH 014/394] delay discovered ice candidates until JingleRtpConnection is ready to receive otherwise setLocalDescription and the arrival of first candidates might race the rtpContentDescription being set --- .../xmpp/jingle/JingleRtpConnection.java | 39 +++++++++++++++++-- .../xmpp/jingle/WebRTCWrapper.java | 28 ++++++++++++- 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 37c51bfdc..4796d0c58 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -34,6 +34,7 @@ import java.util.Map; import java.util.Queue; import java.util.Set; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -567,6 +568,7 @@ private void prepareSessionAccept(final org.webrtc.SessionDescription webRTCSess final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription); this.responderRtpContentMap = respondingRtpContentMap; + webRTCWrapper.setIsReadyToReceiveIceCandidates(true); final ListenableFuture outgoingContentMapFuture = prepareOutgoingContentMap(respondingRtpContentMap); Futures.addCallback(outgoingContentMapFuture, new FutureCallback() { @@ -872,6 +874,7 @@ private void prepareSessionInitiate(final org.webrtc.SessionDescription webRTCSe final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); this.initiatorRtpContentMap = rtpContentMap; + this.webRTCWrapper.setIsReadyToReceiveIceCandidates(true); final ListenableFuture outgoingContentMapFuture = encryptSessionInitiate(rtpContentMap); Futures.addCallback(outgoingContentMapFuture, new FutureCallback() { @Override @@ -1339,7 +1342,7 @@ public void onIceCandidate(final IceCandidate iceCandidate) { @Override public void onConnectionChange(final PeerConnection.PeerConnectionState newState) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnectionState changed to" + newState); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnectionState changed to " + newState); this.stateHistory.add(newState); if (newState == PeerConnection.PeerConnectionState.CONNECTED) { this.sessionDuration.start(); @@ -1348,7 +1351,10 @@ public void onConnectionChange(final PeerConnection.PeerConnectionState newState } final boolean neverConnected = !this.stateHistory.contains(PeerConnection.PeerConnectionState.CONNECTED); - final boolean failedOrDisconnected = Arrays.asList(PeerConnection.PeerConnectionState.FAILED, PeerConnection.PeerConnectionState.DISCONNECTED).contains(newState); + final boolean failedOrDisconnected = Arrays.asList( + PeerConnection.PeerConnectionState.FAILED, + PeerConnection.PeerConnectionState.DISCONNECTED + ).contains(newState); if (neverConnected && failedOrDisconnected) { @@ -1356,7 +1362,7 @@ public void onConnectionChange(final PeerConnection.PeerConnectionState newState Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": not sending session-terminate after connectivity error because session is already in state " + this.state); return; } - new Thread(this::closeWebRTCSessionAfterFailedConnection).start(); + webRTCWrapper.execute(this::closeWebRTCSessionAfterFailedConnection); } else if (newState == PeerConnection.PeerConnectionState.FAILED) { Log.d(Config.LOGTAG, "attempting to restart ICE"); webRTCWrapper.restartIce(); @@ -1367,6 +1373,33 @@ public void onConnectionChange(final PeerConnection.PeerConnectionState newState @Override public void onRenegotiationNeeded() { Log.d(Config.LOGTAG, "onRenegotiationNeeded()"); + this.webRTCWrapper.execute(this::renegotiate); + } + + private void renegotiate() { + this.webRTCWrapper.setIsReadyToReceiveIceCandidates(false); + try { + final SessionDescription sessionDescription = setLocalSessionDescription(); + final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); + setRenegotiatedContentMap(rtpContentMap); + this.webRTCWrapper.setIsReadyToReceiveIceCandidates(true); + } catch (final Exception e) { + Log.d(Config.LOGTAG, "failed to renegotiate", e); + //TODO send some sort of failure (comparable to when initiating) + } + } + + private void setRenegotiatedContentMap(final RtpContentMap rtpContentMap) { + if (isInitiator()) { + this.initiatorRtpContentMap = rtpContentMap; + } else { + this.responderRtpContentMap = rtpContentMap; + } + } + + private SessionDescription setLocalSessionDescription() throws ExecutionException, InterruptedException { + final org.webrtc.SessionDescription sessionDescription = this.webRTCWrapper.setLocalDescription().get(); + return SessionDescription.parse(sessionDescription.description); } private void closeWebRTCSessionAfterFailedConnection() { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 5ee6c5d5e..9ea4cd389 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -45,8 +45,13 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedList; import java.util.List; +import java.util.Queue; import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -59,6 +64,8 @@ public class WebRTCWrapper { private static final String EXTENDED_LOGGING_TAG = WebRTCWrapper.class.getSimpleName(); + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + //we should probably keep this in sync with: https://github.com/signalapp/Signal-Android/blob/master/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java#L296 private static final Set HARDWARE_AEC_BLACKLIST = new ImmutableSet.Builder() .add("Pixel") @@ -79,6 +86,8 @@ public class WebRTCWrapper { private static final int CAPTURING_MAX_FRAME_RATE = 30; private final EventCallback eventCallback; + private final AtomicBoolean readyToReceivedIceCandidates = new AtomicBoolean(false); + private final Queue iceCandidates = new LinkedList<>(); private final AppRTCAudioManager.AudioManagerEvents audioManagerEvents = new AppRTCAudioManager.AudioManagerEvents() { @Override public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices) { @@ -125,7 +134,11 @@ public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringSt @Override public void onIceCandidate(IceCandidate iceCandidate) { - eventCallback.onIceCandidate(iceCandidate); + if (readyToReceivedIceCandidates.get()) { + eventCallback.onIceCandidate(iceCandidate); + } else { + iceCandidates.add(iceCandidate); + } } @Override @@ -294,7 +307,14 @@ synchronized void initializePeerConnection(final Set media, final List requirePeerConnection().restartIce()); + } + + public void setIsReadyToReceiveIceCandidates(final boolean ready) { + readyToReceivedIceCandidates.set(ready); + while(ready && iceCandidates.peek() != null) { + eventCallback.onIceCandidate(iceCandidates.poll()); + } } synchronized void close() { @@ -528,6 +548,10 @@ AppRTCAudioManager getAudioManager() { return appRTCAudioManager; } + void execute(final Runnable command) { + executorService.execute(command); + } + public interface EventCallback { void onIceCandidate(IceCandidate iceCandidate); From 5b80c62a637fabea89ceafb39dc9353bc50773d6 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 14 Nov 2021 18:22:18 +0100 Subject: [PATCH 015/394] treat transport-info w/o candidates and changed credentials as offer --- .../eu/siacs/conversations/xml/Element.java | 5 +- .../xmpp/jingle/JingleRtpConnection.java | 193 +++++++++++++----- .../xmpp/jingle/RtpContentMap.java | 34 ++- .../xmpp/jingle/WebRTCWrapper.java | 34 ++- .../jingle/stanzas/IceUdpTransportInfo.java | 45 +++- 5 files changed, 254 insertions(+), 57 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xml/Element.java b/src/main/java/eu/siacs/conversations/xml/Element.java index c0ece7f4c..4d53a17b7 100644 --- a/src/main/java/eu/siacs/conversations/xml/Element.java +++ b/src/main/java/eu/siacs/conversations/xml/Element.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.xml; +import org.jetbrains.annotations.NotNull; + import java.util.ArrayList; import java.util.Hashtable; import java.util.List; @@ -165,8 +167,9 @@ public Hashtable getAttributes() { return this.attributes; } + @NotNull public String toString() { - StringBuilder elementOutput = new StringBuilder(); + final StringBuilder elementOutput = new StringBuilder(); if ((content == null) && (children.size() == 0)) { Tag emptyTag = Tag.empty(name); emptyTag.setAtttributes(this.attributes); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 4796d0c58..71cdb02c4 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -25,7 +25,6 @@ import org.webrtc.PeerConnection; import org.webrtc.VideoTrack; -import java.util.ArrayDeque; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -142,7 +141,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private final WebRTCWrapper webRTCWrapper = new WebRTCWrapper(this); - private final ArrayDeque>> pendingIceCandidates = new ArrayDeque<>(); + //TODO convert to Queue>? + private final Queue> pendingIceCandidates = new LinkedList<>(); private final OmemoVerification omemoVerification = new OmemoVerification(); private final Message message; private State state = State.NULL; @@ -193,7 +193,6 @@ private static State reasonToState(Reason reason) { @Override synchronized void deliverPacket(final JinglePacket jinglePacket) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": packet delivered to JingleRtpConnection"); switch (jinglePacket.getAction()) { case SESSION_INITIATE: receiveSessionInitiate(jinglePacket); @@ -254,23 +253,29 @@ private void receiveSessionTerminate(final JinglePacket jinglePacket) { private void receiveTransportInfo(final JinglePacket jinglePacket) { //Due to the asynchronicity of processing session-init we might move from NULL|PROCEED to INITIALIZED only after transport-info has been received if (isInState(State.NULL, State.PROCEED, State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) { - respondOk(jinglePacket); final RtpContentMap contentMap; try { contentMap = RtpContentMap.of(jinglePacket); - } catch (IllegalArgumentException | NullPointerException e) { + } catch (final IllegalArgumentException | NullPointerException e) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents; ignoring", e); + respondOk(jinglePacket); return; } final Set> candidates = contentMap.contents.entrySet(); if (this.state == State.SESSION_ACCEPTED) { + //zero candidates + modified credentials are an ICE restart offer + if (checkForIceRestart(contentMap, jinglePacket)) { + return; + } + respondOk(jinglePacket); try { processCandidates(candidates); } catch (final WebRTCWrapper.PeerConnectionNotInitialized e) { Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnection was not initialized when processing transport info. this usually indicates a race condition that can be ignored"); } } else { - pendingIceCandidates.push(candidates); + respondOk(jinglePacket); + pendingIceCandidates.addAll(candidates); } } else { if (isTerminated()) { @@ -283,37 +288,106 @@ private void receiveTransportInfo(final JinglePacket jinglePacket) { } } - private void processCandidates(final Set> contents) { - final RtpContentMap rtpContentMap = isInitiator() ? this.responderRtpContentMap : this.initiatorRtpContentMap; - final Group originalGroup = rtpContentMap.group; - final List identificationTags = originalGroup == null ? rtpContentMap.getNames() : originalGroup.getIdentificationTags(); - if (identificationTags.size() == 0) { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": no identification tags found in initial offer. we won't be able to calculate mLineIndices"); + private boolean checkForIceRestart(final RtpContentMap rtpContentMap, final JinglePacket jinglePacket) { + final RtpContentMap existing = getRemoteContentMap(); + final Map existingCredentials = existing.getCredentials(); + final Map newCredentials = rtpContentMap.getCredentials(); + if (!existingCredentials.keySet().equals(newCredentials.keySet())) { + return false; + } + if (existingCredentials.equals(newCredentials)) { + return false; + } + final boolean isOffer = rtpContentMap.emptyCandidates(); + Log.d(Config.LOGTAG, "detected ICE restart. offer=" + isOffer + " " + jinglePacket); + //TODO reset to 'actpass'? + final RtpContentMap restartContentMap = existing.modifiedCredentials(newCredentials); + try { + if (applyIceRestart(isOffer, restartContentMap)) { + return false; + } else { + Log.d(Config.LOGTAG, "responding with tie break"); + //TODO respond with conflict + return true; + } + } catch (Exception e) { + Log.d(Config.LOGTAG, "failure to apply ICE restart. sending error", e); + //TODO send some kind of error + return true; } - processCandidates(identificationTags, contents); } - private void processCandidates(final List indices, final Set> contents) { + private boolean applyIceRestart(final boolean isOffer, final RtpContentMap restartContentMap) throws ExecutionException, InterruptedException { + final SessionDescription sessionDescription = SessionDescription.of(restartContentMap); + final org.webrtc.SessionDescription.Type type = isOffer ? org.webrtc.SessionDescription.Type.OFFER : org.webrtc.SessionDescription.Type.ANSWER; + org.webrtc.SessionDescription sdp = new org.webrtc.SessionDescription(type, sessionDescription.toString()); + if (isOffer && webRTCWrapper.getSignalingState() != PeerConnection.SignalingState.STABLE) { + if (isInitiator()) { + //We ignore the offer and respond with tie-break. This will clause the responder not to apply the content map + return false; + } + //rollback our own local description. should happen automatically but doesn't + webRTCWrapper.rollbackLocalDescription().get(); + } + webRTCWrapper.setRemoteDescription(sdp).get(); + if (isInitiator()) { + this.responderRtpContentMap = restartContentMap; + } else { + this.initiatorRtpContentMap = restartContentMap; + } + if (isOffer) { + webRTCWrapper.setIsReadyToReceiveIceCandidates(false); + final SessionDescription localSessionDescription = setLocalSessionDescription(); + setLocalContentMap(RtpContentMap.of(localSessionDescription)); + webRTCWrapper.setIsReadyToReceiveIceCandidates(true); + } + return true; + } + + private void processCandidates(final Set> contents) { for (final Map.Entry content : contents) { - final String ufrag = content.getValue().transport.getAttribute("ufrag"); - for (final IceUdpTransportInfo.Candidate candidate : content.getValue().transport.getCandidates()) { - final String sdp; - try { - sdp = candidate.toSdpAttribute(ufrag); - } catch (IllegalArgumentException e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring invalid ICE candidate " + e.getMessage()); - continue; - } - final String sdpMid = content.getKey(); - final int mLineIndex = indices.indexOf(sdpMid); - if (mLineIndex < 0) { - Log.w(Config.LOGTAG, "mLineIndex not found for " + sdpMid + ". available indices " + indices); - } - final IceCandidate iceCandidate = new IceCandidate(sdpMid, mLineIndex, sdp); - Log.d(Config.LOGTAG, "received candidate: " + iceCandidate); - this.webRTCWrapper.addIceCandidate(iceCandidate); + processCandidate(content); + } + } + + private void processCandidate(final Map.Entry content) { + final RtpContentMap rtpContentMap = getRemoteContentMap(); + final List indices = toIdentificationTags(rtpContentMap); + final String sdpMid = content.getKey(); //aka content name + final IceUdpTransportInfo transport = content.getValue().transport; + final IceUdpTransportInfo.Credentials credentials = transport.getCredentials(); + + //TODO check that credentials remained the same + + for (final IceUdpTransportInfo.Candidate candidate : transport.getCandidates()) { + final String sdp; + try { + sdp = candidate.toSdpAttribute(credentials.ufrag); + } catch (final IllegalArgumentException e) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring invalid ICE candidate " + e.getMessage()); + continue; } + final int mLineIndex = indices.indexOf(sdpMid); + if (mLineIndex < 0) { + Log.w(Config.LOGTAG, "mLineIndex not found for " + sdpMid + ". available indices " + indices); + } + final IceCandidate iceCandidate = new IceCandidate(sdpMid, mLineIndex, sdp); + Log.d(Config.LOGTAG, "received candidate: " + iceCandidate); + this.webRTCWrapper.addIceCandidate(iceCandidate); + } + } + + private RtpContentMap getRemoteContentMap() { + return isInitiator() ? this.responderRtpContentMap : this.initiatorRtpContentMap; + } + + private List toIdentificationTags(final RtpContentMap rtpContentMap) { + final Group originalGroup = rtpContentMap.group; + final List identificationTags = originalGroup == null ? rtpContentMap.getNames() : originalGroup.getIdentificationTags(); + if (identificationTags.size() == 0) { + Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": no identification tags found in initial offer. we won't be able to calculate mLineIndices"); } + return identificationTags; } private ListenableFuture receiveRtpContentMap(final JinglePacket jinglePacket, final boolean expectVerification) { @@ -401,11 +475,7 @@ private void receiveSessionInitiate(final JinglePacket jinglePacket, final RtpCo } if (transition(target, () -> this.initiatorRtpContentMap = contentMap)) { respondOk(jinglePacket); - - final Set> candidates = contentMap.contents.entrySet(); - if (candidates.size() > 0) { - pendingIceCandidates.push(candidates); - } + pendingIceCandidates.addAll(contentMap.contents.entrySet()); if (target == State.SESSION_INITIALIZED_PRE_APPROVED) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": automatically accepting session-initiate"); sendSessionAccept(); @@ -495,8 +565,7 @@ private void receiveSessionAccept(final RtpContentMap contentMap) { sendSessionTerminate(Reason.FAILED_APPLICATION); return; } - final List identificationTags = contentMap.group == null ? contentMap.getNames() : contentMap.group.getIdentificationTags(); - processCandidates(identificationTags, contentMap.contents.entrySet()); + processCandidates(contentMap.contents.entrySet()); } private void sendSessionAccept() { @@ -558,9 +627,10 @@ private void failureToAcceptSession(final Throwable throwable) { } private void addIceCandidatesFromBlackLog() { - while (!this.pendingIceCandidates.isEmpty()) { - processCandidates(this.pendingIceCandidates.poll()); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": added candidates from back log"); + Map.Entry foo; + while ((foo = this.pendingIceCandidates.poll()) != null) { + processCandidate(foo); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": added candidate from back log"); } } @@ -1335,7 +1405,13 @@ void transitionOrThrow(final State target) { @Override public void onIceCandidate(final IceCandidate iceCandidate) { - final IceUdpTransportInfo.Candidate candidate = IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp); + final RtpContentMap rtpContentMap = isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap; + final Collection currentUfrags = Collections2.transform(rtpContentMap.getCredentials().values(), c -> c.ufrag); + final IceUdpTransportInfo.Candidate candidate = IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp, currentUfrags); + if (candidate == null) { + Log.d(Config.LOGTAG,"ignoring (not sending) candidate: "+iceCandidate.toString()); + return; + } Log.d(Config.LOGTAG, "sending candidate: " + iceCandidate.toString()); sendTransportInfo(iceCandidate.sdpMid, candidate); } @@ -1373,23 +1449,42 @@ public void onConnectionChange(final PeerConnection.PeerConnectionState newState @Override public void onRenegotiationNeeded() { Log.d(Config.LOGTAG, "onRenegotiationNeeded()"); - this.webRTCWrapper.execute(this::renegotiate); + this.webRTCWrapper.execute(this::initiateIceRestart); } - private void renegotiate() { + private void initiateIceRestart() { + PeerConnection.SignalingState signalingState = webRTCWrapper.getSignalingState(); + Log.d(Config.LOGTAG, "initiateIceRestart() - " + signalingState); + if (signalingState != PeerConnection.SignalingState.STABLE) { + return; + } this.webRTCWrapper.setIsReadyToReceiveIceCandidates(false); + final SessionDescription sessionDescription; try { - final SessionDescription sessionDescription = setLocalSessionDescription(); - final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); - setRenegotiatedContentMap(rtpContentMap); - this.webRTCWrapper.setIsReadyToReceiveIceCandidates(true); + sessionDescription = setLocalSessionDescription(); } catch (final Exception e) { Log.d(Config.LOGTAG, "failed to renegotiate", e); //TODO send some sort of failure (comparable to when initiating) + return; } + final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); + final RtpContentMap transportInfo = rtpContentMap.transportInfo(); + final JinglePacket jinglePacket = transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); + Log.d(Config.LOGTAG, "initiating ice restart: " + jinglePacket); + jinglePacket.setTo(id.with); + xmppConnectionService.sendIqPacket(id.account, jinglePacket, (account, response) -> { + if (response.getType() == IqPacket.TYPE.RESULT) { + Log.d(Config.LOGTAG, "received success to our ice restart"); + setLocalContentMap(rtpContentMap); + webRTCWrapper.setIsReadyToReceiveIceCandidates(true); + } else { + Log.d(Config.LOGTAG, "received failure to our ice restart"); + //TODO handle tie-break. Rollback? + } + }); } - private void setRenegotiatedContentMap(final RtpContentMap rtpContentMap) { + private void setLocalContentMap(final RtpContentMap rtpContentMap) { if (isInitiator()) { this.initiatorRtpContentMap = rtpContentMap; } else { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java index 9baffcf81..99db8bd34 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -1,7 +1,5 @@ package eu.siacs.conversations.xmpp.jingle; -import android.util.Log; - import com.google.common.base.Function; import com.google.common.base.Preconditions; import com.google.common.base.Strings; @@ -17,9 +15,9 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; -import eu.siacs.conversations.Config; import eu.siacs.conversations.xmpp.jingle.stanzas.Content; import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo; @@ -137,7 +135,37 @@ RtpContentMap transportInfo(final String contentName, final IceUdpTransportInfo. final IceUdpTransportInfo newTransportInfo = transportInfo.cloneWrapper(); newTransportInfo.addChild(candidate); return new RtpContentMap(null, ImmutableMap.of(contentName, new DescriptionTransport(null, newTransportInfo))); + } + + RtpContentMap transportInfo() { + return new RtpContentMap( + null, + Maps.transformValues(contents, dt -> new DescriptionTransport(null, dt.transport.cloneWrapper())) + ); + } + + public Map getCredentials() { + return Maps.transformValues(contents, dt -> dt.transport.getCredentials()); + } + public boolean emptyCandidates() { + int count = 0; + for (DescriptionTransport descriptionTransport : contents.values()) { + count += descriptionTransport.transport.getCandidates().size(); + } + return count == 0; + } + + public RtpContentMap modifiedCredentials(Map credentialsMap) { + final ImmutableMap.Builder contentMapBuilder = new ImmutableMap.Builder<>(); + for (final Map.Entry content : contents.entrySet()) { + final RtpDescription rtpDescription = content.getValue().description; + IceUdpTransportInfo transportInfo = content.getValue().transport; + final IceUdpTransportInfo.Credentials credentials = Objects.requireNonNull(credentialsMap.get(content.getKey())); + final IceUdpTransportInfo modifiedTransportInfo = transportInfo.modifyCredentials(credentials); + contentMapBuilder.put(content.getKey(), new DescriptionTransport(rtpDescription, modifiedTransportInfo)); + } + return new RtpContentMap(this.group, contentMapBuilder.build()); } public static class DescriptionTransport { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 9ea4cd389..401121b86 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -17,7 +17,6 @@ import org.webrtc.AudioSource; import org.webrtc.AudioTrack; -import org.webrtc.Camera1Enumerator; import org.webrtc.Camera2Enumerator; import org.webrtc.CameraEnumerationAndroid; import org.webrtc.CameraEnumerator; @@ -87,6 +86,7 @@ public class WebRTCWrapper { private final EventCallback eventCallback; private final AtomicBoolean readyToReceivedIceCandidates = new AtomicBoolean(false); + private final AtomicBoolean ignoreOnRenegotiationNeeded = new AtomicBoolean(false); private final Queue iceCandidates = new LinkedList<>(); private final AppRTCAudioManager.AudioManagerEvents audioManagerEvents = new AppRTCAudioManager.AudioManagerEvents() { @Override @@ -163,6 +163,10 @@ public void onDataChannel(DataChannel dataChannel) { @Override public void onRenegotiationNeeded() { + if (ignoreOnRenegotiationNeeded.get()) { + Log.d(EXTENDED_LOGGING_TAG, "ignoring onRenegotiationNeeded()"); + return; + } Log.d(EXTENDED_LOGGING_TAG, "onRenegotiationNeeded()"); final PeerConnection.PeerConnectionState currentState = peerConnection == null ? null : peerConnection.connectionState(); if (currentState != null && currentState != PeerConnection.PeerConnectionState.NEW) { @@ -307,12 +311,12 @@ synchronized void initializePeerConnection(final Set media, final List requirePeerConnection().restartIce()); + executorService.execute(() -> requirePeerConnection().restartIce()); } public void setIsReadyToReceiveIceCandidates(final boolean ready) { readyToReceivedIceCandidates.set(ready); - while(ready && iceCandidates.peek() != null) { + while (ready && iceCandidates.peek() != null) { eventCallback.onIceCandidate(iceCandidates.poll()); } } @@ -452,6 +456,26 @@ public void onSetFailure(final String message) { }, MoreExecutors.directExecutor()); } + public ListenableFuture rollbackLocalDescription() { + final SettableFuture future = SettableFuture.create(); + final SessionDescription rollback = new SessionDescription(SessionDescription.Type.ROLLBACK, ""); + ignoreOnRenegotiationNeeded.set(true); + requirePeerConnection().setLocalDescription(new SetSdpObserver() { + @Override + public void onSetSuccess() { + future.set(null); + ignoreOnRenegotiationNeeded.set(false); + } + + @Override + public void onSetFailure(final String message) { + future.setException(new FailureToSetDescriptionException(message)); + } + }, rollback); + return future; + } + + private static void logDescription(final SessionDescription sessionDescription) { for (final String line : sessionDescription.description.split(eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) { Log.d(EXTENDED_LOGGING_TAG, line); @@ -552,6 +576,10 @@ void execute(final Runnable command) { executorService.execute(command); } + public PeerConnection.SignalingState getSignalingState() { + return requirePeerConnection().signalingState(); + } + public interface EventCallback { void onIceCandidate(IceCandidate iceCandidate); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java index 022c4d2dd..2b8770578 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java @@ -1,6 +1,7 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; import com.google.common.base.Joiner; +import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.ArrayListMultimap; @@ -8,6 +9,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; +import java.util.Collection; import java.util.HashMap; import java.util.Hashtable; import java.util.LinkedHashMap; @@ -58,6 +60,12 @@ public Fingerprint getFingerprint() { return fingerprint == null ? null : Fingerprint.upgrade(fingerprint); } + public Credentials getCredentials() { + final String ufrag = this.getAttribute("ufrag"); + final String password = this.getAttribute("pwd"); + return new Credentials(ufrag, password); + } + public List getCandidates() { final ImmutableList.Builder builder = new ImmutableList.Builder<>(); for (final Element child : getChildren()) { @@ -74,6 +82,37 @@ public IceUdpTransportInfo cloneWrapper() { return transportInfo; } + public IceUdpTransportInfo modifyCredentials(Credentials credentials) { + final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo(); + transportInfo.setAttribute("ufrag", credentials.ufrag); + transportInfo.setAttribute("pwd", credentials.password); + transportInfo.setChildren(getChildren()); + return transportInfo; + } + + public static class Credentials { + public final String ufrag; + public final String password; + + public Credentials(String ufrag, String password) { + this.ufrag = ufrag; + this.password = password; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Credentials that = (Credentials) o; + return Objects.equal(ufrag, that.ufrag) && Objects.equal(password, that.password); + } + + @Override + public int hashCode() { + return Objects.hashCode(ufrag, password); + } + } + public static class Candidate extends Element { private Candidate() { @@ -89,7 +128,7 @@ public static Candidate upgrade(final Element element) { } // https://tools.ietf.org/html/draft-ietf-mmusic-ice-sip-sdp-39#section-5.1 - public static Candidate fromSdpAttribute(final String attribute) { + public static Candidate fromSdpAttribute(final String attribute, Collection currentUfrags) { final String[] pair = attribute.split(":", 2); if (pair.length == 2 && "candidate".equals(pair[0])) { final String[] segments = pair[1].split(" "); @@ -105,6 +144,10 @@ public static Candidate fromSdpAttribute(final String attribute) { for (int i = 6; i < segments.length - 1; i = i + 2) { additional.put(segments[i], segments[i + 1]); } + final String ufrag = additional.get("ufrag"); + if (ufrag != null && !currentUfrags.contains(ufrag)) { + return null; + } final Candidate candidate = new Candidate(); candidate.setAttribute("component", component); candidate.setAttribute("foundation", foundation); From 3f402b132b122f0018a9b27afe846d558ae7b40c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 15 Nov 2021 13:03:04 +0100 Subject: [PATCH 016/394] respond with tie-break to prevent ICE restart race --- .../xmpp/jingle/JingleRtpConnection.java | 80 +++++++++++-------- .../jingle/stanzas/IceUdpTransportInfo.java | 9 +++ 2 files changed, 56 insertions(+), 33 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 71cdb02c4..e6a34cd8e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -261,22 +261,7 @@ private void receiveTransportInfo(final JinglePacket jinglePacket) { respondOk(jinglePacket); return; } - final Set> candidates = contentMap.contents.entrySet(); - if (this.state == State.SESSION_ACCEPTED) { - //zero candidates + modified credentials are an ICE restart offer - if (checkForIceRestart(contentMap, jinglePacket)) { - return; - } - respondOk(jinglePacket); - try { - processCandidates(candidates); - } catch (final WebRTCWrapper.PeerConnectionNotInitialized e) { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnection was not initialized when processing transport info. this usually indicates a race condition that can be ignored"); - } - } else { - respondOk(jinglePacket); - pendingIceCandidates.addAll(candidates); - } + receiveTransportInfo(jinglePacket, contentMap); } else { if (isTerminated()) { respondOk(jinglePacket); @@ -288,7 +273,26 @@ private void receiveTransportInfo(final JinglePacket jinglePacket) { } } - private boolean checkForIceRestart(final RtpContentMap rtpContentMap, final JinglePacket jinglePacket) { + private void receiveTransportInfo(final JinglePacket jinglePacket, final RtpContentMap contentMap) { + final Set> candidates = contentMap.contents.entrySet(); + if (this.state == State.SESSION_ACCEPTED) { + //zero candidates + modified credentials are an ICE restart offer + if (checkForIceRestart(jinglePacket, contentMap)) { + return; + } + respondOk(jinglePacket); + try { + processCandidates(candidates); + } catch (final WebRTCWrapper.PeerConnectionNotInitialized e) { + Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnection was not initialized when processing transport info. this usually indicates a race condition that can be ignored"); + } + } else { + respondOk(jinglePacket); + pendingIceCandidates.addAll(candidates); + } + } + + private boolean checkForIceRestart(final JinglePacket jinglePacket, final RtpContentMap rtpContentMap) { final RtpContentMap existing = getRemoteContentMap(); final Map existingCredentials = existing.getCredentials(); final Map newCredentials = rtpContentMap.getCredentials(); @@ -299,25 +303,30 @@ private boolean checkForIceRestart(final RtpContentMap rtpContentMap, final Jing return false; } final boolean isOffer = rtpContentMap.emptyCandidates(); - Log.d(Config.LOGTAG, "detected ICE restart. offer=" + isOffer + " " + jinglePacket); - //TODO reset to 'actpass'? + if (isOffer) { + Log.d(Config.LOGTAG, "received offer to restart ICE " + newCredentials); + } else { + Log.d(Config.LOGTAG, "received confirmation of ICE restart" + newCredentials); + } + //TODO rewrite setup attribute + //https://groups.google.com/g/discuss-webrtc/c/DfpIMwvUfeM + //https://datatracker.ietf.org/doc/html/draft-ietf-mmusic-dtls-sdp-15#section-5.5 final RtpContentMap restartContentMap = existing.modifiedCredentials(newCredentials); try { - if (applyIceRestart(isOffer, restartContentMap)) { - return false; + if (applyIceRestart(jinglePacket, restartContentMap, isOffer)) { + return isOffer; } else { - Log.d(Config.LOGTAG, "responding with tie break"); - //TODO respond with conflict + respondWithTieBreak(jinglePacket); return true; } } catch (Exception e) { Log.d(Config.LOGTAG, "failure to apply ICE restart. sending error", e); - //TODO send some kind of error + //TODO respond OK and then terminate session return true; } } - private boolean applyIceRestart(final boolean isOffer, final RtpContentMap restartContentMap) throws ExecutionException, InterruptedException { + private boolean applyIceRestart(final JinglePacket jinglePacket, final RtpContentMap restartContentMap, final boolean isOffer) throws ExecutionException, InterruptedException { final SessionDescription sessionDescription = SessionDescription.of(restartContentMap); final org.webrtc.SessionDescription.Type type = isOffer ? org.webrtc.SessionDescription.Type.OFFER : org.webrtc.SessionDescription.Type.ANSWER; org.webrtc.SessionDescription sdp = new org.webrtc.SessionDescription(type, sessionDescription.toString()); @@ -339,6 +348,8 @@ private boolean applyIceRestart(final boolean isOffer, final RtpContentMap resta webRTCWrapper.setIsReadyToReceiveIceCandidates(false); final SessionDescription localSessionDescription = setLocalSessionDescription(); setLocalContentMap(RtpContentMap.of(localSessionDescription)); + //We need to respond OK before sending any candidates + respondOk(jinglePacket); webRTCWrapper.setIsReadyToReceiveIceCandidates(true); } return true; @@ -447,6 +458,7 @@ public void onFailure(@NonNull final Throwable throwable) { private void receiveSessionInitiate(final JinglePacket jinglePacket, final RtpContentMap contentMap) { try { contentMap.requireContentDescriptions(); + //TODO require actpass contentMap.requireDTLSFingerprint(); } catch (final RuntimeException e) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", Throwables.getRootCause(e)); @@ -1072,8 +1084,16 @@ private void terminateWithOutOfOrder(final JinglePacket jinglePacket) { this.finish(); } + private void respondWithTieBreak(final JinglePacket jinglePacket) { + respondWithJingleError(jinglePacket, "tie-break", "conflict", "cancel"); + } + private void respondWithOutOfOrder(final JinglePacket jinglePacket) { - jingleConnectionManager.respondWithJingleError(id.account, jinglePacket, "out-of-order", "unexpected-request", "wait"); + respondWithJingleError(jinglePacket, "out-of-order", "unexpected-request", "wait"); + } + + void respondWithJingleError(final IqPacket original, String jingleCondition, String condition, String conditionType) { + jingleConnectionManager.respondWithJingleError(id.account, original, jingleCondition, condition, conditionType); } private void respondOk(final JinglePacket jinglePacket) { @@ -1409,7 +1429,7 @@ public void onIceCandidate(final IceCandidate iceCandidate) { final Collection currentUfrags = Collections2.transform(rtpContentMap.getCredentials().values(), c -> c.ufrag); final IceUdpTransportInfo.Candidate candidate = IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp, currentUfrags); if (candidate == null) { - Log.d(Config.LOGTAG,"ignoring (not sending) candidate: "+iceCandidate.toString()); + Log.d(Config.LOGTAG, "ignoring (not sending) candidate: " + iceCandidate.toString()); return; } Log.d(Config.LOGTAG, "sending candidate: " + iceCandidate.toString()); @@ -1448,16 +1468,10 @@ public void onConnectionChange(final PeerConnection.PeerConnectionState newState @Override public void onRenegotiationNeeded() { - Log.d(Config.LOGTAG, "onRenegotiationNeeded()"); this.webRTCWrapper.execute(this::initiateIceRestart); } private void initiateIceRestart() { - PeerConnection.SignalingState signalingState = webRTCWrapper.getSignalingState(); - Log.d(Config.LOGTAG, "initiateIceRestart() - " + signalingState); - if (signalingState != PeerConnection.SignalingState.STABLE) { - return; - } this.webRTCWrapper.setIsReadyToReceiveIceCandidates(false); final SessionDescription sessionDescription; try { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java index 2b8770578..1586557b7 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java @@ -1,6 +1,7 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; import com.google.common.base.Joiner; +import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.base.Strings; @@ -111,6 +112,14 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hashCode(ufrag, password); } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("ufrag", ufrag) + .add("password", password) + .toString(); + } } public static class Candidate extends Element { From 0a3947b8e308fbeb04492542dba3e726d81a3104 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 15 Nov 2021 17:18:43 +0100 Subject: [PATCH 017/394] terminate with application failure when failing to apply ICE restart This is fairly unlikely to happen in practice --- .../xmpp/jingle/JingleRtpConnection.java | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index e6a34cd8e..04c43ec1c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -319,14 +319,17 @@ private boolean checkForIceRestart(final JinglePacket jinglePacket, final RtpCon respondWithTieBreak(jinglePacket); return true; } - } catch (Exception e) { - Log.d(Config.LOGTAG, "failure to apply ICE restart. sending error", e); - //TODO respond OK and then terminate session + } catch (final Exception exception) { + respondOk(jinglePacket); + final Throwable rootCause = Throwables.getRootCause(exception); + Log.d(Config.LOGTAG, "failure to apply ICE restart", rootCause); + webRTCWrapper.close(); + sendSessionTerminate(Reason.ofThrowable(rootCause), rootCause.getMessage()); return true; } } - private boolean applyIceRestart(final JinglePacket jinglePacket, final RtpContentMap restartContentMap, final boolean isOffer) throws ExecutionException, InterruptedException { + private boolean applyIceRestart(final JinglePacket jinglePacket, final RtpContentMap restartContentMap, final boolean isOffer) throws ExecutionException, InterruptedException { final SessionDescription sessionDescription = SessionDescription.of(restartContentMap); final org.webrtc.SessionDescription.Type type = isOffer ? org.webrtc.SessionDescription.Type.OFFER : org.webrtc.SessionDescription.Type.ANSWER; org.webrtc.SessionDescription sdp = new org.webrtc.SessionDescription(type, sessionDescription.toString()); @@ -574,7 +577,7 @@ private void receiveSessionAccept(final RtpContentMap contentMap) { } catch (final Exception e) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to set remote description after receiving session-accept", Throwables.getRootCause(e)); webRTCWrapper.close(); - sendSessionTerminate(Reason.FAILED_APPLICATION); + sendSessionTerminate(Reason.FAILED_APPLICATION, Throwables.getRootCause(e).getMessage()); return; } processCandidates(contentMap.contents.entrySet()); @@ -624,7 +627,6 @@ private synchronized void sendSessionAccept(final Set media, final Sessio org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.setLocalDescription().get(); prepareSessionAccept(webRTCSessionDescription); } catch (final Exception e) { - //TODO sending the error text is worthwhile as well. Especially for FailureToSet exceptions failureToAcceptSession(e); } } @@ -633,9 +635,10 @@ private void failureToAcceptSession(final Throwable throwable) { if (isTerminated()) { return; } - Log.d(Config.LOGTAG, "unable to send session accept", Throwables.getRootCause(throwable)); + final Throwable rootCause = Throwables.getRootCause(throwable); + Log.d(Config.LOGTAG, "unable to send session accept", rootCause); webRTCWrapper.close(); - sendSessionTerminate(Reason.ofThrowable(throwable)); + sendSessionTerminate(Reason.ofThrowable(rootCause), rootCause.getMessage()); } private void addIceCandidatesFromBlackLog() { From 70b5d8d81aa3059623570972a58b7c595c1e1f26 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 15 Nov 2021 21:49:31 +0100 Subject: [PATCH 018/394] set proper peer dtls setup on ice restart received --- .../xmpp/jingle/JingleRtpConnection.java | 31 +++++++++----- .../xmpp/jingle/RtpContentMap.java | 16 ++++++-- .../xmpp/jingle/SessionDescription.java | 5 ++- .../jingle/stanzas/IceUdpTransportInfo.java | 40 +++++++++++++++++-- 4 files changed, 74 insertions(+), 18 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 04c43ec1c..c0ddfd96a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -303,16 +303,18 @@ private boolean checkForIceRestart(final JinglePacket jinglePacket, final RtpCon return false; } final boolean isOffer = rtpContentMap.emptyCandidates(); - if (isOffer) { - Log.d(Config.LOGTAG, "received offer to restart ICE " + newCredentials); - } else { - Log.d(Config.LOGTAG, "received confirmation of ICE restart" + newCredentials); - } - //TODO rewrite setup attribute - //https://groups.google.com/g/discuss-webrtc/c/DfpIMwvUfeM - //https://datatracker.ietf.org/doc/html/draft-ietf-mmusic-dtls-sdp-15#section-5.5 - final RtpContentMap restartContentMap = existing.modifiedCredentials(newCredentials); + final RtpContentMap restartContentMap; try { + if (isOffer) { + Log.d(Config.LOGTAG, "received offer to restart ICE " + newCredentials.values()); + restartContentMap = existing.modifiedCredentials(newCredentials, IceUdpTransportInfo.Setup.ACTPASS); + } else { + final IceUdpTransportInfo.Setup setup = getPeerDtlsSetup(); + Log.d(Config.LOGTAG, "received confirmation of ICE restart" + newCredentials.values()+" peer_setup="+setup); + // DTLS setup attribute needs to be rewritten to reflect current peer state + // https://groups.google.com/g/discuss-webrtc/c/DfpIMwvUfeM + restartContentMap = existing.modifiedCredentials(newCredentials, setup); + } if (applyIceRestart(jinglePacket, restartContentMap, isOffer)) { return isOffer; } else { @@ -329,6 +331,14 @@ private boolean checkForIceRestart(final JinglePacket jinglePacket, final RtpCon } } + private IceUdpTransportInfo.Setup getPeerDtlsSetup() { + final IceUdpTransportInfo.Setup responderSetup = this.responderRtpContentMap.getDtlsSetup(); + if (responderSetup == null || responderSetup == IceUdpTransportInfo.Setup.ACTPASS) { + throw new IllegalStateException("Invalid DTLS setup value in responder content map"); + } + return isInitiator() ? responderSetup : responderSetup.flip(); + } + private boolean applyIceRestart(final JinglePacket jinglePacket, final RtpContentMap restartContentMap, final boolean isOffer) throws ExecutionException, InterruptedException { final SessionDescription sessionDescription = SessionDescription.of(restartContentMap); final org.webrtc.SessionDescription.Type type = isOffer ? org.webrtc.SessionDescription.Type.OFFER : org.webrtc.SessionDescription.Type.ANSWER; @@ -1496,7 +1506,8 @@ private void initiateIceRestart() { webRTCWrapper.setIsReadyToReceiveIceCandidates(true); } else { Log.d(Config.LOGTAG, "received failure to our ice restart"); - //TODO handle tie-break. Rollback? + //TODO ignore tie break (maybe rollback?) + //TODO handle other errors } }); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java index 99db8bd34..5366460ec 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -6,6 +6,7 @@ import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import com.google.common.collect.Sets; @@ -104,7 +105,8 @@ void requireDTLSFingerprint() { if (fingerprint == null || Strings.isNullOrEmpty(fingerprint.getContent()) || Strings.isNullOrEmpty(fingerprint.getHash())) { throw new SecurityException(String.format("Use of DTLS-SRTP (XEP-0320) is required for content %s", entry.getKey())); } - if (Strings.isNullOrEmpty(fingerprint.getSetup())) { + final IceUdpTransportInfo.Setup setup = fingerprint.getSetup(); + if (setup == null) { throw new SecurityException(String.format("Use of DTLS-SRTP (XEP-0320) is required for content %s but missing setup attribute", entry.getKey())); } } @@ -148,6 +150,14 @@ public Map getCredentials() { return Maps.transformValues(contents, dt -> dt.transport.getCredentials()); } + public IceUdpTransportInfo.Setup getDtlsSetup() { + final Set setups = ImmutableSet.copyOf(Collections2.transform( + contents.values(), + dt->dt.transport.getFingerprint().getSetup() + )); + return setups.size() == 1 ? Iterables.getFirst(setups, null) : null; + } + public boolean emptyCandidates() { int count = 0; for (DescriptionTransport descriptionTransport : contents.values()) { @@ -156,13 +166,13 @@ public boolean emptyCandidates() { return count == 0; } - public RtpContentMap modifiedCredentials(Map credentialsMap) { + public RtpContentMap modifiedCredentials(Map credentialsMap, final IceUdpTransportInfo.Setup setup) { final ImmutableMap.Builder contentMapBuilder = new ImmutableMap.Builder<>(); for (final Map.Entry content : contents.entrySet()) { final RtpDescription rtpDescription = content.getValue().description; IceUdpTransportInfo transportInfo = content.getValue().transport; final IceUdpTransportInfo.Credentials credentials = Objects.requireNonNull(credentialsMap.get(content.getKey())); - final IceUdpTransportInfo modifiedTransportInfo = transportInfo.modifyCredentials(credentials); + final IceUdpTransportInfo modifiedTransportInfo = transportInfo.modifyCredentials(credentials, setup); contentMapBuilder.put(content.getKey(), new DescriptionTransport(rtpDescription, modifiedTransportInfo)); } return new RtpContentMap(this.group, contentMapBuilder.build()); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java index 39031c4a9..e113146b1 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java @@ -156,7 +156,10 @@ public static SessionDescription of(final RtpContentMap contentMap) { final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint(); if (fingerprint != null) { mediaAttributes.put("fingerprint", fingerprint.getHash() + " " + fingerprint.getContent()); - mediaAttributes.put("setup", fingerprint.getSetup()); + final IceUdpTransportInfo.Setup setup = fingerprint.getSetup(); + if (setup != null) { + mediaAttributes.put("setup", setup.toString().toLowerCase(Locale.ROOT)); + } } final ImmutableList.Builder formatBuilder = new ImmutableList.Builder<>(); for (RtpDescription.PayloadType payloadType : description.getPayloadTypes()) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java index 1586557b7..9af1186fc 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java @@ -10,6 +10,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Hashtable; @@ -83,11 +84,19 @@ public IceUdpTransportInfo cloneWrapper() { return transportInfo; } - public IceUdpTransportInfo modifyCredentials(Credentials credentials) { + public IceUdpTransportInfo modifyCredentials(final Credentials credentials, final Setup setup) { final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo(); transportInfo.setAttribute("ufrag", credentials.ufrag); transportInfo.setAttribute("pwd", credentials.password); - transportInfo.setChildren(getChildren()); + for (final Element child : getChildren()) { + if (child.getName().equals("fingerprint") && Namespace.JINGLE_APPS_DTLS.equals(child.getNamespace())) { + final Fingerprint fingerprint = new Fingerprint(); + fingerprint.setAttributes(new Hashtable<>(child.getAttributes())); + fingerprint.setContent(child.getContent()); + fingerprint.setAttribute("setup", setup.toString().toLowerCase(Locale.ROOT)); + transportInfo.addChild(fingerprint); + } + } return transportInfo; } @@ -337,8 +346,31 @@ public String getHash() { return this.getAttribute("hash"); } - public String getSetup() { - return this.getAttribute("setup"); + public Setup getSetup() { + final String setup = this.getAttribute("setup"); + return setup == null ? null : Setup.of(setup); + } + } + + public enum Setup { + ACTPASS, PASSIVE, ACTIVE; + + public static Setup of(String setup) { + try { + return valueOf(setup.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException e) { + return null; + } + } + + public Setup flip() { + if (this == PASSIVE) { + return ACTIVE; + } + if (this == ACTIVE) { + return PASSIVE; + } + throw new IllegalStateException(this.name()+" can not be flipped"); } } } From 0698fa0d8c55507d0f52051d47603dd8ca3bef9e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 16 Nov 2021 11:21:11 +0100 Subject: [PATCH 019/394] store peer dtls setup for later use in ice restart --- .../xmpp/jingle/JingleRtpConnection.java | 34 ++++++++++++++----- .../xmpp/jingle/RtpContentMap.java | 6 +++- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index c0ddfd96a..ce726a2f0 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -150,6 +150,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private Set proposedMedia; private RtpContentMap initiatorRtpContentMap; private RtpContentMap responderRtpContentMap; + private IceUdpTransportInfo.Setup peerDtlsSetup; private final Stopwatch sessionDuration = Stopwatch.createUnstarted(); private final Queue stateHistory = new LinkedList<>(); private ScheduledFuture ringingTimeoutFuture; @@ -332,11 +333,18 @@ private boolean checkForIceRestart(final JinglePacket jinglePacket, final RtpCon } private IceUdpTransportInfo.Setup getPeerDtlsSetup() { - final IceUdpTransportInfo.Setup responderSetup = this.responderRtpContentMap.getDtlsSetup(); - if (responderSetup == null || responderSetup == IceUdpTransportInfo.Setup.ACTPASS) { - throw new IllegalStateException("Invalid DTLS setup value in responder content map"); + final IceUdpTransportInfo.Setup peerSetup = this.peerDtlsSetup; + if (peerSetup == null || peerSetup == IceUdpTransportInfo.Setup.ACTPASS) { + throw new IllegalStateException("Invalid peer setup"); } - return isInitiator() ? responderSetup : responderSetup.flip(); + return peerSetup; + } + + private void storePeerDtlsSetup(final IceUdpTransportInfo.Setup setup) { + if (setup == null || setup == IceUdpTransportInfo.Setup.ACTPASS) { + throw new IllegalArgumentException("Trying to store invalid peer dtls setup"); + } + this.peerDtlsSetup = setup; } private boolean applyIceRestart(final JinglePacket jinglePacket, final RtpContentMap restartContentMap, final boolean isOffer) throws ExecutionException, InterruptedException { @@ -352,11 +360,7 @@ private boolean applyIceRestart(final JinglePacket jinglePacket, final RtpConten webRTCWrapper.rollbackLocalDescription().get(); } webRTCWrapper.setRemoteDescription(sdp).get(); - if (isInitiator()) { - this.responderRtpContentMap = restartContentMap; - } else { - this.initiatorRtpContentMap = restartContentMap; - } + setRemoteContentMap(restartContentMap); if (isOffer) { webRTCWrapper.setIsReadyToReceiveIceCandidates(false); final SessionDescription localSessionDescription = setLocalSessionDescription(); @@ -364,6 +368,8 @@ private boolean applyIceRestart(final JinglePacket jinglePacket, final RtpConten //We need to respond OK before sending any candidates respondOk(jinglePacket); webRTCWrapper.setIsReadyToReceiveIceCandidates(true); + } else { + storePeerDtlsSetup(restartContentMap.getDtlsSetup()); } return true; } @@ -569,6 +575,7 @@ private void receiveSessionAccept(final JinglePacket jinglePacket, final RtpCont private void receiveSessionAccept(final RtpContentMap contentMap) { this.responderRtpContentMap = contentMap; + this.storePeerDtlsSetup(contentMap.getDtlsSetup()); final SessionDescription sessionDescription; try { sessionDescription = SessionDescription.of(contentMap); @@ -663,6 +670,7 @@ private void prepareSessionAccept(final org.webrtc.SessionDescription webRTCSess final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription); this.responderRtpContentMap = respondingRtpContentMap; + storePeerDtlsSetup(respondingRtpContentMap.getDtlsSetup().flip()); webRTCWrapper.setIsReadyToReceiveIceCandidates(true); final ListenableFuture outgoingContentMapFuture = prepareOutgoingContentMap(respondingRtpContentMap); Futures.addCallback(outgoingContentMapFuture, @@ -1520,6 +1528,14 @@ private void setLocalContentMap(final RtpContentMap rtpContentMap) { } } + private void setRemoteContentMap(final RtpContentMap rtpContentMap) { + if (isInitiator()) { + this.responderRtpContentMap = rtpContentMap; + } else { + this.initiatorRtpContentMap = rtpContentMap; + } + } + private SessionDescription setLocalSessionDescription() throws ExecutionException, InterruptedException { final org.webrtc.SessionDescription sessionDescription = this.webRTCWrapper.setLocalDescription().get(); return SessionDescription.parse(sessionDescription.description); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java index 5366460ec..091bf7c10 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -155,7 +155,11 @@ public IceUdpTransportInfo.Setup getDtlsSetup() { contents.values(), dt->dt.transport.getFingerprint().getSetup() )); - return setups.size() == 1 ? Iterables.getFirst(setups, null) : null; + final IceUdpTransportInfo.Setup setup = Iterables.getFirst(setups, null); + if (setups.size() == 1 && setup != null) { + return setup; + } + throw new IllegalStateException("Content map doesn't have distinct DTLS setup"); } public boolean emptyCandidates() { From 297a843b9c6b245f4e3bb07a23e487a4f6d5d9f8 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 16 Nov 2021 13:17:10 +0100 Subject: [PATCH 020/394] use implicit rollback (needed to be enabled on libwebrtc) --- .../xmpp/jingle/JingleRtpConnection.java | 2 -- .../xmpp/jingle/WebRTCWrapper.java | 26 +------------------ 2 files changed, 1 insertion(+), 27 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index ce726a2f0..5c474a754 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -356,8 +356,6 @@ private boolean applyIceRestart(final JinglePacket jinglePacket, final RtpConten //We ignore the offer and respond with tie-break. This will clause the responder not to apply the content map return false; } - //rollback our own local description. should happen automatically but doesn't - webRTCWrapper.rollbackLocalDescription().get(); } webRTCWrapper.setRemoteDescription(sdp).get(); setRemoteContentMap(restartContentMap); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 401121b86..13b695b0e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -86,7 +86,6 @@ public class WebRTCWrapper { private final EventCallback eventCallback; private final AtomicBoolean readyToReceivedIceCandidates = new AtomicBoolean(false); - private final AtomicBoolean ignoreOnRenegotiationNeeded = new AtomicBoolean(false); private final Queue iceCandidates = new LinkedList<>(); private final AppRTCAudioManager.AudioManagerEvents audioManagerEvents = new AppRTCAudioManager.AudioManagerEvents() { @Override @@ -163,10 +162,6 @@ public void onDataChannel(DataChannel dataChannel) { @Override public void onRenegotiationNeeded() { - if (ignoreOnRenegotiationNeeded.get()) { - Log.d(EXTENDED_LOGGING_TAG, "ignoring onRenegotiationNeeded()"); - return; - } Log.d(EXTENDED_LOGGING_TAG, "onRenegotiationNeeded()"); final PeerConnection.PeerConnectionState currentState = peerConnection == null ? null : peerConnection.connectionState(); if (currentState != null && currentState != PeerConnection.PeerConnectionState.NEW) { @@ -277,6 +272,7 @@ synchronized void initializePeerConnection(final Set media, final List rollbackLocalDescription() { - final SettableFuture future = SettableFuture.create(); - final SessionDescription rollback = new SessionDescription(SessionDescription.Type.ROLLBACK, ""); - ignoreOnRenegotiationNeeded.set(true); - requirePeerConnection().setLocalDescription(new SetSdpObserver() { - @Override - public void onSetSuccess() { - future.set(null); - ignoreOnRenegotiationNeeded.set(false); - } - - @Override - public void onSetFailure(final String message) { - future.setException(new FailureToSetDescriptionException(message)); - } - }, rollback); - return future; - } - - private static void logDescription(final SessionDescription sessionDescription) { for (final String line : sessionDescription.description.split(eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) { Log.d(EXTENDED_LOGGING_TAG, line); From abb671616cf0b085d9dbe2fc3b1d3eb446decab2 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 16 Nov 2021 15:17:12 +0100 Subject: [PATCH 021/394] synchronize setDescription calls --- .../conversations/xmpp/XmppConnection.java | 1 - .../xmpp/jingle/JingleRtpConnection.java | 30 +++++++++-------- .../xmpp/jingle/WebRTCWrapper.java | 33 ++++++++++--------- 3 files changed, 34 insertions(+), 30 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index abe5d161f..c3a3b1532 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -54,7 +54,6 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.DomainHostnameVerifier; import eu.siacs.conversations.crypto.XmppDomainVerifier; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.sasl.Anonymous; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 5c474a754..66cf5c23b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -311,7 +311,7 @@ private boolean checkForIceRestart(final JinglePacket jinglePacket, final RtpCon restartContentMap = existing.modifiedCredentials(newCredentials, IceUdpTransportInfo.Setup.ACTPASS); } else { final IceUdpTransportInfo.Setup setup = getPeerDtlsSetup(); - Log.d(Config.LOGTAG, "received confirmation of ICE restart" + newCredentials.values()+" peer_setup="+setup); + Log.d(Config.LOGTAG, "received confirmation of ICE restart" + newCredentials.values() + " peer_setup=" + setup); // DTLS setup attribute needs to be rewritten to reflect current peer state // https://groups.google.com/g/discuss-webrtc/c/DfpIMwvUfeM restartContentMap = existing.modifiedCredentials(newCredentials, setup); @@ -319,12 +319,18 @@ private boolean checkForIceRestart(final JinglePacket jinglePacket, final RtpCon if (applyIceRestart(jinglePacket, restartContentMap, isOffer)) { return isOffer; } else { + Log.d(Config.LOGTAG,"ignored ice restart. offer="+isOffer); respondWithTieBreak(jinglePacket); return true; } } catch (final Exception exception) { respondOk(jinglePacket); final Throwable rootCause = Throwables.getRootCause(exception); + if (rootCause instanceof WebRTCWrapper.PeerConnectionNotInitialized) { + Log.d(Config.LOGTAG,"ignoring PeerConnectionNotInitialized"); + //TODO don’t respond OK but respond with out-of-order + return true; + } Log.d(Config.LOGTAG, "failure to apply ICE restart", rootCause); webRTCWrapper.close(); sendSessionTerminate(Reason.ofThrowable(rootCause), rootCause.getMessage()); @@ -1466,21 +1472,18 @@ public void onConnectionChange(final PeerConnection.PeerConnectionState newState } final boolean neverConnected = !this.stateHistory.contains(PeerConnection.PeerConnectionState.CONNECTED); - final boolean failedOrDisconnected = Arrays.asList( - PeerConnection.PeerConnectionState.FAILED, - PeerConnection.PeerConnectionState.DISCONNECTED - ).contains(newState); - - if (neverConnected && failedOrDisconnected) { - if (isTerminated()) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": not sending session-terminate after connectivity error because session is already in state " + this.state); + if (newState == PeerConnection.PeerConnectionState.FAILED) { + if (neverConnected) { + if (isTerminated()) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": not sending session-terminate after connectivity error because session is already in state " + this.state); + return; + } + webRTCWrapper.execute(this::closeWebRTCSessionAfterFailedConnection); return; + } else { + webRTCWrapper.restartIce(); } - webRTCWrapper.execute(this::closeWebRTCSessionAfterFailedConnection); - } else if (newState == PeerConnection.PeerConnectionState.FAILED) { - Log.d(Config.LOGTAG, "attempting to restart ICE"); - webRTCWrapper.restartIce(); } updateEndUserState(); } @@ -1491,6 +1494,7 @@ public void onRenegotiationNeeded() { } private void initiateIceRestart() { + this.stateHistory.clear(); this.webRTCWrapper.setIsReadyToReceiveIceCandidates(false); final SessionDescription sessionDescription; try { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 13b695b0e..0712fa900 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -431,7 +431,7 @@ void setVideoEnabled(final boolean enabled) { videoTrack.setEnabled(enabled); } - ListenableFuture setLocalDescription() { + synchronized ListenableFuture setLocalDescription() { return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { final SettableFuture future = SettableFuture.create(); peerConnection.setLocalDescription(new SetSdpObserver() { @@ -458,7 +458,7 @@ private static void logDescription(final SessionDescription sessionDescription) } } - ListenableFuture setRemoteDescription(final SessionDescription sessionDescription) { + synchronized ListenableFuture setRemoteDescription(final SessionDescription sessionDescription) { Log.d(EXTENDED_LOGGING_TAG, "setting remote description:"); logDescription(sessionDescription); return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { @@ -482,12 +482,20 @@ public void onSetFailure(final String message) { private ListenableFuture getPeerConnectionFuture() { final PeerConnection peerConnection = this.peerConnection; if (peerConnection == null) { - return Futures.immediateFailedFuture(new IllegalStateException("initialize PeerConnection first")); + return Futures.immediateFailedFuture(new PeerConnectionNotInitialized()); } else { return Futures.immediateFuture(peerConnection); } } + private PeerConnection requirePeerConnection() { + final PeerConnection peerConnection = this.peerConnection; + if (peerConnection == null) { + throw new PeerConnectionNotInitialized(); + } + return peerConnection; + } + void addIceCandidate(IceCandidate iceCandidate) { requirePeerConnection().addIceCandidate(iceCandidate); } @@ -512,10 +520,15 @@ private Optional getVideoCapturer() { } } - public PeerConnection.PeerConnectionState getState() { + PeerConnection.PeerConnectionState getState() { return requirePeerConnection().connectionState(); } + public PeerConnection.SignalingState getSignalingState() { + return requirePeerConnection().signalingState(); + } + + EglBase.Context getEglBaseContext() { return this.eglBase.getEglBaseContext(); } @@ -528,14 +541,6 @@ Optional getRemoteVideoTrack() { return Optional.fromNullable(this.remoteVideoTrack); } - private PeerConnection requirePeerConnection() { - final PeerConnection peerConnection = this.peerConnection; - if (peerConnection == null) { - throw new PeerConnectionNotInitialized(); - } - return peerConnection; - } - private Context requireContext() { final Context context = this.context; if (context == null) { @@ -552,10 +557,6 @@ void execute(final Runnable command) { executorService.execute(command); } - public PeerConnection.SignalingState getSignalingState() { - return requirePeerConnection().signalingState(); - } - public interface EventCallback { void onIceCandidate(IceCandidate iceCandidate); From 0a18c8613f83d2a6861a4ae4fef4e534f3f122f9 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 16 Nov 2021 17:08:34 +0100 Subject: [PATCH 022/394] assume credentials are the same for all contents when restarting ICE --- .../conversations/ui/RtpSessionActivity.java | 1 + .../xmpp/jingle/JingleRtpConnection.java | 23 +++++++++++-------- .../xmpp/jingle/RtpContentMap.java | 17 ++++++++++---- .../jingle/stanzas/IceUdpTransportInfo.java | 4 ++-- 4 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index d0bdbb788..39ba7429f 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -1003,6 +1003,7 @@ private void updateVideoViews(final RtpEndUserState state) { RendererCommon.ScalingType.SCALE_ASPECT_FILL, RendererCommon.ScalingType.SCALE_ASPECT_FIT ); + //TODO this should probably only be 'connected' if (STATES_CONSIDERED_CONNECTED.contains(state)) { binding.appBarLayout.setVisibility(View.GONE); getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 66cf5c23b..26c068c08 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -141,7 +141,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private final WebRTCWrapper webRTCWrapper = new WebRTCWrapper(this); - //TODO convert to Queue>? private final Queue> pendingIceCandidates = new LinkedList<>(); private final OmemoVerification omemoVerification = new OmemoVerification(); private final Message message; @@ -295,9 +294,13 @@ private void receiveTransportInfo(final JinglePacket jinglePacket, final RtpCont private boolean checkForIceRestart(final JinglePacket jinglePacket, final RtpContentMap rtpContentMap) { final RtpContentMap existing = getRemoteContentMap(); - final Map existingCredentials = existing.getCredentials(); - final Map newCredentials = rtpContentMap.getCredentials(); - if (!existingCredentials.keySet().equals(newCredentials.keySet())) { + final IceUdpTransportInfo.Credentials existingCredentials; + final IceUdpTransportInfo.Credentials newCredentials; + try { + existingCredentials = existing.getCredentials(); + newCredentials = rtpContentMap.getCredentials(); + } catch (final IllegalStateException e) { + Log.d(Config.LOGTAG, "unable to gather credentials for comparison", e); return false; } if (existingCredentials.equals(newCredentials)) { @@ -307,11 +310,11 @@ private boolean checkForIceRestart(final JinglePacket jinglePacket, final RtpCon final RtpContentMap restartContentMap; try { if (isOffer) { - Log.d(Config.LOGTAG, "received offer to restart ICE " + newCredentials.values()); + Log.d(Config.LOGTAG, "received offer to restart ICE " + newCredentials); restartContentMap = existing.modifiedCredentials(newCredentials, IceUdpTransportInfo.Setup.ACTPASS); } else { final IceUdpTransportInfo.Setup setup = getPeerDtlsSetup(); - Log.d(Config.LOGTAG, "received confirmation of ICE restart" + newCredentials.values() + " peer_setup=" + setup); + Log.d(Config.LOGTAG, "received confirmation of ICE restart" + newCredentials + " peer_setup=" + setup); // DTLS setup attribute needs to be rewritten to reflect current peer state // https://groups.google.com/g/discuss-webrtc/c/DfpIMwvUfeM restartContentMap = existing.modifiedCredentials(newCredentials, setup); @@ -319,7 +322,7 @@ private boolean checkForIceRestart(final JinglePacket jinglePacket, final RtpCon if (applyIceRestart(jinglePacket, restartContentMap, isOffer)) { return isOffer; } else { - Log.d(Config.LOGTAG,"ignored ice restart. offer="+isOffer); + Log.d(Config.LOGTAG, "ignoring ICE restart. sending tie-break"); respondWithTieBreak(jinglePacket); return true; } @@ -327,7 +330,7 @@ private boolean checkForIceRestart(final JinglePacket jinglePacket, final RtpCon respondOk(jinglePacket); final Throwable rootCause = Throwables.getRootCause(exception); if (rootCause instanceof WebRTCWrapper.PeerConnectionNotInitialized) { - Log.d(Config.LOGTAG,"ignoring PeerConnectionNotInitialized"); + Log.d(Config.LOGTAG, "ignoring PeerConnectionNotInitialized"); //TODO don’t respond OK but respond with out-of-order return true; } @@ -1451,8 +1454,8 @@ void transitionOrThrow(final State target) { @Override public void onIceCandidate(final IceCandidate iceCandidate) { final RtpContentMap rtpContentMap = isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap; - final Collection currentUfrags = Collections2.transform(rtpContentMap.getCredentials().values(), c -> c.ufrag); - final IceUdpTransportInfo.Candidate candidate = IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp, currentUfrags); + final String ufrag = rtpContentMap.getCredentials().ufrag; + final IceUdpTransportInfo.Candidate candidate = IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp, ufrag); if (candidate == null) { Log.d(Config.LOGTAG, "ignoring (not sending) candidate: " + iceCandidate.toString()); return; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java index 091bf7c10..ea351cb1b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -146,14 +146,22 @@ RtpContentMap transportInfo() { ); } - public Map getCredentials() { - return Maps.transformValues(contents, dt -> dt.transport.getCredentials()); + public IceUdpTransportInfo.Credentials getCredentials() { + final Set allCredentials = ImmutableSet.copyOf(Collections2.transform( + contents.values(), + dt -> dt.transport.getCredentials() + )); + final IceUdpTransportInfo.Credentials credentials = Iterables.getFirst(allCredentials, null); + if (allCredentials.size() == 1 && credentials != null) { + return credentials; + } + throw new IllegalStateException("Content map does not have distinct credentials"); } public IceUdpTransportInfo.Setup getDtlsSetup() { final Set setups = ImmutableSet.copyOf(Collections2.transform( contents.values(), - dt->dt.transport.getFingerprint().getSetup() + dt -> dt.transport.getFingerprint().getSetup() )); final IceUdpTransportInfo.Setup setup = Iterables.getFirst(setups, null); if (setups.size() == 1 && setup != null) { @@ -170,12 +178,11 @@ public boolean emptyCandidates() { return count == 0; } - public RtpContentMap modifiedCredentials(Map credentialsMap, final IceUdpTransportInfo.Setup setup) { + public RtpContentMap modifiedCredentials(IceUdpTransportInfo.Credentials credentials, final IceUdpTransportInfo.Setup setup) { final ImmutableMap.Builder contentMapBuilder = new ImmutableMap.Builder<>(); for (final Map.Entry content : contents.entrySet()) { final RtpDescription rtpDescription = content.getValue().description; IceUdpTransportInfo transportInfo = content.getValue().transport; - final IceUdpTransportInfo.Credentials credentials = Objects.requireNonNull(credentialsMap.get(content.getKey())); final IceUdpTransportInfo modifiedTransportInfo = transportInfo.modifyCredentials(credentials, setup); contentMapBuilder.put(content.getKey(), new DescriptionTransport(rtpDescription, modifiedTransportInfo)); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java index 9af1186fc..45260cafb 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java @@ -146,7 +146,7 @@ public static Candidate upgrade(final Element element) { } // https://tools.ietf.org/html/draft-ietf-mmusic-ice-sip-sdp-39#section-5.1 - public static Candidate fromSdpAttribute(final String attribute, Collection currentUfrags) { + public static Candidate fromSdpAttribute(final String attribute, String currentUfrag) { final String[] pair = attribute.split(":", 2); if (pair.length == 2 && "candidate".equals(pair[0])) { final String[] segments = pair[1].split(" "); @@ -163,7 +163,7 @@ public static Candidate fromSdpAttribute(final String attribute, Collection Date: Tue, 16 Nov 2021 22:01:48 +0100 Subject: [PATCH 023/394] video calls: leave full screen mode during reconnect --- .../conversations/ui/RtpSessionActivity.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 39ba7429f..65beae35d 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -103,6 +103,11 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe RtpEndUserState.CONNECTED, RtpEndUserState.RECONNECTING ); + private static final List STATES_SHOWING_PIP_PLACEHOLDER = Arrays.asList( + RtpEndUserState.ACCEPTING_CALL, + RtpEndUserState.CONNECTING, + RtpEndUserState.RECONNECTING + ); private static final String PROXIMITY_WAKE_LOCK_TAG = "conversations:in-rtp-session"; private static final int REQUEST_ACCEPT_CALL = 0x1111; private WeakReference rtpConnectionReference; @@ -640,8 +645,8 @@ private void ensureSurfaceViewRendererIsSetup(final SurfaceViewRenderer surfaceV surfaceViewRenderer.setVisibility(View.VISIBLE); try { surfaceViewRenderer.init(requireRtpConnection().getEglBaseContext(), null); - } catch (IllegalStateException e) { - Log.d(Config.LOGTAG, "SurfaceViewRenderer was already initialized"); + } catch (final IllegalStateException e) { + //Log.d(Config.LOGTAG, "SurfaceViewRenderer was already initialized"); } surfaceViewRenderer.setEnableHardwareScaler(true); } @@ -975,7 +980,7 @@ private void updateVideoViews(final RtpEndUserState state) { getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); return; } - if (isPictureInPicture() && (state == RtpEndUserState.CONNECTING || state == RtpEndUserState.ACCEPTING_CALL)) { + if (isPictureInPicture() && STATES_SHOWING_PIP_PLACEHOLDER.contains(state)) { binding.localVideo.setVisibility(View.GONE); binding.remoteVideoWrapper.setVisibility(View.GONE); binding.appBarLayout.setVisibility(View.GONE); @@ -1003,12 +1008,12 @@ private void updateVideoViews(final RtpEndUserState state) { RendererCommon.ScalingType.SCALE_ASPECT_FILL, RendererCommon.ScalingType.SCALE_ASPECT_FIT ); - //TODO this should probably only be 'connected' - if (STATES_CONSIDERED_CONNECTED.contains(state)) { + if (state == RtpEndUserState.CONNECTED) { binding.appBarLayout.setVisibility(View.GONE); getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); binding.remoteVideoWrapper.setVisibility(View.VISIBLE); } else { + binding.appBarLayout.setVisibility(View.VISIBLE); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); binding.remoteVideoWrapper.setVisibility(View.GONE); } From 61fb38cd8442086596b347b45ef2e4b69f647af2 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 17 Nov 2021 10:49:16 +0100 Subject: [PATCH 024/394] clean up some error handling error ICE restarts --- .../eu/siacs/conversations/xml/Namespace.java | 1 + .../xmpp/jingle/JingleConnectionManager.java | 2 +- .../xmpp/jingle/JingleRtpConnection.java | 101 +++++++++++------- .../xmpp/jingle/RtpContentMap.java | 7 ++ 4 files changed, 71 insertions(+), 40 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index b0c4fe85c..09bbda4cd 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -28,6 +28,7 @@ public final class Namespace { public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0"; public static final String AVATAR_CONVERSION = "urn:xmpp:pep-vcard-conversion:0"; public static final String JINGLE = "urn:xmpp:jingle:1"; + public static final String JINGLE_ERRORS = "urn:xmpp:jingle:errors:1"; public static final String JINGLE_MESSAGE = "urn:xmpp:jingle-message:0"; public static final String JINGLE_ENCRYPTED_TRANSPORT = "urn:xmpp:jingle:jet:0"; public static final String JINGLE_ENCRYPTED_TRANSPORT_OMEMO = "urn:xmpp:jingle:jet-omemo:0"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 6b94f1f4d..cbf4b85fd 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -206,7 +206,7 @@ void respondWithJingleError(final Account account, final IqPacket original, Stri final Element error = response.addChild("error"); error.setAttribute("type", conditionType); error.addChild(condition, "urn:ietf:params:xml:ns:xmpp-stanzas"); - error.addChild(jingleCondition, "urn:xmpp:jingle:errors:1"); + error.addChild(jingleCondition, Namespace.JINGLE_ERRORS); account.getXmppConnection().sendIqPacket(response, null); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 26c068c08..1295bf908 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -306,6 +306,9 @@ private boolean checkForIceRestart(final JinglePacket jinglePacket, final RtpCon if (existingCredentials.equals(newCredentials)) { return false; } + //TODO an alternative approach is to check if we already got an iq result to our ICE-restart + // and if that's the case we are seeing an answer. + // This might be more spec compliant but also more error prone potentially final boolean isOffer = rtpContentMap.emptyCandidates(); final RtpContentMap restartContentMap; try { @@ -330,8 +333,8 @@ private boolean checkForIceRestart(final JinglePacket jinglePacket, final RtpCon respondOk(jinglePacket); final Throwable rootCause = Throwables.getRootCause(exception); if (rootCause instanceof WebRTCWrapper.PeerConnectionNotInitialized) { - Log.d(Config.LOGTAG, "ignoring PeerConnectionNotInitialized"); - //TODO don’t respond OK but respond with out-of-order + //If this happens a termination is already in progress + Log.d(Config.LOGTAG, "ignoring PeerConnectionNotInitialized on ICE restart"); return true; } Log.d(Config.LOGTAG, "failure to apply ICE restart", rootCause); @@ -484,8 +487,7 @@ public void onFailure(@NonNull final Throwable throwable) { private void receiveSessionInitiate(final JinglePacket jinglePacket, final RtpContentMap contentMap) { try { contentMap.requireContentDescriptions(); - //TODO require actpass - contentMap.requireDTLSFingerprint(); + contentMap.requireDTLSFingerprint(true); } catch (final RuntimeException e) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", Throwables.getRootCause(e)); respondOk(jinglePacket); @@ -1072,36 +1074,48 @@ private void send(final JinglePacket jinglePacket) { private synchronized void handleIqResponse(final Account account, final IqPacket response) { if (response.getType() == IqPacket.TYPE.ERROR) { - final String errorCondition = response.getErrorCondition(); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ-error from " + response.getFrom() + " in RTP session. " + errorCondition); - if (isTerminated()) { - Log.i(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring error because session was already terminated"); - return; - } - this.webRTCWrapper.close(); - final State target; - if (Arrays.asList( - "service-unavailable", - "recipient-unavailable", - "remote-server-not-found", - "remote-server-timeout" - ).contains(errorCondition)) { - target = State.TERMINATED_CONNECTIVITY_ERROR; - } else { - target = State.TERMINATED_APPLICATION_FAILURE; - } - transitionOrThrow(target); - this.finish(); - } else if (response.getType() == IqPacket.TYPE.TIMEOUT) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ timeout in RTP session with " + id.with + ". terminating with connectivity error"); - if (isTerminated()) { - Log.i(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring error because session was already terminated"); - return; - } - this.webRTCWrapper.close(); - transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR); - this.finish(); + handleIqErrorResponse(response); + return; + } + if (response.getType() == IqPacket.TYPE.TIMEOUT) { + handleIqTimeoutResponse(response); + } + } + + private void handleIqErrorResponse(final IqPacket response) { + Preconditions.checkArgument(response.getType() == IqPacket.TYPE.ERROR); + final String errorCondition = response.getErrorCondition(); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ-error from " + response.getFrom() + " in RTP session. " + errorCondition); + if (isTerminated()) { + Log.i(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring error because session was already terminated"); + return; + } + this.webRTCWrapper.close(); + final State target; + if (Arrays.asList( + "service-unavailable", + "recipient-unavailable", + "remote-server-not-found", + "remote-server-timeout" + ).contains(errorCondition)) { + target = State.TERMINATED_CONNECTIVITY_ERROR; + } else { + target = State.TERMINATED_APPLICATION_FAILURE; + } + transitionOrThrow(target); + this.finish(); + } + + private void handleIqTimeoutResponse(final IqPacket response) { + Preconditions.checkArgument(response.getType() == IqPacket.TYPE.ERROR); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ timeout in RTP session with " + id.with + ". terminating with connectivity error"); + if (isTerminated()) { + Log.i(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring error because session was already terminated"); + return; } + this.webRTCWrapper.close(); + transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR); + this.finish(); } private void terminateWithOutOfOrder(final JinglePacket jinglePacket) { @@ -1503,8 +1517,9 @@ private void initiateIceRestart() { try { sessionDescription = setLocalSessionDescription(); } catch (final Exception e) { - Log.d(Config.LOGTAG, "failed to renegotiate", e); - //TODO send some sort of failure (comparable to when initiating) + final Throwable cause = Throwables.getRootCause(e); + Log.d(Config.LOGTAG, "failed to renegotiate", cause); + sendSessionTerminate(Reason.FAILED_APPLICATION, cause.getMessage()); return; } final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); @@ -1517,10 +1532,18 @@ private void initiateIceRestart() { Log.d(Config.LOGTAG, "received success to our ice restart"); setLocalContentMap(rtpContentMap); webRTCWrapper.setIsReadyToReceiveIceCandidates(true); - } else { - Log.d(Config.LOGTAG, "received failure to our ice restart"); - //TODO ignore tie break (maybe rollback?) - //TODO handle other errors + return; + } + if (response.getType() == IqPacket.TYPE.ERROR) { + final Element error = response.findChild("error"); + if (error != null && error.hasChild("tie-break", Namespace.JINGLE_ERRORS)) { + Log.d(Config.LOGTAG, "received tie-break as result of ice restart"); + return; + } + handleIqErrorResponse(response); + } + if (response.getType() == IqPacket.TYPE.TIMEOUT) { + handleIqTimeoutResponse(response); } }); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java index ea351cb1b..21684a165 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -96,6 +96,10 @@ void requireContentDescriptions() { } void requireDTLSFingerprint() { + requireDTLSFingerprint(false); + } + + void requireDTLSFingerprint(final boolean requireActPass) { if (this.contents.size() == 0) { throw new IllegalStateException("No contents available"); } @@ -109,6 +113,9 @@ void requireDTLSFingerprint() { if (setup == null) { throw new SecurityException(String.format("Use of DTLS-SRTP (XEP-0320) is required for content %s but missing setup attribute", entry.getKey())); } + if (requireActPass && setup != IceUdpTransportInfo.Setup.ACTPASS) { + throw new SecurityException("Initiator needs to offer ACTPASS as setup for DTLS-SRTP (XEP-0320)"); + } } } From a508a81553604b7a6af6f650146f2ccd736b4066 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 17 Nov 2021 11:33:15 +0100 Subject: [PATCH 025/394] externalize rtc config generation into seperate method --- .../xmpp/jingle/JingleRtpConnection.java | 1 + .../xmpp/jingle/WebRTCWrapper.java | 21 +++++++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 1295bf908..e4661a3aa 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -1511,6 +1511,7 @@ public void onRenegotiationNeeded() { } private void initiateIceRestart() { + //TODO discover new TURN/STUN credentials this.stateHistory.clear(); this.webRTCWrapper.setIsReadyToReceiveIceCandidates(false); final SessionDescription sessionDescription; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 0712fa900..6722f9f2c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -267,12 +267,7 @@ synchronized void initializePeerConnection(final Set media, final List media, final List iceServers) { + final PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers); + rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED; //XEP-0176 doesn't support tcp + rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY; + rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN; + rtcConfig.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.NEGOTIATE; + rtcConfig.enableImplicitRollback = true; + return rtcConfig; + } + + void reconfigurePeerConnection(final List iceServers) { + requirePeerConnection().setConfiguration(buildConfiguration(iceServers)); + } + void restartIce() { executorService.execute(() -> requirePeerConnection().restartIce()); } From 5d526a77e3899714750d5f56536364a5ed9b5149 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 18 Nov 2021 11:24:10 +0100 Subject: [PATCH 026/394] include uncertainty into shared geo uri --- .../ui/ConversationFragment.java | 12 +- .../ui/ShareLocationActivity.java | 421 +++++++++--------- 2 files changed, 221 insertions(+), 212 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 0077684a5..771c99e9c 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -856,9 +856,15 @@ private void handlePositiveActivityResult(int requestCode, final Intent data) { toggleInputMethod(); break; case ATTACHMENT_CHOICE_LOCATION: - double latitude = data.getDoubleExtra("latitude", 0); - double longitude = data.getDoubleExtra("longitude", 0); - Uri geo = Uri.parse("geo:" + latitude + "," + longitude); + final double latitude = data.getDoubleExtra("latitude", 0); + final double longitude = data.getDoubleExtra("longitude", 0); + final int accuracy = data.getIntExtra("accuracy", 0); + final Uri geo; + if (accuracy > 0) { + geo = Uri.parse(String.format("geo:%s,%s;u=%s", latitude, longitude, accuracy)); + } else { + geo = Uri.parse(String.format("geo:%s,%s", latitude, longitude)); + } mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), geo, Attachment.Type.LOCATION)); toggleInputMethod(); break; diff --git a/src/main/java/eu/siacs/conversations/ui/ShareLocationActivity.java b/src/main/java/eu/siacs/conversations/ui/ShareLocationActivity.java index 641a01e5c..7e53fe897 100644 --- a/src/main/java/eu/siacs/conversations/ui/ShareLocationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ShareLocationActivity.java @@ -13,10 +13,13 @@ import androidx.databinding.DataBindingUtil; import com.google.android.material.snackbar.Snackbar; +import com.google.common.math.DoubleMath; import org.osmdroid.api.IGeoPoint; import org.osmdroid.util.GeoPoint; +import java.math.RoundingMode; + import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivityShareLocationBinding; @@ -28,213 +31,213 @@ public class ShareLocationActivity extends LocationActivity implements LocationListener { - private Snackbar snackBar; - private ActivityShareLocationBinding binding; - private boolean marker_fixed_to_loc = false; - private static final String KEY_FIXED_TO_LOC = "fixed_to_loc"; - private Boolean noAskAgain = false; - - @Override - protected void onSaveInstanceState(@NonNull final Bundle outState) { - super.onSaveInstanceState(outState); - - outState.putBoolean(KEY_FIXED_TO_LOC, marker_fixed_to_loc); - } - - @Override - protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - - if (savedInstanceState.containsKey(KEY_FIXED_TO_LOC)) { - this.marker_fixed_to_loc = savedInstanceState.getBoolean(KEY_FIXED_TO_LOC); - } - } - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - this.binding = DataBindingUtil.setContentView(this,R.layout.activity_share_location); - setSupportActionBar(binding.toolbar); - configureActionBar(getSupportActionBar()); - setupMapView(binding.map, LocationProvider.getGeoPoint(this)); - - this.binding.cancelButton.setOnClickListener(view -> { - setResult(RESULT_CANCELED); - finish(); - }); - - this.snackBar = Snackbar.make(this.binding.snackbarCoordinator, R.string.location_disabled, Snackbar.LENGTH_INDEFINITE); - this.snackBar.setAction(R.string.enable, view -> { - if (isLocationEnabledAndAllowed()) { - updateUi(); - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !hasLocationPermissions()) { - requestPermissions(REQUEST_CODE_SNACKBAR_PRESSED); - } else if (!isLocationEnabled()) { - startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)); - } - }); - ThemeHelper.fix(this.snackBar); - - this.binding.shareButton.setOnClickListener(view -> { - final Intent result = new Intent(); - - if (marker_fixed_to_loc && myLoc != null) { - result.putExtra("latitude", myLoc.getLatitude()); - result.putExtra("longitude", myLoc.getLongitude()); - result.putExtra("altitude", myLoc.getAltitude()); - result.putExtra("accuracy", (int) myLoc.getAccuracy()); - } else { - final IGeoPoint markerPoint = this.binding.map.getMapCenter(); - result.putExtra("latitude", markerPoint.getLatitude()); - result.putExtra("longitude", markerPoint.getLongitude()); - } - - setResult(RESULT_OK, result); - finish(); - }); - - this.marker_fixed_to_loc = isLocationEnabledAndAllowed(); - - this.binding.fab.setOnClickListener(view -> { - if (!marker_fixed_to_loc) { - if (!isLocationEnabled()) { - startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)); - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - requestPermissions(REQUEST_CODE_FAB_PRESSED); - } - } - toggleFixedLocation(); - }); - } - - @Override - public void onRequestPermissionsResult(final int requestCode, - @NonNull final String[] permissions, - @NonNull final int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - - if (grantResults.length > 0 && - grantResults[0] != PackageManager.PERMISSION_GRANTED && - Build.VERSION.SDK_INT >= 23 && - permissions.length > 0 && - ( - Manifest.permission.LOCATION_HARDWARE.equals(permissions[0]) || - Manifest.permission.ACCESS_FINE_LOCATION.equals(permissions[0]) || - Manifest.permission.ACCESS_COARSE_LOCATION.equals(permissions[0]) - ) && - !shouldShowRequestPermissionRationale(permissions[0])) { - noAskAgain = true; - } - - if (!noAskAgain && requestCode == REQUEST_CODE_SNACKBAR_PRESSED && !isLocationEnabled() && hasLocationPermissions()) { - startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)); - } - updateUi(); - } - - @Override - protected void gotoLoc(final boolean setZoomLevel) { - if (this.myLoc != null && mapController != null) { - if (setZoomLevel) { - mapController.setZoom(Config.Map.FINAL_ZOOM_LEVEL); - } - mapController.animateTo(new GeoPoint(this.myLoc)); - } - } - - @Override - protected void setMyLoc(final Location location) { - this.myLoc = location; - } - - @Override - protected void onPause() { - super.onPause(); - } - - @Override - protected void updateLocationMarkers() { - super.updateLocationMarkers(); - if (this.myLoc != null) { - this.binding.map.getOverlays().add(new MyLocation(this, null, this.myLoc)); - if (this.marker_fixed_to_loc) { - this.binding.map.getOverlays().add(new Marker(marker_icon, new GeoPoint(this.myLoc))); - } else { - this.binding.map.getOverlays().add(new Marker(marker_icon)); - } - } else { - this.binding.map.getOverlays().add(new Marker(marker_icon)); - } - } - - @Override - public void onLocationChanged(final Location location) { - if (this.myLoc == null) { - this.marker_fixed_to_loc = true; - } - updateUi(); - if (LocationHelper.isBetterLocation(location, this.myLoc)) { - final Location oldLoc = this.myLoc; - this.myLoc = location; - - // Don't jump back to the users location if they're not moving (more or less). - if (oldLoc == null || (this.marker_fixed_to_loc && this.myLoc.distanceTo(oldLoc) > 1)) { - gotoLoc(); - } - - updateLocationMarkers(); - } - } - - @Override - public void onStatusChanged(final String provider, final int status, final Bundle extras) { - - } - - @Override - public void onProviderEnabled(final String provider) { - - } - - @Override - public void onProviderDisabled(final String provider) { - - } - - private boolean isLocationEnabledAndAllowed() { - return this.hasLocationFeature && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || this.hasLocationPermissions()) && this.isLocationEnabled(); - } - - private void toggleFixedLocation() { - this.marker_fixed_to_loc = isLocationEnabledAndAllowed() && !this.marker_fixed_to_loc; - if (this.marker_fixed_to_loc) { - gotoLoc(false); - } - updateLocationMarkers(); - updateUi(); - } - - @Override - protected void updateUi() { - if (!hasLocationFeature || noAskAgain || isLocationEnabledAndAllowed()) { - this.snackBar.dismiss(); - } else { - this.snackBar.show(); - } - - if (isLocationEnabledAndAllowed()) { - this.binding.fab.setVisibility(View.VISIBLE); - runOnUiThread(() -> { - this.binding.fab.setImageResource(marker_fixed_to_loc ? R.drawable.ic_gps_fixed_white_24dp : - R.drawable.ic_gps_not_fixed_white_24dp); - this.binding.fab.setContentDescription(getResources().getString( - marker_fixed_to_loc ? R.string.action_unfix_from_location : R.string.action_fix_to_location - )); - this.binding.fab.invalidate(); - }); - } else { - this.binding.fab.setVisibility(View.GONE); - } - } + private Snackbar snackBar; + private ActivityShareLocationBinding binding; + private boolean marker_fixed_to_loc = false; + private static final String KEY_FIXED_TO_LOC = "fixed_to_loc"; + private Boolean noAskAgain = false; + + @Override + protected void onSaveInstanceState(@NonNull final Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putBoolean(KEY_FIXED_TO_LOC, marker_fixed_to_loc); + } + + @Override + protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + + if (savedInstanceState.containsKey(KEY_FIXED_TO_LOC)) { + this.marker_fixed_to_loc = savedInstanceState.getBoolean(KEY_FIXED_TO_LOC); + } + } + + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + this.binding = DataBindingUtil.setContentView(this, R.layout.activity_share_location); + setSupportActionBar(binding.toolbar); + configureActionBar(getSupportActionBar()); + setupMapView(binding.map, LocationProvider.getGeoPoint(this)); + + this.binding.cancelButton.setOnClickListener(view -> { + setResult(RESULT_CANCELED); + finish(); + }); + + this.snackBar = Snackbar.make(this.binding.snackbarCoordinator, R.string.location_disabled, Snackbar.LENGTH_INDEFINITE); + this.snackBar.setAction(R.string.enable, view -> { + if (isLocationEnabledAndAllowed()) { + updateUi(); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !hasLocationPermissions()) { + requestPermissions(REQUEST_CODE_SNACKBAR_PRESSED); + } else if (!isLocationEnabled()) { + startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)); + } + }); + ThemeHelper.fix(this.snackBar); + + this.binding.shareButton.setOnClickListener(this::shareLocation); + + this.marker_fixed_to_loc = isLocationEnabledAndAllowed(); + + this.binding.fab.setOnClickListener(view -> { + if (!marker_fixed_to_loc) { + if (!isLocationEnabled()) { + startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + requestPermissions(REQUEST_CODE_FAB_PRESSED); + } + } + toggleFixedLocation(); + }); + } + + private void shareLocation(final View view) { + final Intent result = new Intent(); + if (marker_fixed_to_loc && myLoc != null) { + result.putExtra("latitude", myLoc.getLatitude()); + result.putExtra("longitude", myLoc.getLongitude()); + result.putExtra("altitude", myLoc.getAltitude()); + result.putExtra("accuracy", DoubleMath.roundToInt(myLoc.getAccuracy(), RoundingMode.HALF_UP)); + } else { + final IGeoPoint markerPoint = this.binding.map.getMapCenter(); + result.putExtra("latitude", markerPoint.getLatitude()); + result.putExtra("longitude", markerPoint.getLongitude()); + } + setResult(RESULT_OK, result); + finish(); + } + + @Override + public void onRequestPermissionsResult(final int requestCode, + @NonNull final String[] permissions, + @NonNull final int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + + if (grantResults.length > 0 && + grantResults[0] != PackageManager.PERMISSION_GRANTED && + Build.VERSION.SDK_INT >= 23 && + permissions.length > 0 && + ( + Manifest.permission.LOCATION_HARDWARE.equals(permissions[0]) || + Manifest.permission.ACCESS_FINE_LOCATION.equals(permissions[0]) || + Manifest.permission.ACCESS_COARSE_LOCATION.equals(permissions[0]) + ) && + !shouldShowRequestPermissionRationale(permissions[0])) { + noAskAgain = true; + } + + if (!noAskAgain && requestCode == REQUEST_CODE_SNACKBAR_PRESSED && !isLocationEnabled() && hasLocationPermissions()) { + startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)); + } + updateUi(); + } + + @Override + protected void gotoLoc(final boolean setZoomLevel) { + if (this.myLoc != null && mapController != null) { + if (setZoomLevel) { + mapController.setZoom(Config.Map.FINAL_ZOOM_LEVEL); + } + mapController.animateTo(new GeoPoint(this.myLoc)); + } + } + + @Override + protected void setMyLoc(final Location location) { + this.myLoc = location; + } + + @Override + protected void onPause() { + super.onPause(); + } + + @Override + protected void updateLocationMarkers() { + super.updateLocationMarkers(); + if (this.myLoc != null) { + this.binding.map.getOverlays().add(new MyLocation(this, null, this.myLoc)); + if (this.marker_fixed_to_loc) { + this.binding.map.getOverlays().add(new Marker(marker_icon, new GeoPoint(this.myLoc))); + } else { + this.binding.map.getOverlays().add(new Marker(marker_icon)); + } + } else { + this.binding.map.getOverlays().add(new Marker(marker_icon)); + } + } + + @Override + public void onLocationChanged(final Location location) { + if (this.myLoc == null) { + this.marker_fixed_to_loc = true; + } + updateUi(); + if (LocationHelper.isBetterLocation(location, this.myLoc)) { + final Location oldLoc = this.myLoc; + this.myLoc = location; + + // Don't jump back to the users location if they're not moving (more or less). + if (oldLoc == null || (this.marker_fixed_to_loc && this.myLoc.distanceTo(oldLoc) > 1)) { + gotoLoc(); + } + + updateLocationMarkers(); + } + } + + @Override + public void onStatusChanged(final String provider, final int status, final Bundle extras) { + + } + + @Override + public void onProviderEnabled(final String provider) { + + } + + @Override + public void onProviderDisabled(final String provider) { + + } + + private boolean isLocationEnabledAndAllowed() { + return this.hasLocationFeature && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || this.hasLocationPermissions()) && this.isLocationEnabled(); + } + + private void toggleFixedLocation() { + this.marker_fixed_to_loc = isLocationEnabledAndAllowed() && !this.marker_fixed_to_loc; + if (this.marker_fixed_to_loc) { + gotoLoc(false); + } + updateLocationMarkers(); + updateUi(); + } + + @Override + protected void updateUi() { + if (!hasLocationFeature || noAskAgain || isLocationEnabledAndAllowed()) { + this.snackBar.dismiss(); + } else { + this.snackBar.show(); + } + + if (isLocationEnabledAndAllowed()) { + this.binding.fab.setVisibility(View.VISIBLE); + runOnUiThread(() -> { + this.binding.fab.setImageResource(marker_fixed_to_loc ? R.drawable.ic_gps_fixed_white_24dp : + R.drawable.ic_gps_not_fixed_white_24dp); + this.binding.fab.setContentDescription(getResources().getString( + marker_fixed_to_loc ? R.string.action_unfix_from_location : R.string.action_fix_to_location + )); + this.binding.fab.invalidate(); + }); + } else { + this.binding.fab.setVisibility(View.GONE); + } + } } \ No newline at end of file From f8a94161dbc3398e6d355175e45d7b5e407bf32c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 19 Nov 2021 12:25:27 +0100 Subject: [PATCH 027/394] don't play tone going from connect->reconnect->connect --- .../java/eu/siacs/conversations/xmpp/jingle/ToneManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java index 4fb9dee16..e368d3b09 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java @@ -51,7 +51,7 @@ private static ToneState of(final boolean isInitiator, final RtpEndUserState sta return ToneState.ENDING_CALL; } } - if (state == RtpEndUserState.CONNECTED) { + if (state == RtpEndUserState.CONNECTED || state == RtpEndUserState.RECONNECTING) { if (media.contains(Media.VIDEO)) { return ToneState.NULL; } else { From db834a1f07ee32005d8ba87e11ff03265f986a24 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 19 Nov 2021 12:26:11 +0100 Subject: [PATCH 028/394] indicate call reconnect in notification --- .../services/NotificationService.java | 17 +++++++++++++---- .../services/XmppConnectionService.java | 18 ++++++++++-------- .../xmpp/jingle/JingleRtpConnection.java | 14 +++++++++++--- src/main/res/values/strings.xml | 2 ++ 4 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index 2f6f36c59..ca4499300 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -488,14 +488,23 @@ private void showIncomingCallNotification(final AbstractJingleConnection.Id id, notify(INCOMING_CALL_NOTIFICATION_ID, notification); } - public Notification getOngoingCallNotification(final AbstractJingleConnection.Id id, final Set media) { + public Notification getOngoingCallNotification(final XmppConnectionService.OngoingCall ongoingCall) { + final AbstractJingleConnection.Id id = ongoingCall.id; final NotificationCompat.Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "ongoing_calls"); - if (media.contains(Media.VIDEO)) { + if (ongoingCall.media.contains(Media.VIDEO)) { builder.setSmallIcon(R.drawable.ic_videocam_white_24dp); - builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_video_call)); + if (ongoingCall.reconnecting) { + builder.setContentTitle(mXmppConnectionService.getString(R.string.reconnecting_video_call)); + } else { + builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_video_call)); + } } else { builder.setSmallIcon(R.drawable.ic_call_white_24dp); - builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_call)); + if (ongoingCall.reconnecting) { + builder.setContentTitle(mXmppConnectionService.getString(R.string.reconnecting_call)); + } else { + builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_call)); + } } builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName()); builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 815182680..42b699e46 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1298,8 +1298,8 @@ public void toggleForegroundService() { toggleForegroundService(false); } - public void setOngoingCall(AbstractJingleConnection.Id id, Set media) { - ongoingCall.set(new OngoingCall(id, media)); + public void setOngoingCall(AbstractJingleConnection.Id id, Set media, final boolean reconnecting) { + ongoingCall.set(new OngoingCall(id, media, reconnecting)); toggleForegroundService(false); } @@ -1315,7 +1315,7 @@ private void toggleForegroundService(boolean force) { final Notification notification; final int id; if (ongoing != null) { - notification = this.mNotificationService.getOngoingCallNotification(ongoing.id, ongoing.media); + notification = this.mNotificationService.getOngoingCallNotification(ongoing); id = NotificationService.ONGOING_CALL_NOTIFICATION_ID; startForeground(id, notification); mNotificationService.cancel(NotificationService.FOREGROUND_NOTIFICATION_ID); @@ -4869,12 +4869,14 @@ public void onReceive(Context context, Intent intent) { } public static class OngoingCall { - private final AbstractJingleConnection.Id id; - private final Set media; + public final AbstractJingleConnection.Id id; + public final Set media; + public final boolean reconnecting; - public OngoingCall(AbstractJingleConnection.Id id, Set media) { + public OngoingCall(AbstractJingleConnection.Id id, Set media, final boolean reconnecting) { this.id = id; this.media = media; + this.reconnecting = reconnecting; } @Override @@ -4882,12 +4884,12 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; OngoingCall that = (OngoingCall) o; - return Objects.equal(id, that.id); + return reconnecting == that.reconnecting && Objects.equal(id, that.id) && Objects.equal(media, that.media); } @Override public int hashCode() { - return Objects.hashCode(id); + return Objects.hashCode(id, media, reconnecting); } } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index e4661a3aa..12ba35733 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -1484,8 +1484,10 @@ public void onConnectionChange(final PeerConnection.PeerConnectionState newState this.stateHistory.add(newState); if (newState == PeerConnection.PeerConnectionState.CONNECTED) { this.sessionDuration.start(); + updateOngoingCallNotification(); } else if (this.sessionDuration.isRunning()) { this.sessionDuration.stop(); + updateOngoingCallNotification(); } final boolean neverConnected = !this.stateHistory.contains(PeerConnection.PeerConnectionState.CONNECTED); @@ -1633,8 +1635,15 @@ private void updateEndUserState() { } private void updateOngoingCallNotification() { - if (STATES_SHOWING_ONGOING_CALL.contains(this.state)) { - xmppConnectionService.setOngoingCall(id, getMedia()); + final State state = this.state; + if (STATES_SHOWING_ONGOING_CALL.contains(state)) { + final boolean reconnecting; + if (state == State.SESSION_ACCEPTED) { + reconnecting = getPeerConnectionStateAsEndUserState() == RtpEndUserState.RECONNECTING; + } else { + reconnecting = false; + } + xmppConnectionService.setOngoingCall(id, getMedia(), reconnecting); } else { xmppConnectionService.removeOngoingCall(); } @@ -1758,7 +1767,6 @@ public Optional getRemoteVideoTrack() { return webRTCWrapper.getRemoteVideoTrack(); } - public EglBase.Context getEglBaseContext() { return webRTCWrapper.getEglBaseContext(); } diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index dae88c606..ff1894533 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -920,6 +920,8 @@ Hang up Ongoing call Ongoing video call + Reconnecting call + Reconnecting video call Disable Tor to make calls Incoming call Incoming call · %s From 51db83d62975d12e060320f3f35207cb1c0ec300 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 8 Jan 2022 11:14:29 +0100 Subject: [PATCH 029/394] pulled translations from transifex --- src/main/res/values-bg/strings.xml | 35 ++++++++++++++++++++------ src/main/res/values-da-rDK/strings.xml | 8 +++++- src/main/res/values-gl/strings.xml | 8 +++--- src/main/res/values-ja/strings.xml | 2 +- 4 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src/main/res/values-bg/strings.xml b/src/main/res/values-bg/strings.xml index 41b22aed6..1530e1630 100644 --- a/src/main/res/values-bg/strings.xml +++ b/src/main/res/values-bg/strings.xml @@ -101,7 +101,7 @@ Изпращане нешифровано Неуспешно дешифроване. Възможно е да нямате правилния частен ключ. OpenKeychain - OpenKeychain, за да шифрова и дешифрова съобщенията и да управлява публичните Ви ключове.OpenKeychain е с лиценз GPLv3+ и може се свали от F-Droid и Google Play.

(Моля, рестартирайте %1$s след това.)]]>
+ OpenKeychain, за да шифрова и дешифрова съобщенията и да управлява публичните Ви ключове.

OpenKeychain е под лиценза GPLv3+ и може се свали от F-Droid и Google Play.

(Моля, рестартирайте %1$s след това.)]]>
Рестартиране Инсталиране Моля, инсталирайте OpenKeychain @@ -188,7 +188,7 @@ XMPP адрес username@example.com Парола - Това не е валиден XMPP адрес + Това не е правилен XMPP адрес Няма достатъчно памет. Изображението е твърде голямо. Искате ли да добавите %s към адресния си указател? Инф. за сървъра @@ -351,11 +351,11 @@ Отхвърлен Член Разширен режим - Дай членски привилегии - Премахни членски привилегии - Даване на администраторски права + Даване на правомощия на член + Премахване на правомощията на член + Даване на правомощия на администратор Отмяна на администраторските права - Дай права на собственик + Даване на правомощия на собственик Премахване от груповия разговор Неуспешна промяна на принадлежността на %s Забраняване на достъпа до груповия разговор @@ -448,6 +448,8 @@ %d съобщения Зареждане на още съобщения + Дайте на %1$s разрешение за достъп до външната памет + Дайте на %1$s разрешение за достъп до камерата Синхронизиране с контактите
Ние няма да пазим копия на тези телефонни номера.\n\nЗа повече информация, прочетете декларацията ни за поверителност.

Сега ще Ви помолим да дадете достъп до контактите си.]]>
Известяване за всички съобщения @@ -490,6 +492,9 @@ Поверителност Тема Изберете цветовата схема + Автоматично + Светла + Тъмна Зелен фон Получените съобщения ще бъдат на зелен фон Това устройство вече не се използва @@ -505,6 +510,7 @@ Няма позволение за достъп до %s Отдалеченият сървър не е намерен Времето за изчакване на отдалечения сървър изтече + Докладване този XMPP адрес за спам. Изтриване на идентификаторите OMEMO Изтриване на избраните ключове. Трябва да бъдете свързан(а), за да публикувате аватара си. @@ -563,6 +569,7 @@ Съответстващите разговори са затворени. Контактът е блокиран. Известия от непознати + Известяване за съобщения и обаждания от непознати. Получено е съобщение от непознат Блокиране на непознатия Блокиране на целия домейн @@ -572,11 +579,14 @@ Механизмът на SASL е понижен Сървърът изисква регистриране чрез уеб сайт Отваряне на уеб сайта + Няма намерено приложение за отваряне на уеб сайта Изскачащи известия + Показване на изскачащи известия Днес Вчера Проверка на името на сървъра чрез DNSSEC Сървърните сертификати, които съдържат проверено име на сървъра, се смятат за потвърдени + Сертификатът не съдържа XMPP адрес частично Запис на видео Копиране в буфера @@ -598,6 +608,9 @@ Редактиране на съобщението за състоянието Редактиране на съобщението за състоянието Изключване на шифроването + %1$s не може да изпраща шифровани съобщения до %2$s. Възможно е Вашият контакт да използва остарял сървър или клиент, който не може да работи с OMEMO. + Неуспешно получаване на списъка с устройства + Неуспешно получаване на ключове за шифроване Съвет: В някои случаи това може да се оправи, ако се добавите един друг в списъците си с контакти. Наистина ли искате да изключите шифроването чрез OMEMO за този разговор?\nТова ще позволи на администратора Ви да чете съобщенията Ви, но пък най-вероятно е единственият начин за общуване с хората, използващи стари клиенти. Изключване сега @@ -626,13 +639,16 @@ Споделяне на местоположението Показване на местоположението Споделяне + Записът не може да започне Моля, изчакайте… + Дайте на %1$s разрешение за достъп до микрофона Търсене в съобщенията GIF Преглед на разговора Разширение за споделяне на местоположението Използване на разширението за споделяне на местоположението вместо вградената карта Копиране на уеб адрес + Копиране на XMPP адрес Споделяне на файлове през HTTP за S3 Директно търсене На екрана за „Започване на разговор“ да се отваря клавиатурата и да се поставя курсорът в полето за търсене @@ -645,12 +661,17 @@ Въвеждането на име не е задължитално Име на груповия разговор Този групов разговор е унищожен + Записът не може да бъде запазен Услуга на преден план + Тази категория известия се използва за показване на постоянно известие, което показва, че %1$s работи. Информация за състоянието Проблеми с връзката Тази категория известия се използва за показване на известие, в случай че има проблем със свързването с профил. Съобщения + Обаждания Съобщения + Входящи обаждания + Изходящи обаждания Тихи съобщения Тази категория известия се използва за показване на известия, които не бива да изпълняват звук. Това може да се използва, например, докато използвате друго устройство (по време на Период на пренебрегване). Важност, звук, вибрация @@ -723,6 +744,6 @@ Създаване на групов разговор XMPP адрес Присъединихте се към съществуващ канал - Добави съществуващ профил + Добавяне на съществуващ профил Зает diff --git a/src/main/res/values-da-rDK/strings.xml b/src/main/res/values-da-rDK/strings.xml index bbcd082f0..8c06e3f50 100644 --- a/src/main/res/values-da-rDK/strings.xml +++ b/src/main/res/values-da-rDK/strings.xml @@ -132,6 +132,8 @@ Ved at indsende \"stack traces\" hjælper du udviklingen Bekræft beskeder Lad dine kontakter vide når du har modtaget og læst deres beskeder + Forbyd skærmbillede + Skjul app indhold i app-skifteren og bloker skærmbilleder UI OpenKeychain producerede en fejl Dårlig nøgle til kryptering @@ -414,6 +416,7 @@ lyd video billede + vektorgrafik PDF dokument Android App Kontakt @@ -912,6 +915,7 @@ Forbindelsen tabt Tilbagetrukket opkald App fejl + Bekræftelsesproblem Læg på Udgående opkald Igangværende videoopkald @@ -963,4 +967,6 @@ Ingen aktive konti understøtter denne funktion Sikkerhedskopieringen er startet. Du får en notifikation, når den er afsluttet. Kunne ikke aktivere video. - + Ren tekstdokument + + diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index b272a1b4a..fb320cfaa 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -89,7 +89,7 @@ Eliminar historial da conversa ¿Queres eliminar as mensaxes desta conversa?\n\nAviso: Esto non lle afecta as mensaxes gardadas noutros dispositivos ou servidores. Eliminar ficheiro - Está segura de querer eliminar este ficheiro?\n\nAviso: Esto non eliminará as copias de este ficheiro que están gardadas en outros dispositivos ou servidores. + Tes a certeza de querer eliminar este ficheiro?\n\nAviso: Esto non eliminará as copias de este ficheiro que están gardadas noutros dispositivos ou servidores. Pechar a conversa tras baleirar Escoller dispositivo Enviar mensaxe non cifrada @@ -244,7 +244,7 @@ Eliminar marcador Destruír a conversa en grupo Eliminar canle - Está segura de querer destruír esta conversa en grupo?\n\nAviso: A conversa en grupo será totalmente eliminada do servidor. + Tes a certeza de querer destruír esta conversa en grupo?\n\nAviso: A conversa en grupo será totalmente eliminada do servidor. Tes a certeza de querer eliminar a canle?\n\nAviso: A canle será eliminada completamente do servidor. Non se desfixo a conversa en grupo Non se puido eliminar a canle @@ -688,7 +688,7 @@ ¿Aceptar certificado descoñecido? O certificado do servidor non está asinado por unha autoridade de certificación coñecida. ¿Aceptar un nome de servidor que non coincida? - O servidor non pode autenticarse como \"%s\". O certificado só é válido para: + O servidor non pode autenticarse como \"%s\". O certificado só é válido para: Queres conectarte de todos os xeitos? Detalles do certificado: Unha vez @@ -828,7 +828,7 @@ Rexeitar solicitude Instalar Orbot Iniciar Orbot - Non ten loxa de aplicacións instalada. + Non hai tenda de apps instalada. Esta canle fará público o teu enderezo XMPP e-book Orixinal (non comprimido) diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml index 9037dd471..9963796ae 100644 --- a/src/main/res/values-ja/strings.xml +++ b/src/main/res/values-ja/strings.xml @@ -130,7 +130,7 @@ メッセージを確認 あなたがメッセージを受信して読んだときに、連絡先に知らせる スクリーンショットを防ぐ - アプリスイッチャーでアプリの内容を隠し、スクリーンショットを防ぐ + アプリスイッチャー内でアプリの内容を隠し、スクリーンショットを防ぐ UI OpenKeychain でエラーが発生しました。 暗号化の鍵が不正です。 From 666ca485dbd0e00ac26b32fddcec295c15a7dc15 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 13 Jan 2022 20:58:47 +0100 Subject: [PATCH 030/394] pulled translations from transifex --- src/conversations/res/values-sv/strings.xml | 11 +++-- src/main/res/values-bg/strings.xml | 48 ++++++++++++++++++++- src/main/res/values-de/strings.xml | 2 +- src/main/res/values-sv/strings.xml | 3 ++ 4 files changed, 59 insertions(+), 5 deletions(-) diff --git a/src/conversations/res/values-sv/strings.xml b/src/conversations/res/values-sv/strings.xml index e898b5643..a6185650e 100644 --- a/src/conversations/res/values-sv/strings.xml +++ b/src/conversations/res/values-sv/strings.xml @@ -1,8 +1,13 @@ - Välj din XMPP leverantör + Välj din XMPP-leverantör Använd conversations.im - Skapa nytt konto - Din server inbjudan + Skapa ett nytt konto + Har du redan ett XMPP-konto? Detta kan vara fallet om du redan använder en annan XMPP-klient eller om du har använt Conversations tidigare. Om inte, kan du skapa ett nytt XMPP-konto på en gång.\nTips: Vissa e-postleverantörer tillhandahåller även XMPP-konton. + Din serverinbjudan + Felaktigt formaterad provisioneringskod + Tryck på dela-knappen för att skicka en inbjudan till din kontakt till %1$s. + Om din kontakt är i närheten, kan de också skanna koden nedan för att acceptera din inbjudan. + Gå med %1$s och chatta med mig: %2$s Dela inbjudan med... \ No newline at end of file diff --git a/src/main/res/values-bg/strings.xml b/src/main/res/values-bg/strings.xml index 1530e1630..c1465e59c 100644 --- a/src/main/res/values-bg/strings.xml +++ b/src/main/res/values-bg/strings.xml @@ -59,6 +59,7 @@ Споделяне с… Започване на разговор Поканете контакт + Поканете Контакти Контакт Отказ @@ -164,6 +165,7 @@ Потребителското име е заето Регистрацията е завършена Регистрацията не се поддържа от сървъра + Неправилен регистрационен идентификатор Договарянето чрез TLS беше неуспешно Домейнът не може да се провери Нарушение на политиката @@ -182,10 +184,11 @@ Наистина ли искате да премахнете своя публичен OpenPGP ключ от известяването си за присъствие?\nКонтактите Ви вече няма да могат да Ви изпращат съобщение, шифровани чрез OpenPGP. Публичният OpenPGP ключ е публикуван. Активиране на профила - Сигурни ли сте? + Наистина ли искате това? Изтриването на профила Ви ще изтрие и цялата история на разговорите Ви Запис на глас XMPP адрес + Блокиране на XMPP адрес username@example.com Парола Това не е правилен XMPP адрес @@ -224,6 +227,7 @@ Изтегляне на ключове… Готово Дешифроване + Отметки Търсене Въведете контакт Изтриване на контакта @@ -274,8 +278,10 @@ Включване Груповият разговор изисква парола Въведете парола + Моля, първо помолете контакта за актуализации на присъствието му.\n\nТова ще бъде използвано, за да се провери какво приложение използва контактът. Поискване сега Пренебрегване + Внимание: Изпращането на това без съвместни актуализации на присъствието може да доведе до неочаквани проблеми.\n\nПогледнете подробностите за контакта, за да проверите дали сте абониран за актуализации на присъствието. Сигурност Позволяване на поправянето на съобщения Позволяване на контактите да редактират съобщенията си след като са ги изпратили. @@ -289,6 +295,8 @@ Известията ще бъдат заглушени по време на тихите часове Други Синхронизиране с отметките + Автоматично присъединяване към групови разговори, ако такава е настройката на отметката + Отпечатъкът OMEMO е копиран Достъпът Ви до този групов разговор е забранен Този групов разговор е само за членове Ограничение на ресурса @@ -307,6 +315,7 @@ Повторно изпращане Адрес на файла Копиране на адреса + XMPP-адресът е копиран Съобщението за грешка е копирано уеб адрес Сканиране на 2-измерен баркод @@ -317,6 +326,14 @@ Повторен опит Услуга на преден план Предотвратява прекъсването на връзката Ви от операционната система + Създаване на резервно копие + Резервните копия ще се пазят в %s + Създаване на резервни копия + Резервното копие е създадено + Файловете на резервното копие бяха запазени в %s + Възстановяване от резервно копие + Възстановяването от резервно копие е завършено + Не забравяйте да включите профила. Изберете файл Получаване на %1$s (%2$d%% завършено) Сваляне на %s @@ -324,16 +341,27 @@ файл Отваряне на %s изпращане (%1$d%% завършено) + Файлът се подготвя за споделяне %s е предложен за сваляне Отказ на прехвърлянето + файлът не може да бъде споделен + изпращането на файла е отменено + Файлът е изтрит + Няма намерено приложение за отваряне на файла + Няма намерено приложение за отваряне на връзката + Няма намерено приложение за преглед на контакта Динамични етикети Показване на етикети, предназначени само за четене под контактите Включване на известията Не е открит сървър за груповия разговор + Груповият разговор не може да бъде създаден Аватар на профила Копиране на отпечатъка OMEMO Повторно създаване на ключа OMEMO Премахване на устройствата + Наистина ли искате да премахнете всички останали устройства от обявлението OMEMO? Следващия път, когато устройствата Ви се свържат, те ще обявят себе си отново, но може да не получат съобщенията, изпратени междувременно. + Няма ключове, които могат да бъдат използвани за този контакт.\nОт сървъра не могат да бъдат изтеглени нови ключове. Възможно е да има проблем със сървъра на контакта Ви. + Няма ключове, които могат да бъдат използвани за този контакт.\nУверете се, че и двамата имате абонамент за присъствието. Нещо се обърка Получаване на историята от сървъра Няма повече история на сървъра @@ -343,6 +371,7 @@ Промяна на паролата Текуща парола Нова парола + Паролата не може да е празна Активиране на всички профили Деактивиране на всички профили Изпълнение на действието с @@ -356,9 +385,13 @@ Даване на правомощия на администратор Отмяна на администраторските права Даване на правомощия на собственик + Премахване на правомощията на собственик Премахване от груповия разговор + Премахване от канала Неуспешна промяна на принадлежността на %s Забраняване на достъпа до груповия разговор + Забраняване на достъпа до канала + Опитвате се да премахнете%s от публичен канал. Единственият начин да направите това е да блокирате завинаги потребителя. Забраняване на достъпа сега Неуспешна промяна на ролята на %s Частно, само за членове @@ -411,6 +444,7 @@ Използвани наскоро Изберете бързо действие Търсене в контактите + Търсене в отметките Изпращане на лично съобщение Потребителско име Потребителско име @@ -732,6 +766,7 @@ Инсталиране на Orbot Пускане на Orbot Няма инсталирано приложение за инсталиране на приложения. + Този канал ще направи Вашия XMPP-адрес публичен е-книга Оригинално (некомпресирано) Отваряне с… @@ -742,8 +777,19 @@ Въведете паролата си за профила %s, за да направите възстановяване от резервно копие. Не използвайте възможността за възстановяване от резервно копие, за да клонирате (да изпълнявате едновременно) инсталацията. Възстановяването от резервно копие е предназначено за мигриране или в случай, че сте загубили устройството си. Създаване на групов разговор + Присъединяване към публичен канал + Създаване на публичен канал XMPP адрес + Създаване на публичен канал… Присъединихте се към съществуващ канал + Собствениците могат да канят други хора. + Всеки може да кани други хора. + В този публичен канал няма никакви участници. Поканете контактите си или използвайте бутона за споделяне, за да разпространите XMPP-адреса на канала. Добавяне на съществуващ профил + Присъединяване към публичен канал… + Повечето потребители трябва да изберат „jabber.network“ за по-добри предложения от цялата публична екосистема на XMPP. Зает + Поканата не може да бъде анализирана + Сървърът не поддържа създаването на покани + Видеото не може да бъде включено. diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index c5a7e680b..4318db444 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -270,7 +270,7 @@ an %s Private Nachricht an %s senden Verbinden - Das Konto existiert bereits + Dieses Konto existiert bereits Weiter Sitzung wiederhergestellt Überspringen diff --git a/src/main/res/values-sv/strings.xml b/src/main/res/values-sv/strings.xml index 96b77ff57..35666eb4a 100644 --- a/src/main/res/values-sv/strings.xml +++ b/src/main/res/values-sv/strings.xml @@ -71,11 +71,13 @@ Avblockera Spara Ok + %1$s har kraschat Skicka nu Fråga aldrig igen Kunde inte ansluta till konto Kunde inte ansluta till flera konton Bifoga fil + Vill du lägga till den här saknade kontakten i din kontaktlista? Lägg till kontakt sändning misslyckades Förbereder att skicka bild @@ -135,6 +137,7 @@ Ta ny bild Tillåt abonnemangsbegäran i förväg Filen du valt är inte en bild + Det gick inte att konvertera bildfilen Filen hittas ej Generellt I/O-fel. Du kanske fick slut på plats? Okänd From 68fd17778ccd16a66f164ae1bb19a83e6d550a4b Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 18 Jan 2022 09:48:10 +0100 Subject: [PATCH 031/394] bump agp version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a95b2ff8e..e4d037114 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.0.3' + classpath 'com.android.tools.build:gradle:7.0.4' } } From eed5c5e74319520e7541102f906e02d81849ad62 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 18 Jan 2022 09:49:10 +0100 Subject: [PATCH 032/394] add additional logging to image compression --- .../java/eu/siacs/conversations/persistance/FileBackend.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 48ffae422..a4644fd56 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -746,12 +746,15 @@ private void copyImageToPrivateStorage(File file, Uri image, int sampleSize) thr final int imageMaxSize = mXmppConnectionService.getResources().getInteger(R.integer.auto_accept_filesize); while (!targetSizeReached) { os = new FileOutputStream(file); + Log.d(Config.LOGTAG, "compressing image with quality " + quality); boolean success = scaledBitmap.compress(Config.IMAGE_FORMAT, quality, os); if (!success) { throw new FileCopyException(R.string.error_compressing_image); } os.flush(); - targetSizeReached = file.length() <= imageMaxSize || quality <= 50; + final long fileSize = file.length(); + Log.d(Config.LOGTAG, "achieved file size of " + fileSize); + targetSizeReached = fileSize <= imageMaxSize || quality <= 50; quality -= 5; } scaledBitmap.recycle(); From b6442c0bd45f13ae701fa42863338b6192ee91e7 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 18 Jan 2022 11:30:17 +0100 Subject: [PATCH 033/394] add Samsung S4 to hardware aec blacklist fixes #4267 --- .../eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 6722f9f2c..0b990db43 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -64,8 +64,7 @@ public class WebRTCWrapper { private static final String EXTENDED_LOGGING_TAG = WebRTCWrapper.class.getSimpleName(); private final ExecutorService executorService = Executors.newSingleThreadExecutor(); - - //we should probably keep this in sync with: https://github.com/signalapp/Signal-Android/blob/master/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java#L296 + private static final Set HARDWARE_AEC_BLACKLIST = new ImmutableSet.Builder() .add("Pixel") .add("Pixel XL") @@ -79,6 +78,9 @@ public class WebRTCWrapper { .add("Redmi Note 5") .add("FP2") // Fairphone FP2 .add("MI 5") + .add("GT-I9515") // Samsung Galaxy S4 Value Edition (jfvelte) + .add("GT-I9515L") // Samsung Galaxy S4 Value Edition (jfvelte) + .add("GT-I9505") // Samsung Galaxy S4 (jfltexx) .build(); private static final int CAPTURING_RESOLUTION = 1920; From f2a67f899b5f4d5fbe1419f1b0568d9e84c35921 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 9 Feb 2022 12:17:29 +0100 Subject: [PATCH 034/394] pulled translations from transifex --- src/conversations/res/values-bg/strings.xml | 10 +- src/conversations/res/values-fi/strings.xml | 14 + src/main/res/values-bg/strings.xml | 188 +++- src/main/res/values-de/strings.xml | 14 +- src/main/res/values-es/strings.xml | 8 +- src/main/res/values-fi/strings.xml | 918 ++++++++++++++++++++ src/main/res/values-ru/strings.xml | 7 +- src/main/res/values-sv/strings.xml | 128 +++ src/quicksy/res/values-bg/strings.xml | 2 +- src/quicksy/res/values-fi/strings.xml | 12 + 10 files changed, 1279 insertions(+), 22 deletions(-) create mode 100644 src/conversations/res/values-fi/strings.xml create mode 100644 src/main/res/values-fi/strings.xml create mode 100644 src/quicksy/res/values-fi/strings.xml diff --git a/src/conversations/res/values-bg/strings.xml b/src/conversations/res/values-bg/strings.xml index 2981a6542..7ef32e025 100644 --- a/src/conversations/res/values-bg/strings.xml +++ b/src/conversations/res/values-bg/strings.xml @@ -1,13 +1,13 @@ - Изберете вашият XMPP доставчик + Изберете своя XMPP доставчик Използвайте conversations.im Създаване не нов профил - Имате ли вече XMPP профил? Това може да се случи, ако вече използвате друг клиент на XMPP или сте използвали преди това Conversations. Ако не, можете да създадете нов XMPP профил в момента.\nСъвет: Някои доставчици на имейл също предоставят XMPP профили. + Имате ли вече XMPP профил? Може да имате, ако вече използвате друг клиент на XMPP или сте използвали Conversations и преди. Ако не, можете да създадете нов XMPP профил сега.\nСъвет: някои доставчици на е-поща също предоставят XMPP профили.   - XMPP е мрежа за общуване чрез мигновени съобщения, която не е обвързана с конкретен доставчик. Можете да използвате клиента с всеки сървър, който работи с XMPP.\nЗа Ваше удобство, ние предоставяме лесен начин да си създадете профил в conversations.im¹ — сървър, пригоден да работи добре с Conversations. - Бяхте поканен(а) в %1$s. Ще Ви преведем през процеса на създаване на акаунт.\nИзбирайки %1$s за доставчик, Вие ще можете да общувате и с потребители на други доставчици, като им предоставите своя пълен адрес за XMPP. - Бяхте поканен(а) в %1$s. Вече Ви избрахме потребителско име. Ще Ви преведем през процеса на създаване на акаунт.\nЩе можете да общувате и с потребители на други доставчици, като им предоставите своя пълен адрес за XMPP. + XMPP е мрежа за общуване чрез мигновени съобщения, която не е обвързана с конкретен доставчик. Можете да използвате клиента с всеки сървър, който работи с XMPP.\nЗа Ваше удобство, обаче, ние предоставяме лесен начин да си създадете профил в conversations.im¹ — сървър, пригоден да работи най-добре с Conversations. + Получихте покана за %1$s. Ще Ви преведем през процеса на създаване на профил.\nИзбирайки %1$s за доставчик, Вие ще можете да общувате и с потребители на други доставчици, като им предоставите своя пълен XMPP адрес. + Получихте покана за %1$s. Вече Ви избрахме потребителско име. Ще Ви преведем през процеса на създаване на профил.\nЩе можете да общувате и с потребители на други доставчици, като им предоставите своя пълен XMPP адрес. Вашата покана за сървъра Неправилно форматиран код за достъп Докоснете бутона за споделяне, за да изпратите на контакта си покана за %1$s. diff --git a/src/conversations/res/values-fi/strings.xml b/src/conversations/res/values-fi/strings.xml new file mode 100644 index 000000000..17c75a297 --- /dev/null +++ b/src/conversations/res/values-fi/strings.xml @@ -0,0 +1,14 @@ + + + Valitse XMPP-palveluntarjoaja + Käytä conversations.im:ää + Luo uusi tili + Onko sinulla jo XMPP-tunnus? Jos käytät jo toista XMPP-sovellusta tai olet käyttänyt Conversationsia aiemmin, niin voi olla. Jos ei, voit tehdä uuden XMPP-tilin saman tien.\nVinkki: Jotkin sähköpostipalvelut tarjoavat myös XMPP-tilin. + XMPP on tietystä palveluntarjoasta riippumaton pikaviestiverkosto. Voit käyttää tätä asiakasohjelmaa minkä tahansa haluamasi XMPP-palvelimen kanssa.\nHelppouden nimissä olemme kuitenkin helpottaneet tilin luomista conversations.im:iin. + Sinut on kutsuttu %1$s:iin. Opastamme sinua tilin luomisen kanssa.\nValitessasi palvelimen %1$s palveluntarjoajaksesi voit jutella muiden palveluntajoajien käyttäjien kanssa kertomalla heille koko XMPP-osoitteesi. + Sinut on kutsuttu palvelimelle %1$s. Käyttäjänimesi on valittu valmiiksi puolestasi. Opastamme sinua tilin luomisen kanssa.\nVoit jutella muiden palveluntarjoajien käyttäjien kanssa kertomalle heille koko XMPP-osoitteesi. + Kutsusi palvelimelle + Virheellisesti muotoiltu koodi + Jos henkilö on lähellä, hän voi myös hyväksyä kutsun lukemalla allaolevan koodin. + Jaa kutsu sovelluksella... + \ No newline at end of file diff --git a/src/main/res/values-bg/strings.xml b/src/main/res/values-bg/strings.xml index c1465e59c..ee4687dfa 100644 --- a/src/main/res/values-bg/strings.xml +++ b/src/main/res/values-bg/strings.xml @@ -263,7 +263,7 @@ Публикуване… Сървърът отказа Вашето публикуване Снимката Ви не може да бъде преобразувана - Неуспешно запазване на аватара на диска + Аватарът не може да бъде запазен на диска (Или задръжте, за да върнете началното) Сървърът Ви не поддържа публикуване на аватари прошепна @@ -367,7 +367,7 @@ Няма повече история на сървъра Актуализиране… Паролата е променена! - Неуспешна промяна на паролата + Паролата не може да бъде променена Промяна на паролата Текуща парола Нова парола @@ -388,16 +388,20 @@ Премахване на правомощията на собственик Премахване от груповия разговор Премахване от канала - Неуспешна промяна на принадлежността на %s + Принадлежността на %s не може да бъде променена Забраняване на достъпа до груповия разговор Забраняване на достъпа до канала Опитвате се да премахнете%s от публичен канал. Единственият начин да направите това е да блокирате завинаги потребителя. Забраняване на достъпа сега - Неуспешна промяна на ролята на %s + Ролята на %s не може да бъде променена + Настройка на частен групов разговор + Настройка на публичен групов разговор Частно, само за членове + Нека XMPP адресите бъдат видими за всички + Нека каналът да се модерира Вие не участвате Настройките на груповия разговор бяха променени! - Неуспешна промяна на настройките на груповия разговор + Настройките на груповия разговор не могат да бъдат променени Никога До отмяна Отлагане @@ -405,11 +409,13 @@ Отбелязване като прочетено Въвеждане Enter изпраща + Използвайте клавиша Enter, за да изпратите съобщение. Винаги може да използвате Ctrl+Enter за изпращане на съобщение, дори тази настройка да е изключена. Показване на клавиша Enter Смяна на клавиша за емотикони с клавиша Enter аудио видео изображение + векторна графика PDF документ Приложение за Андроид Контакт @@ -425,8 +431,11 @@ Така контактите Ви ще разбират, когато им пишете съобщения Изпращане на местоположението Показване на местоположението + Няма намерено приложение за показване на местоположението Местоположение Conversation се затвори + Напуснахте частния групов разговор + Напуснахте публичния канал Да не се вярва на системните сертификати Всички сертификати трябва да бъдат одобрени на ръка Премахване на сертификатите @@ -439,6 +448,7 @@ %d сертификат е изтрит %d сертификата са изтрити + Замяна на бутона „Изпращане“ с бързо действие Бързо действие Нищо Използвани наскоро @@ -446,6 +456,7 @@ Търсене в контактите Търсене в отметките Изпращане на лично съобщение + %1$s напусна груповия разговор Потребителско име Потребителско име Това не е правилно потребителско име @@ -455,16 +466,28 @@ Неуспешно сваляне: Файлът не може да бъде записан Мрежата на Тор е недостъпна Грешка при свързване + Сървърът не отговаря за този домейн Повредено Присъствие + Отсъстващ при заключено устройство + Показване като „отсъстващ“, когато устройството е заключено + Зает в тих режим + Показване като „зает“, когато устройството е в тих режим Тих режим при режим на вибриране + Показване като „зает“, когато устройството е на вибрация Разширени настройки за връзката Показване на настройките за сървър и порт при установка на профил xmpp.example.com + Влизане със сертификат + Сертификатът не може да бъде прочетен Настройки за архивирането Настройки за архивирането на сървъра Получаване на настройките за архивирането. Моля, изчакайте… + Настройките за архивирането не могат да бъдат получени + Проверката е задължителна Въведете текста от горното изображение + Недоверен верижен сертификат + XMPP адресът не съответства на сертификата Подновяване на сертификата Грешка при получаването на ключа за OMEMO! Ключът за OMEMO беше потвърден със сертификат! @@ -474,6 +497,7 @@ Всички връзки да минават през мрежата на Тор. Изисква Орбот Име на сървър Порт + Адрес на сървър или .onion Това не е правилен номер на порт Това не е правилно име на сървър %1$d от %2$d свързани профила @@ -482,25 +506,41 @@ %d съобщения Зареждане на още съобщения + Файлът е споделен с %s + Изображението е споделено с %s + Изображенията са споделени с %s + Текстът е споделен с %s Дайте на %1$s разрешение за достъп до външната памет Дайте на %1$s разрешение за достъп до камерата Синхронизиране с контактите + %1$s иска разрешение за достъп до адресната Ви книга, за да потърси съвпадения със списъка от контакти в XMPP.\nТова ще покаже пълните имена и аватари на контактите Ви.\n\n%1$s само ще прочете адресната книга и ще потърси съвпадения на това устройство – нищо няма да се качва на сървъра Ви.
Ние няма да пазим копия на тези телефонни номера.\n\nЗа повече информация, прочетете декларацията ни за поверителност.

Сега ще Ви помолим да дадете достъп до контактите си.]]>
Известяване за всички съобщения Известяване само при споменаване Известията са изключени Известията са спрени временно Компресия на изображенията + Съвет: използвайте „Изберете файл“ вместо „Изберете снимка“, за да изпращате снимките некомпресирани, независимо от тази настройка. Винаги + Само за големи изображения Оптимизациите за използв. на батерията са вкл. + Устройството Ви прилага сериозни оптимизации за използването на батерията върху %1$s, които може да доведат до забавени известия и дори пропуснати съобщения.\nПрепоръчително е да ги изключите. + Устройството Ви прилага сериозни оптимизации за използването на батерията върху %1$s, които може да доведат до забавени известия и дори пропуснати съобщения.\nСега ще бъдете помолен(а) да ги изключите. Изключване Избраната област е твърде голяма (Няма активирани профили) Това поле е задължително Поправяне на съобщението Изпращане на поправеното съобщение + Вече сте потвърдили доверието си в този човек, чрез защитена проверка на отпечатъка му. Ако изберете „Готово“, ще потвърдите само това, че %s е част от този групов разговор. Вие сте деактивирали този профил + Грешка в сигурността: неправилен достъп до файл! + Няма намерено приложение за споделяне на адреса Споделяне на адреса с… +
Трябва да се регистрирате чрез телефонния си номер, след което Quicksy автоматично ще претърси телефонните номера в указателя Ви и ще Ви предложи контакти в приложението.

Регистрирайки се, Вие се съгласявате с нашата декларация за поверителност.]]>
+ Съгласяване и продължаване + На conversations.im има ръководство за създаване на профил.\nИзбирайки conversations.im за доставчик, Вие ще можете да общувате и с потребители на други доставчици, като им предоставите своя пълен адрес за XMPP. + Пълният Ви XMPP адрес ще бъде: %s Създаване на профил Използване на собствен доставчик Изберете потребителското си име @@ -523,6 +563,8 @@ Кратко Средно Дълго + Информиране за използването + Позволява на контактите Ви да знаят кога използвате Conversations Поверителност Тема Изберете цветовата схема @@ -531,6 +573,7 @@ Тъмна Зелен фон Получените съобщения ще бъдат на зелен фон + Свързването с OpenKeychain е невъзможно Това устройство вече не се използва Компютър Мобилен телефон @@ -538,21 +581,29 @@ Браузър Конзола Изисква се плащане + Дайте разрешение за достъп до Интернет Аз Контакт моли за абонамент за присъствието Позволяване Няма позволение за достъп до %s Отдалеченият сървър не е намерен Времето за изчакване на отдалечения сървър изтече + Профилът не може да бъде обновен Докладване този XMPP адрес за спам. Изтриване на идентификаторите OMEMO + Пресъздайте своите ключове OMEMO. Всички Ваши контакти ще трябва да Ви потвърдят отново. Използвайте това само в краен случай. Изтриване на избраните ключове. Трябва да бъдете свързан(а), за да публикувате аватара си. Показване на грешка Съобщение за грешка - Съхранението на данни е включено + Пестенето на данни е включено + Операционната Ви система не позволява на %1$s да се свързва с Интернет когато работи на заден фон. За да получавате известия за новите съобщения, трябва да дадете на %1$s неограничен достъп когато пестенето на данни е включено.\n%1$s ще продължи да се опитва да записва данните когато е възможно. + Устройството Ви не поддържа изключването на пестенето на данни за %1$s. + Не може да се създаде временен файл Това устройство е потвърдено Копиране на отпечатъка + Потвърдили сте всички ключове OMEMO, които притежавате + Баркодът не съдържа отпечатъци за този разговор. Потвърдени отпечатъци Използвайте камерата, за да сканирате баркода на контакт Моля, изчакайте получаването на ключовете @@ -560,8 +611,11 @@ Споделяне като адрес на XMPP Споделяне като връзка в Интернет Доверяване на сляпо преди потвърждение + Нови устройства на непотвърдени контакти автоматично получават доверие, но нови устройства на потвърдени контакти изискват ръчно потвърждаване. + Доверени на сляпо ключове OMEMO, което означава, че това може да е някой друг, или че някой може да е получил неправомерен достъп. Неприети Грешен 2-измерен баркод + Изчистване на папката с кеша (използвана от камерата) Изчистване на кеша Изчистване на личното място за съхранение Изчистване на мястото, където се съхраняват личните файлове. (Те могат да бъдат повторно изтеглени от сървъра.) @@ -571,6 +625,7 @@ Показване на неактивните Скриване на неактивните Сваляне на доверието + Наистина ли искате да заличите потвърждението на това устройство?\nТова устройство и съобщенията от него ще бъдат отбелязани като „недоверени“. %d секунда %d секунди @@ -708,9 +763,13 @@ Изходящи обаждания Тихи съобщения Тази категория известия се използва за показване на известия, които не бива да изпълняват звук. Това може да се използва, например, докато използвате друго устройство (по време на Период на пренебрегване). + Неуспешни доставяния + Настройки на известията за съобщения + Настройки на известията за обаждания Важност, звук, вибрация Компресия на видеото Преглед на медийното съдържание + Участници Разглеждане на медийното съдържание Файлът е пропуснат поради нарушение на сигурността. Качество на видеото @@ -748,6 +807,9 @@ Кодът, който Ви изпратихме, е с изтекла давност. Неизвестна мрежова грешка. Непознат отговор от сървъра. + Свързването със сървъра е невъзможно. + Установяването на защитена връзка е невъзможно. + Сървърът не може да бъде намерен. Нещо се обърка при обработването на заявката Ви. Неправилно въведени данни Временно недостъпно. Опитайте отново по-късно. @@ -776,20 +838,132 @@ Възстановяване Въведете паролата си за профила %s, за да направите възстановяване от резервно копие. Не използвайте възможността за възстановяване от резервно копие, за да клонирате (да изпълнявате едновременно) инсталацията. Възстановяването от резервно копие е предназначено за мигриране или в случай, че сте загубили устройството си. + Не може да се извърши възстановяване от резервно копие. + Резервното копие не може да бъде дешифрирано. Правилна ли е паролата? + Резервни копия и възстановяване + Въведете XMPP адрес Създаване на групов разговор Присъединяване към публичен канал + Създаване на частен групов разговор Създаване на публичен канал + Име на канала XMPP адрес + Моля, задайте име за канала + Моля, задайте XMPP адрес + Това е XMPP адрес. Моля, задайте име. Създаване на публичен канал… + Този канал вече съществува Присъединихте се към съществуващ канал + Настройката на канала не може да бъде запазена + Нека всеки може да редактира темата + Нека всеки може да кани други хора + Всеки може да редактира темата. + Собствениците могат да редактират темата. + Администраторите могат да редактират темата. Собствениците могат да канят други хора. Всеки може да кани други хора. + XMPP адресите са видими за администраторите. + XMPP адресите са видими за всички. В този публичен канал няма никакви участници. Поканете контактите си или използвайте бутона за споделяне, за да разпространите XMPP-адреса на канала. + В този частен групов разговор няма никакви участници. + Управление на правомощията + Търсене на участници + Файлът е твърде голям + Прикачане + Откриване на канали + Търсене на канали + Възможно нарушаване на декларацията за поверителност! + search.jabber.network.

Ако използвате тази функционалност, Вашият IP адрес и въведеният текст за търсене ще бъдат изпратени до сървъра на тази услуга. Разгледайте нейната Декларация за поверителност за повече информация.]]>
+ Вече имам профил Добавяне на съществуващ профил + Регистриране на нов профил + Това прилича на адрес на домейн + Добавяне въпреки това + Това прилича на адрес на канал + Споделяне на файловете с резервни копия + Резервно копие от Conversations + Събитие + Отваряне на резервно копие + Избраният файл не е резервно копие от Conversations + Този профил вече е настроен + Въведете паролата за този профил + Това действие не може да бъде извършено Присъединяване към публичен канал… + Приложението за споделяне не даде разрешение за достъп до този файл. + + jabber.network + Локален сървър Повечето потребители трябва да изберат „jabber.network“ за по-добри предложения от цялата публична екосистема на XMPP. + Метод за откриване на канали + Резервно копие + Относно + Моля, активирайте профил + Направете обаждане + Входящо обаждане + Входящо видео-обаждане + Свързване + Установена връзка + Приемане на обаждане + Приключване на обаждане + Отговор + Отхвърляне + Откриване на устройства + Позвъняване Зает + Свързването с разговора е невъзможно + Връзката беше прекъсната + Върнат разговор + Грешка в приложението + Проблем с потвърждението + Затваряне + Текущо обаждане + Текущо видео-обаждане + Изключете Tor, за да правите обаждания + Входящо обаждане + Входящо обаждане · %s + Пропуснато обаждане · %s + Изходящо обаждане + Изходящо обаждане · %s + Пропуснато обаждане + Гласово обаждане + Видео обаждане + Помо + Превключване към разговор + Микрофонът не е наличен + Не може да има повече от едно обаждане едновременно. + Обратно към текущия разговор + Закачане горе + Откачане от горе + Съобщението не може да бъде поправено + Всички разговори + Този разговор + Вашият аватар + Аватар за %s + Шифровано с OMEMO + Шифровано с OpenPGP + Нешифровано + Изход + Запис на гласова поща + Възпроизвеждане на звука + Пауза на звука + Добавете контакт, създайте или се присъединете към групов разговор, или разгледайте каналите + + Преглед на %1$d член + Преглед на %1$d членове + + + Едно съобщение не може да бъде доставено + Някои съобщения не могат да бъдат доставени + + Неуспешни доставяния + Още настройки + Няма намерено приложение + Канене в Conversations Поканата не може да бъде анализирана Сървърът не поддържа създаването на покани + Нито един от активните профили не поддържа тази функционалност + Създаването на резервно копие е стартирано. Ще получите известие, когато приключи. Видеото не може да бъде включено. -
+ Обикновен текстов документ + + diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 4318db444..787025a67 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -264,7 +264,7 @@ Der Server hat die Veröffentlichung des Avatars abgelehnt. Bild konnte nicht konvertiert werden Avatar kann nicht gespeichert werden - (Oder klicke lange, um Standard wiederherzustellen) + (Oder klicke lange, um den Standard wiederherzustellen) Dein Server unterstützt die Veröffentlichung von Avataren nicht private Nachricht: an %s @@ -326,7 +326,7 @@ Bestätigen Erneut versuchen Vordergrunddienst - Verhindert, dass Android Conversations beendet und die Verbindung unterbricht + Verhindert, dass das Betriebssystem deine Verbindung unterbricht Sicherung erstellen Sicherungsdateien werden gespeichert in %s Erstelle Sicherungsdateien @@ -364,7 +364,7 @@ Für diesen Kontakt sind keine nutzbaren Schlüssel verfügbar.\nEs konnten keine neuen Schlüssel vom Server abgerufen werden. Gibt es vielleicht ein Problem mit dem Server deines Kontaktes? Für diesen Kontakt sind keine benutzbaren Schlüssel verfügbar.\nStelle sicher, dass ihre beide gegenseitig den Online-Status aktiviert habt. Etwas ist schief gelaufen - Lade Chatverlauf… + Lade Chatverlauf vom Server Keine weiteren Nachrichten vorhanden Aktualisieren… Passwort geändert! @@ -557,7 +557,7 @@ Dein Gerät unterstützt kein Ausschalten der Akkuoptimierung Registrierung fehlgeschlagen: Bitte später versuchen Registrierung fehlgeschlagen: Passwort zu schwach - Mitglieder wählen + Teilnehmer wählen Erstelle Gruppenchat… Erneut einladen Deaktivieren @@ -606,7 +606,7 @@ Du hast alle in deinem Besitz befindlichen OMEMO-Schlüssel überprüft Der Barcode enthält keine Fingerabdrücke für diese Unterhaltung. Überprüfte Fingerabdrücke - Nutze Kamera, um Barcodes deiner Kontakte zu scannen + Nutze die Kamera, um Barcodes deiner Kontakte zu scannen Bitte warten, bis die Schlüssel abgerufen werden Als Barcode teilen Als XMPP URI teilen @@ -896,7 +896,7 @@ Lokaler Server Die meisten Benutzer sollten hier ‘jabber.network’ auswählen, um bessere Vorschläge aus dem gesamten, öffentlichen XMPP-Ökosystem zu bekommen. Channelsuchmethode - Sicherungskopie + Sicherung Über Bitte aktiviere ein Konto Anrufen @@ -965,7 +965,7 @@ Einladung kann nicht gelesen werden Server unterstützt keine Generierung von Einladungen Keine aktiven Konten unterstützen diese Funktion - Das Backup wurde gestartet. Du bekommst eine Benachrichtigung sobald es fertig ist. + Die Sicherung wurde gestartet. Du bekommst eine Benachrichtigung, sobald sie fertig ist. Video kann nicht aktiviert werden. Textdokument diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index 1383e8b98..535209db8 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -132,6 +132,8 @@ Al enviar las trazas de error estás ayudando en el desarrollo Confirmar mensajes Permitir a tus contactos saber cuando has recibido y leído sus mensajes + Impedir capturas de pantalla + Ocultar el contenido de la aplicación en el selector de aplicaciones y bloquear las capturas de pantalla Pantalla OpenKeychain causó un error. Clave errónea para el cifrado. @@ -414,6 +416,7 @@ audio vídeo imagen + gráfico de vectores documento PDF Android App Contacto @@ -912,6 +915,7 @@ Conexión perdida Llamada rechazada Fallo en la aplicación + Problema de verificación Colgar Llamada saliente Video llamada saliente @@ -963,4 +967,6 @@ Ninguna cuenta activa soporta esta característica La copia de seguridad ha empezado. Recibirás una notificación cuando se haya completado. No se ha podido habilitar el vídeo. - + Documento de texto plano + + diff --git a/src/main/res/values-fi/strings.xml b/src/main/res/values-fi/strings.xml new file mode 100644 index 000000000..3fa9ad89f --- /dev/null +++ b/src/main/res/values-fi/strings.xml @@ -0,0 +1,918 @@ + + + Asetukset + Uusi keskustelu + Hallitse tilejä + Hallinnoi tiliä + Päätä keskustelu + Yhteystiedot + Ryhmäkeskustelun tiedot + Kanavan tiedot + Lisää tili + Muokkaa nimeä + Lisää yhteystietoihin + Poista yhteystietolistasta + Estä yhteystieto + Peru yhteystiedon esto + Estä verkkotunnus + Peru verkkotunnuksen esto + Estä osallistuja + Peru osallistujan esto + Hallitse tilejä + Asetukset + Aloita keskustelu + Valitse yhteystieto + Valitse yhteystiedot + Jaa tilillä + Estolista + äskettäin + minuutti sitten + %d minuuttia sitten + + %d lukematon keskustelu + + + %d lukematonta keskustelua + + + lähettää... + Puretaan viestin salausta. Odota hetki... + OpenPGP-salattu viesti + Nimimerkki on jo käytössä + Nimimerkki on virheellinen + Ylläpitäjä + Omistaja + Moderaattori + Osallistuja + Vierailija + Poistetaanko %s yhteystiedoistasi? Keskustelujasi hänen kanssaan ei poisteta. + Estetäänkö %s lähettämästä viestejä sinulle? + Perutaanko %s:n esto lähettää viestejä sinulle? + Estetäänkö kaikki yhteydet verkkotunnuksesta %s? + Perutaanko kaikkien verkkotunnuksen %s käyttäjien esto? + Yhteystieto estetty + Estetty + Poistetaanko %s kirjanmerkeistä? Mitään keskustelujasi sen kanssa ei poisteta. + Rekisteröi uusi tili palvelimella + Vaihda salasanaa palvelimelle + Aloita keskustelu + Kutsu yhteystieto + Kutsu + Yhteystiedot + Yhteystieto + Peruuta + Aseta + Lisää + Muokkaa + Poista + Estä + Peruuta esto + Tallenna + OK + %1$s kaatui + Virheenkorjaustietojen lähettäminen XMPP-tililläsi helpottaa %1$s:n kehitystyötä. + Lähetä nyt + Älä koskaan pyydä lähettämään + Tiliin ei saatu yhteyttä + Useisiin tileihin ei saatu yhteyttä + Napauta hallinnoidaksesi tilejä + Liitä tiedosto + Lisätäänkö tämä puuttuva yhteystieto listaasi? + Lisää yhteystieto + toimitus epäonnistui + Valmistaudutaan lähettämään kuva + Valmistaudutaan lähettämään kuvat + Jaetaan tiedostoja. Odota hetki... + Pyyhi historia + Pyyhi keskusteluhistoria + Poistetaanko kaikki keskustelun viestit?\n\nVaroitus: Muilla laitteilla tai palvelimilla säilytettyjä kopioita ei poisteta. + Poista tiedosto + Haluatko varmasti poistaa tämän tiedoston?\n\nVaroitus: Muilla laitteilla tai palvelimilla olevia kopioita ei poisteta. + Päätä keskustelu myös + Valitse laite + Lähetä salaamaton viesti + Lähetä viesti + Lähetä viesti henkilölle %s + Lähetä OMEMO-salattu viesti + Lähetä v\\OMEMO-salattu viesti + Lähetä OpenPGP-salattu viesti + Uusi nimimerkki on jo varattu + Lähetä salaamaton + Salauksen purku epäonnistui. Sinulle ei varmaan ole oikeaa salaista avainta. + OpenKeychain + OpenKeychainia viestien salaamiseeen ja salauksen purkamiseen, sekä julkisten avaintesi hallinointiin.

Se on GPLv3+-lisensoitu ja saatavilla F-Droidista sekä Google Playsta.

(Käynnistä %1$s uudelleen asennettuasi sovelluksen.)]]>
+ Käynnistä uudelleen + Asenna + Asenna OpenKeychain + tarjotaan... + odotetaan... + OpenPGP-avainta ei löydy + Viestin salaaminen ei onnistu koska vastaanottaja ei mainosta julkista avaintaan.\n\nPyydä kontaktiasi ottamaan OpenPGP käyttöön. + OpenPGP-avaimia ei löydy + Viestin salaaminen ei onnistu koska kontaktisi eivät mainosta julkisia avaimiaan.\n\nPyydä heitä ottamaan OpenPGP käyttöön. + Yleinen + Lataa tiedostot + Lataa automaattisesti tiedostot jotka ovat pienempiä kuin... + Liitteet + Ilmoitus + Värinä + Värise uuden viestin saapuessa + LED-ilmoitus + Vilkuta ilmoitusvaloa vastaanotettuasi uuden viestin + Soittoääni + Ilmoitusääni + Uusien viestien ilmoitusääni + Soittoääni saamillesi puheluille + Rauhanaika + Kuinka pitkäksi aikaa ilmoitukset hiljennetään kun jollain toisella laitteillasi tehdään jotain. + Edistyneet + Älä koskaan lähetä vikailmoituksia + Vikailmoituksia lähettämällä autat kehitystyötä + Lukukuittaus + Ilmoita lähettäjälle kun olet vastaanottanut ja lukenut viestin + Estä kuvankaappaukset + Piilota sovelluksen sisältö sovellusvaihtajassa ja estä ruutukaappaukset + Käyttöliittymä + OpenKeychain-virhe + Avain ei kelpaa salaamiseen. + Hyväksy + Virhe tapahtui + Virhe + Tilisi + Lähetä tilapäivityksiä + Vastaanota tilapäivityksiä + Pyydä tilapäivityksiä + Valitse kuva + Ota kuva + Alustavasti hyväksy liittymispyynnöt + Valitsemasi tiedosto ei ole kuva + Kuvatiedoston pakkaaminen epäonnistui + Tiedostoa ei löytynyt + Yleinen I/O-virhe. Ehkä vapaa tallennustila loppui kesken? + Kuvan valitsemiseen käyttämäsi sovellus ei myöntänyt riittävästi oikeuksia kuvan lukemiseen.\n\nKäytä toista tiedostonhallintaohjelmaa kuvan valitsemiseen. + Jakamiseen käyttämäsi sovellus ei myöntänyt riittävästi oikeuksia. + Tuntematon + Väliaikaisesti poistettu käytöstä + Paikalla + Yhdistäää\u2026 + Poissa + Ei sallittu + Palvelinta ei löydy + Ei yhteyttä + Rekisteröinti epäonnistui + Käyttäjänimi on varattu + Rekisteröinti valmis + Palvelin ei tue rekisteröintiä + Viallinen rekisteröintitunnus + TLS-kättely epäonnistui + Verkkotunnuksen varmentaminen epäonnistui + Yhteensopimaton palvelin + Salaamaton + OTR + OpenPGP + OMEMO + Poista tili + Poista käytöstä väliaikaisesti + Julkaise proofilikuva + Julkaise OpenPGP julkinen avain + Poista OpenPGP julkinen avain + Haluatko varmasti poistaa OpenPGP-avaimesi tilamainostuksistasi?\nYhteystietosi eivät voi enää lähettää sinulle OpenPGP-salattuja viestejä. + OpenPGP julkinen avain julkaistu. + Ota tunnus käyttöön + Oletko varma? + Tilin poistaminen pyyhkii koko keskusteluhistoriasi + Nauhoita ääntä + XMPP-osoite + Estä XMPP-osoite + käyttäjä@esimerkki.fi + Salasana + Tämä ei ole kunnollinen XMPP-osoite + Muisti loppui. Kuva on liian suuri. + Lisätäänkö %s osoitekirjaan? + Tietoa palvelimesta + XEP-0313: MAM + XEP-0280: Viestin kopiot + XEP-0352: Asiakkaan tilan vihjaus + XEP-0191: Estokomennot + XEP-0237: Yhteystietolistan versiointi + XEP-0198: Virran hallinta + XEP-0215: Ulkoisten palveluiden löytäminen + XEP-0163: PEP (Profiilikuvat / OMEMO) + XEP-0363: Tiedoston lähetys HTTP:llä + XEP-0357: Työntö + käytössä + ei käytössä + Julkisten avainten mainostus puuttuu + nähty juuri äsken + nähty minuutti sitten + nähty %d minuuttia sitten + nähty tunti sitten + nähty %d tuntia sitten + nähty päivä sitten + nähty %d päivää sitten + Salattu viesti. Asenna OpenKeychain purkaaksesi salauksen. + Löytyi uusi OpenPGP-salattu viesti + OpenPGP-avaimen tunniste + OMEMO-sormenjälki + v\\OMEMO-sormenjälki + OMEMO-sormenjälki (viestin lähettäjä) + v\\OMEMO-sormenjälki (viestin lähettäjä) + Muut laitteet + Luota OMEMO-sormenjälkiin + Haetaan avaimia... + Valmis + Pura salaus + Kirjanmerkit + Haku + Syötä yhteystieto + Poista yhteystieto + Näytä yhteystieto + Estä yhteystieto + Peruuta yhteystiedon esto + Luo + Valitse + Yhteystieto on jo olemassa + Liity + kanava@kokous.esimerkki.fi/nimimerkki + kanava@kokous.esimerkki.fi + Tallenna kirjanmerkkinä + Poista kirjanmerkki + Tuhoa ryhmäkeskustelu + Tuhoa kanava + Haluatko varmasti tuhota tämän ryhmäkeskustelun?\n\nVaroitus: Ryhmäkeskustelu poistetaan lopullisesti palvelimelta. + Haluatko varmasti tuhota tämän julkisen kanavan?\n\nVaroitus: Kanava poistetaan lopullisesti palvelimelta. + Ryhmäkeskustelun tuhomainen epäonnistui + Kanavan tuhoaminen epäonnistui + Muokkaa ryhmäkeskustelun aihetta + Aihe + Liitytään ryhmäkeskusteluun... + Poistu + Yhteystieto lisätty luetteloon + Lisää takaisin + %s on lukenut tähän asti + %s ovat lukeneet tähän asti + %1$s ja %2$d muuta on lukenut tähän asti + Kaikki ovat lukeneet tähän asti + Julkaise + Napauta profiilikuvaa valitaksesi kuvan galleriasta + Julkaistaan... + Palvelin hylkäsi julkaisusi + Kuvan muuntaminen epäonnistui + Profiilikuvan tallentaminen levylle epäonnistui + (Tai paina pitkään palauttaaksesi oletuksen) + Palvelin ei tue profiilikuvien julkaisua + kuiskasi + %s:lle + Lähetä yksityisviesti %s:lle + Yhdistä + Tili on jo olemassa + Seuraava + Istunto aloitettu + Ohita + Poista ilmoitukset käytöstä + Ota käyttöön + Ryhmäkeskustelu vaatii salasanan + Kirjoita salasana + Pyydä yhteystietoa ensin lähettämään tilapäivityksiä.\n\nTätä käytetään sen tunnistamiseen mitä sovellusta tämä käyttää. + Pyydä nyt + Ohita + Varoitus: Tämän lähettäminen ilman molemminpuolisia tilapäivityksiä voi aiheuttaa odottamattomia ongelmia.\n\nMene \"Yhteystiedon tietoihin\" tarkistaaksesi tilapäivitysten tilauksesi. + Turvallisuus + Salli viestien korjaaminen + Mahdollistaa muiden muokata sinulle lähettämiään viestejä jälkikäteen + Edistyneet asetukset + Ole varovainen näiden kanssa + Tietoa %s + Hiljaisuus + Alku + Loppu + Ota käyttöön hiljaisuus + Ilmoitukset vaimennetaan hiljaisuuden aikana + Muut + Synkronoi kirjanmerkkien kanssa + Liity ryhmään automaattisesti jos se on kirjanmerkeissäsi + OMEMO-sormenjälki kopioitu leikepöydälle + Sinut on estetty tästä ryhmäkeskustelusta + Tämä ryhmäkeskustelu on vain jäsenille + Resurssin rajallisuus + Sinut on poistettu tästä ryhmästä + Ryhmäkeskustelu on suljettu + Et ole enää tässä ryhmäkeskustelussa + käytetään tiliä %s + palvelimella %s + Tarkistetaan %s HTTP-palvelimella + Et ole yhteydessä. Yritä myöhemmin uudelleen + Tarkista %s:n koko + Tarkista %1$s:n koko palvelimella %2$s + Toiminnot + Lainaa + Liitä lainauksena + Kopio alkuperäinen URL + Lähetä uudestaan + Tiedoston URL + URL kopioitu leikepöydälle + XMPP-osoite kopioitu leikepöydälle + Vikailmoitus kopioitu leikepöydälle + web-osoite + Lue 2D-viivakoodi + Näytä 2D-viivakoodi + Näytä estolista + Tilitiedot + Vahvista + Yritä uudelleen + Palvelu etualalla + Estää käyttöjärjestelmää katkaisemasta yhteyttäsi + Tee varmuuskopio + Varmuuskopioiden säilytyspaikka: %s + Tehdään varmuuskopiota + Varmuuskopiosi on luotu + Varmuuskopiotiedostot tallennettu kansioon %s + Palautetaan varmuuskopiota + Varmuuskopiosi on palautettu + Älä unohda ottaa tiliä käyttöön. + Valitse tiedosto + Vastaanotetaan %1$s (%2$d%% valmis) + Lataa %s + Poista %s + tiedosto + Avaa %s + Lähetetään (%1$d%% valmis) + Valmistellaan tiedoston lähettämistä + %s tarjottu ladattavaksi + Peru siirto + tiedoston jako epäonnistui + tiedoston siirto peruttu + Tiedosto poistettu + Tiedoston avaamiseen sopivaa sovellusta ei löytynyt + Linkin avaamiseen sopivaa sovellusta ei löytynyt + Yhteystiedon näyttämiseen sopivaa sovellusta ei löytynyt + Dynaamiset tunnisteet + Näytä tunnisteet yhteystiedon alla (vain luku) + Näytä ilmoitukset + Ryhmäkeskustelupalvelinta ei löytynyt + Ryhmäkeskustelun luominen epäonnistui + Tilin profiilikuva + Kopioi OMEMO-sormenjälki leikepöydälle + Uusi OMEMO-avain + Poista laitteet + Haluatko poistaa kaikki muut laitteet OMEMO-mainoksistasi? Kun laite seuraavan kerran yhdistää, se lisää itsensä, mutta ennen sitä lähetettyjä viestejä et välttämättä saa. + Jokin meni pieleen + Haetaan historiaa palvelimelta + Palvelimella ei ollut enempää historiaa + Päivitetään... + Salasana vaihdettu! + Salasanan vaihto epäonnistui + Vaihda salasana + Nykyinen salasana + Uusi salasana + Salasana ei voi olla tyhjä + Kaikki tilit käyttöön + Kaikki tilit pois käytöstä + Poissa + Hylkiö + Jäsen + Edistynyt tila + Myönnä jäsenyys + Peru jäsenyys + Myönnä ylläpitäjän oikeudet + Peru ylläpitäjän oikeudet + Myönnä omistajuus + Peru omistajuus + Poista ryhmäkeskustelusta + Poista kanavalta + Estä ryhmäkeskustelusta + Estä kanavalta + Olet poistamassa %s:n julkiselta kanavalta. Ainoa tapa tehdä se on estää kyseinen käyttäjä ikuisesti. + Estä nyt + %s:n roolin muuttaminen epäonnistui + Yksityisen ryhmäkeskustelun asetukset + Julkisen kanavan asetukset + Yksityinen, vain jäsenille + Tee XMPP-osoitteista kaikille näkyvät + Tee kanavasta moderoitu + Et osallistu + Ryhmäkeskustelun asetuksia muutettu! + Ryhmäkeskustelun asetuksia ei voitu muuttaa + Ei koskaan + Kunnes toisin mainitaan + Torkku + Vastaa + Merkitse luetuksi + Syöttö + Enter lähettää + Käytä Enteriä viestin lähetämiseen. Ctrl+Enter lähettää viestin joka tapauksessa. + Näytä enter-nappi + Vaihtaa emojinapin enteriksi + ääni + video + kuva + vektorigrafiikka + PDF-asiakirja + Android-sovellus + Yhteystieto + Profiilikuva julkaistu! + Lähetetään %s + Tarjotaan %s + Piilota poissaolevat + %s kirjoittaa... + %s lopetti kirjoittamisen + %s kirjoittavat... + %s lopettivat kirjoittamisen + Kirjoitusilmoitukset + Näyttää ytheystiedoillesi kun kirjoitat heille viestiä + Lähetä sijainti + Näytä sijainti + Sijainnin näyttämiseen sopivaa sovellusta ei löytynyt + Sijainti + Keskustelu suljettu + Poistuit yksityisestä ryhmäkeskustelusta + Poistuit julkisesta kanavasta + Älä luota varmenteiden myöntäjiin + Kaikki varmenteet täytyy hyväksyä käsin + Poista varmenteet + Poista käsin hyväksytyt varmenteet + Ei käsin hyväksyttyjä varmenteita + Poista varmenteet + Peruuta + + %d varmenne poistettiin + %d varmennetta poistettiin + + Korvaa \"Lähetä\"-nappi pikatoiminnolla + Pikatoiminto + Ei mikään + Viimeksi käytetty + Valitse pikatoiminto + Lähetä yksityisviesti + %1$s poistui ryhmäkeskustelusta + Käyttäjänimi + Käyttäjänimi + Lataus epäonnistui: Palvelinta ei löydy + Lataus epäonnistui: Tiedostoa ei löytynyt + Lataus epöonnistui: Isäntään ei saatu yhteyttä + Lataus epäonnistui: Tiedoston tallennus epäonnistui + Tor-verkkoa ei saavutettu + Palvelin ei vastaa tästä verkkotunnuksesta + Rikki + Saatavuus + Poissa kun laite on lukittu + Näytä minut poissaolevana kun näyttö on lukittu + Kiireinen kun laite on äänetön + Näytä minut kiireisenä kun laite on äänettömänäq + Kohtele vain värinä -tilaa äänettömän lailla + Näytä minut kiireisenä kun laite on vain värinä -tilassa + Laajemmat yhteysasetukset + Näytä isäntänimen ja portin valinta tiliä lisätessä + xmpp.esimerkki.fi + Kirjaudu varmenteella + Varmenteen jäsennys epäonnistui + Arkistointiasetukset + Palvelimen arkitsointiasetukset + Kysytään arkistointiasetuksia. Odota hetki... + Arkistointiasetusten haku epäonnistui + CAPTCHA vaaditaan + Kirjoita ylläolevassa kuvassa näkyvä teksti + Varmenneketju ei ole luotettu + XMPP-osoite on eri kuin varmenteessa + Uusi varmenne + OMEMO-avaimen lataus epäonnistui! + OMEMO-avain vahvistettu varmenteella! + Laitteesi ei tue käyttäjän varmenteen valitsemista! + Yhteys + Käytä Tor-verkkoa + Tunneloi kaikki yhteydet Tor-verkon kautta. Vaatii Orbot-sovelluksen + Isäntänimi + Portti + Palvelin- tai .onion-osoite + Porttinumero on virheellinen + Isäntänimi on virheellinen + %1$d tunnusta %2$d:sta yhdistetty + + %d viesti + %d viestiä + + Lataa lisää viestejä + Tiedosto jaettu %s:n kanssa + Kuva jaettu %s:n kanssa + Kuvat jaettu %s:n kanssa + Salli %1$s:n käyttää ulkoista tallennustilaa + Salli %1$s:n käyttää kameraa + Synkronoi yhteystietojen kanssa + %1$s haluaa pääsyn osoitekirjaasi yhdistääkseen sen XMPP-yhteystietojesi kanssa.\nTämä näyttää yhteystietojesi koko nimen ja kuvan.\n\n%1$s pelkästään lukee osoitekirjaasi ja vertailee niitä paikallisesti, lähettämättä mitään palvelimelle. + Ilmoita kaikista uusista viesteistä + Ilmoita vain kun minut mainitaan + Ilmoitukset pois käytöstä + Ilmoitukset keskeytetty + Kuvan pakkaus + Vinkki: Käytä \'Lähetä tiedosto\':a \'Lähetä kuva\':n sijaan lähettääksesi kuvan pakkaamattomana sillä kertaa tästä asetuksesta huolimatta. + Aina + Vain isot kuvat + Akun käytön optimointi käytössä + Poista käytöstä + Valittu alue on liian suuri + (Ei aktivoituja tilejä) + Tämä kenttä on pakollinen + Korjaa viestiä + Lähetä korjattu viesti + Olet jo varmistanut tämän henkilön OMEMO-sormenjäljen turvallisesti luottamuksen varmistamikseksi. Hyväksymällä varmistat vain että %s on osa tätä ryhmäkeskustelua. + Olet poistanut tämän tilin käytöstä + URI:n jakamiseen sopivaa sovellusta ei löytynyt + Jaa URI sovelluksella... + Hyväksy ja jatka + XMPP-osoitteesi tulee olemaan kokonaisuudessaan: %s + Luo tunnus + Käytän itse valitsemaani palveluntarjoajaa + Valitse käyttäjänimi + Hallitse saatavuutta käsin + Valitse saatavuutesi itse muokatessasi tilaviestiäsi. + Tila + Vapaa juttelemaan + Paikalla + Poissa + Ei saatavilla + Kiireellinen + Turvallinen salasana on luotu + Laitteesi ei tue akun kulutuksen optimoinnin ohittamista + Rekisteröinti epäonnistui: Yritä myöhemmin uudelleen + Rekisteröinti epäonnistui: Salasana on liian heikko + Valitse osallistujat + Luodaan ryhmää... + Kutsu uudestaan + Poista käytöstä + Lyhyt + Keskipitkä + Pitkä + Kertoo yhteystiedoillesi milloin käytät Conversationsia + Yksityisyys + Teema + Valitse väripaletti + Automaattinen + Vaalea + Tumma + Vihreä tausta + Näytä vastaanotetut viestit vihreällä taustalla + OpenKeychainiin ei saatu yhteyttä + Laite ei ole enää käytössä + Tietokone + Puhelin + Tabletti + Selain + Pääte + Maksu vaaditaan + Salli internetin käyttö + Minä + Salli + Käyttöoikeus %s puuttuu + Etäpalvelinta ei löytynyt + Etäpalvelimen aikakatkaisu + Tiliä ei saatu muutettua + Ilmoita tämä XMPP-osoite roskapostista. + Poista OMEMO-identiteetit + Tee uudet OMEMO-avaimet. Kaikkien yhteystietojesi pitää varmistaa sinut uudestaan. Käytä tätä ainoastaan viimeisenä oljenkortena. + Poista valitut avaimet + Yhteys vaaditaan profiilikuvan julkaisemista varten. + Näytä virheilmoitus + Virheilmoitus + Datansäästö käytössä + Käyttöjärjestelmäsi estää %1$s:tä käyttämästä nettiä ollessaan taustalla. Vastaanottaaksesi ilmoitukset uusista viesteistä, salli %1$s:n käyttää esteettä verkkoa datansäästön ollessa käytössä. %1$s tekee silti parhaansa käyttääkseen mahdollisimman vähän dataa. + Laitteesi ei tue datansäästön poistamista käytöstä sovellukselle %1$s. + Väliaikaisen tiedoston luominen epäonnistui + Laite on varmennettu + Kopioi sormenjälki + Olet varmentanut kaikki hallussasi olevat OMEMO-avaimet + Viivakoodi ei sisällä tähän keskusteluun liittyviä sormenjälkiä. + Varmennetut sormenjäljet + Käytä kameraa yhteystietosi viivakoodin lukemiseen + Odota hetki, avaimia ladataan + Jaa viivakoodina + Jaa XMPP-osoitteena + Jaa HTTP-linkkinä + Sokea luottamus ennen varmistusta + Luota uusiin laitteisiin varmistamattomilta yhteystiedoilta, mutta vaadi varmistettujen yhteystietojen uusien laitteiden manuaalinen hyväksyminen. + OMEMO-avaimiin luotetaan sokeasti, eli ne voivat olla jonkun muun tai joku voi salakuunnella. + Ei luotettu + Viallinen 2D-viivakoodi + Tyhjennä välimuisti (kamerasovelluksen käyttämä) + Tyhjennä välimuisti + Siivoa yksityinen tallennustila + Tyhjennä tallennustila jossa tiedostoja säilytetään (Ne voi ladata uudestaan palvelimelta) + Seurasin tätä linkkiä luotettavasta lähteestä + Olet varmistamassa yhteystiedon %1$s OMEMO-avaimia linkin avaamalla. Tämä on turvallista ainoastaan jos löysit linkin luotettavasta paikasta jossa vain %2$s on sen kyennyt julkaisemaan. + Varmista OMEMO-avaimet + + %d sekunti + %d sekuntia + + + %d minuutti + %d minuuttia + + + %d tunti + %d tuntia + + + %d päivä + %d päivää + + + %d viikko + %d viikkoa + + + %d kuukausi + %d kuukautta + + Viestien automaattinen poisto + Poista tältä laitteelta automaattisesti valittua vanhemmat viestit. + Salataan viestiä + Pakataan videota + Yhteystieto estettiin. + Ilmoitukset tuntemattomila + Ilmoita tuntemattomien henkilöiden viesteistä ja puheluista. + Sait viestin tuntemattomalta henkilöltä + Estä tuntematon + Estä koko verkkotunnus + paikalla juuri nyt + Yritä salauksen purkua uudestaan + Istuntovirhe + Alennettu SASL-mekanismi + Palvelin vaatii tekemään rekisteröinnin verkkosivulla + Avaa verkkosivu + Verkkosivun avaamiseen sopivaa sovellusta ei löytynyt + Tänään + Eilen + Varmenna verkkotunnus DNSSEC:llä + Palvelinvarmenteet jotka sisältävät varmennetun verkkotunnukset hyväksytään + Varmenne ei sisällä XMPP-osoitetta + osittain + Nauhoita video + Kopioi leikepöydälle + Viesti kopioitu leikepöydälle + Viesti + Yksityisviestit on poistettu käytöstä + Suojatut sovellukset + Saadaksesi ilmoituksia silloinkin kun näyttö on sammutettu, Conversations pitää lisätä suojattujen sovellusten luetteloon. + Hyväksytäänkö tuntematon varmenne? + Palvelimen varmenne ei ole luotetun myöntäjän allekirjoittama. + Hyväksytäänkö eriävä palvelimen nimi? + Palvelin ei voinut tunnistautua olevansa verkkotunnusta \"%s\". Varmenne sisältää vain seuraavat verkkotunnukset: + Haluatko yhdistää joka tapauksessa? + Varmenteen tiedot: + Kerran + QR-koodilukija tarvitsee luvan käyttää kameraa + Vieritä alas asti + Vieritä alas veistin lähetyksen jälkeen + Vaihda tila + Muokkaa tilaa + Poista salaus käytöstä + %1$s ei voi lähettää salattuja viestejä %2$s:lle. Se voi johtua siitä että vastaanottajan palvelin on vanhentunut tai hänen käyttämänsä sovellus ei tue OMEMO:a. + Laiteluettelon lataus epäonnistui + Salausavainten lataus epäonnistui + Vinkki: Jossain tapauksissa tämä ratkeaa kun molemmat osapuolet lisäävät toisena yhteystietoihinsa. + Haluatko varmasti poistaa OMEMO-salauksen käytöstä tässä keskustelussa?\nPalvelimen ylläpitäjä voi tällöin lukea viestinne, mutta se voi olla ainoa mahdollinen tapa keskustella vanhentunutta sovellusta käyttävän henkilön kanssa. + Poista käytöstä nyt + Luonnos: + OMEMO-salaus + OMEMO:a ei koskaan käytetä oletuksena uusissa keskusteluissa. + Luo pikakuvake + Kirjasinkoko + Kirjasimen suhteellinen koko sovelluksen sisällä. + Käytössä oletuksena + Oletuksena pois käytöstä + Pieni + Keksikokoinen + Suuri + Viestiä ei salattu tälle laitteelle + OMEMO-salatun viestin purku epäonnistui + peru + Sijainnin jakaminen on pois käytöstä + Kopioi sijainti + Jaa sijainti + Reittiohjeet + Jaa sijainti + Näytä sijainti + Jaa + Odota hetki... + Salli %1$s:n käyttää mikrofonia + GIF + Näytä keskustelu + Sijainnin jako -lisäosa + Käytä lisäosaa sisäänrakennetun kartan sijaan + Kopioi web-osoite + Kopioi XMPP-osoite + Tiedostonjako HTTP:llä S3:een + \'Aloita keskustelu\' -näytöllä avaa näppäimistö ja siirrä kursori hakukenttään + Ryhmän kuvake + Isäntäpalvelin ei tue ryhmäkeskustelun kuvakkeita + Vain omistaja voi vaihtaa kuvakkeen + Yhteystiedon nimi + Nimimerkki + Nimi + Nimen antaminen on vapaaehtoista + Ryhmäkeskustelun nimi + Ryhmäkeskustelu on tuhottu + Tallennetta ei voitu tallentaa + Edustapalvelu + Tilatieto + Yhteysongelmat + Viestit + Puhelut + Viestit + Saapuvat puhelut + Toimitusvirheet + Tärkeys, ääni, värinä + Videonpakkaus + Näytä media + Osallistujat + Mediaselain + Videon laatu + Heikompi laatu tarkoittaa pienempiä tiedostoja + Keski (360p) + Korkea (720p) + peruutettu + Olet jo aloittanut viestin luonnostelun + Ominaisuutta ei ole toteutettu + Virheellinen maakoodi + Valitse maa + puhelinnumero + Vahvista puhelinnumerosi + Quicksy lähettää viestin sinulle varmistaaksesi puhelinnumerosi. Operaattorisi saattaa laskuttaa siitä. Syötä maakoodi ja puhelinnumero: +
%s

Onko se oikein, vai haluaisitko muuttaa numeroa?]]>
+ %s ei ole kelvollinen puhelinnumero. + Syötä puhelinnumerosi. + Varmista %s + %s.]]> + Lähetimme sinulle uuden viestin 6-numeroisella koodilla. + Kirjoita 6-numeroinen PIN-koodi alapuolelle. + Lähetä uusi tekstiviesti + Lähetä uusi tekstivieti (%s) + Odota (%s) + takaisin + Mahdollinen PIN-koodi liitettiin leikepöydältä automaattisesti + Syötä 6-numeroinen PIN-koodisi. + Haluatko varmasti perua rekisteröintiprosessin? + Kyllä + Ei + Varmistetaan... + Pyydetään tekstiviestiä + Syöttämäsi PIN-koodi on väärä. + Lähettämämme PIN-koodi on vanhentunut. + Tuntematon verkkovirhe. + Tuntematon vastaus palvelimelta. + Palvelimeen ei saatu yhteyttä. + Turvallinen yhteys epäonnistui. + Palvelinta ei löytynyt. + Pyyntösi käsittelyssä tapahtui jokin virhe. + Ei verkkoyhteyttä + Odota %s ja yritä uudelleen + Liian monta yritystä + Käytät vanhentunutta versiota tästä sovelluksesta. + Päivitä + Puhelinnumerolla on tällä hetkellä kirjauduttu sisään toiselle laitteelle. + Nimesi + Kirjoita nimesi + Napauta muokkaa-nappia valitaksesi nimesi. + Hylkää pyyntö + Asenna Orbot + Käynnistä Orbot + Sovelluskauppaa ei löydy. + Tämä kanava julkaisee XMPP-osoitteesi + e-kirja + Alkuperäinen (pakkaamaton) + Avaa sovelluksella... + Conversations-profiilikuva + Valitse tili + Palauta varmuuskopiosta + Palauta + Syötä salasanasi tilille %s palauttaaksesi varmuuskopion. + Älä käytä varmuuskopion palautusta asennuksen kloonaamiseksi (käyttääksesi yhtä aikaa toisella laitteella). Varmuuskopion palautus on tarkoitettu ainoiastaan laitteen tai sovelluksen vaihtamiseksi tai jos olet kadottanut alkuperäisen laitteesi. + Varmuuskopion palautus epäonnistui. + Varmuuskopion salauksen purku epäonnistui. Onko salasana oikein? + Varmuuskopiointi & palautus + Syötä XMPP-osoite + Luo ryhmäkeskustelu + Liity julkiselle kanavalle + Luo yksityinen ryhmäkeskustelu + Luo julkinen kanava + Kanavan nimi + XMPP-osoite + Anna kanavalle nimi + Anna XMPP-osoite + Tämä on XMPP-osoite. Anna nimi sen sijaan. + Luodaan julkista kanavaa... + Kanava on jo olemassa + Liityit olemassa olevalle kanavalle + Kanavan asetuksia ei saatu tallennettua + Salli kenen tahansa vaihtaa aihetta + Salli kenen tahansa kutsua ryhmään + Kuka tahansa voi muokata aihetta. + Omistajat voivat muokata aihetta. + Ylläpitäjät voivat muokata aihetta. + Omistajat voivat kutsua muita. + Kuka tahansa voi kutsua muita. + XMPP-osoitteet ovat näkyvillä ylläpitäjille. + XMPP-osoiteet ovat näkyllä kaikille. + Tällä julkisella kanavalla ei ole osallistujia. Kutsu yhteystietojasi tai käytä jakopainiketta kanavan XMPP-osoitteen levittämiseen. + Tässä yksityisessä ryhmässä ei ole ketään. + Hallitse oikeuksia + Tiedosto on liian iso + Liitä + Löydä kanavia + Hae kanavia + Mahdollinen yksityisyyden loukkaus! + search.jabber.network.

Sen käyttö lähettää IP-osoitteesi ja hakusanat palvelulle. Katso heidän yksityisyyskäytännöstään lisätietoa.]]>
+ Minulla on jo tili + Lisää olemassa oleva tili + Rekisteröi uusi tili + Tämä vaikuttaa verkkotunnukselta + Lisää silti + Tämä vaikuttaa kanavan osoitteelta + Jaa varmuuskopiotiedostot + Tapahtuma + Avaa varmuuskopio + Valitsemasi tiedosto ei ole Conversationsin varmuuskopio + Tämä tili on jo asennettu + Syötä tämän tilin salasana + Toiminnon suorittaminen epäonnistui + Liity julkiselle kanavalle... + Jakava sovellus ei antanut tarvittavaa lupaa lukea tiedostoa. + + jabber.network + Paikallinen palvelin + Suurimman osan käyttäjistä kannattaa valita \'jabber.network\' sillä se tarjoaa parempia ehdotuksia koko julkisesta XMPP-ekosysteemistä. + Kanavien löytötapa + Varmuuskopio + Tietoa + Ota jokin tili käyttöön + Soita puhelu + Saapuva puhelu + Saapuva videopuhelu + Yhdistetään + Yhdistetty + Hyväksytään puhelua + Päätetään puhelua + Vastaa + Hylkää + Etsitään laitteita + Soi + Kiireinen + Puhelun yhdistäminen epäonnistui + Yhteys katkesi + Puhelu peruttu + Sovelluksen virhe + Varmennusvirhe + Päätä puhelu + Puhelu kesken + Videopuhelu kesken + Poista Tor käytöstä soittaaksesi puhelun + Saapuva puhelu + Saapuva puhelu · %s + Vastaamaton puhelu · %s + Lähtevä puhelu + Lähtevä puhelu · %s + Vastaamaton puhelu + Äänipuhelu + Videopuhelu + Apua + Vaihda keskusteluun + Mikrofoni ei käytettävissä + Voit osallistua vain yhteen puheluun kerrallaan. + Takaisin keskeneräiseen puheluun + Kameran vaihtaminen epäonnistui + Kiinnitä + Irrota + GPX-reitti + Viestin korjaaminen epäonnistui + Kaikki keskustelut + Tämä keskustelu + Profiilikuvasi + %s:n profiilikuva + OMEMO-salattu + OpenPGP-salattu + Salaamaton + Poistu + Jätä viesti vastaajaan + Toista ääni + Keskeytä ääni + Lisää yhteystieto, luo tai liity ryhmäkeskusteluun, tai etsi kanava + + Näytä %1$d osallistuja + Näytä %1$d osallistujaa + + + Viestiä ei saatu toimitettua + Joitain viestejä ei saatu toimitettua + + Toimitusvirheet + Lisää vaihtoehtoja + Sovellusta ei löytynyt + Kutsu Conversationsiin + Kutsun jäsentäminen epäonnistui + Palvelin ei tue kutsujen luomista + Yksikään aktiivinen tili ei tue tätä toimintoa + Varmuuskopion teko aloitettu. Saat ilmoituksen kun se on valmis. + Videon käyttöönotto epäonnistui + Perustekstiasiakirja + +
diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index e1405feed..2c2f413d0 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -138,6 +138,8 @@ Отправляя отчеты об ошибках, вы помогаете разработке этого приложения Отчёты о получении Позволяет вашим контактам видеть, когда вы получили и прочитали их сообщения + Запретить скриншоты + Прятать содержимое приложения при переключении приложений и запретить скриншоты Интерфейс OpenKeychain вызвал ошибку. Неподходящий ключ для шифрования. @@ -420,6 +422,7 @@ аудио звук изображение + векторная графика PDF-документ Приложение Android Контакт @@ -989,4 +992,6 @@ Ни один активный аккаунт не поддерживает эту функцию Резервное копирование было начато. Вы получите уведомление, как только оно будет завершено. Невозможно включить видео. - + Текстовые данные + + diff --git a/src/main/res/values-sv/strings.xml b/src/main/res/values-sv/strings.xml index 35666eb4a..138586237 100644 --- a/src/main/res/values-sv/strings.xml +++ b/src/main/res/values-sv/strings.xml @@ -72,10 +72,12 @@ Spara Ok %1$s har kraschat + Att använda ditt XMPP-konto för att skicka in \'stack traces\' hjälper den pågående utvecklingen av %1$s. Skicka nu Fråga aldrig igen Kunde inte ansluta till konto Kunde inte ansluta till flera konton + Tryck för att hantera dina konton Bifoga fil Vill du lägga till den här saknade kontakten i din kontaktlista? Lägg till kontakt @@ -85,7 +87,9 @@ Delar filer. Vänta... Rensa historik Rensa konversationshistorik + Vill du radera alla meddelanden i den här konversationen?\n\nVarning: Det här påverkar inte meddelanden som finns lagrade på andra enheter eller servrar. Ta bort fil + Är du säker på att du vill ta bort den här filen?\n\nVarning: Den här åtgärden kommer inte att ta bort kopior av den här filen som finns lagrad på andra enheter eller servrar. Stäng denna konversation efteråt Välj enhet Skicka okrypterat meddelande @@ -98,13 +102,16 @@ Skicka okrypterat Avkryptering misslyckades. Du har kanske kanske inte rätt privat nyckel. OpenKeychain + OpenKeychain för att kryptera och avkryptera dina publika nycklar.

Programmet är licensierat under GPLv3+ och finns tillgänglig via F-Droid and Google Play.

(Var god och starta om %1$s efter installationen.)]]>
Starta om Installera Installera OpenKeychain erbjuder… väntar… Ingen OpenPGP-nyckel funnen + Det gick inte att kryptera ditt meddelande eftersom att din kontakt inte har annonserat sin publika nyckel.\n\nVänligen be din kontakt att sätta upp OpenPGP. Inga OpenPGP-nycklar funna + Det gick inte att kryptera ditt meddelande eftersom att din kontakt inte har annonserat sina publika nycklar.\n\nVänligen be din kontakt att sätta upp OpenPGP. Generellt Acceptera filer Acceptera automatiskt filer som är mindre än… @@ -119,10 +126,14 @@ Aviseringsljud för nya meddelande Ringsignal för inkommande samtal Notifieringsfrist + Tidsgräns för hur länge notiser ska tystas efter att aktivitet har upptäckts på en av dina andra enheter. Avancerat Skicka aldrig krasch-rapporter + Genom att skicka in stack traces hjälper du utvecklingen Bekräfta meddelanden Låt dina kontakter veta när du har mottagit och läst deras meddelanden + Förhindra skärmdumpar + Dölj innehållet från applikationen i applikationsväxlaren och blockera skärmdumpar Gränssnitt OpenKeychain genererade ett fel. Dålig krypterings-nyckel. @@ -140,6 +151,8 @@ Det gick inte att konvertera bildfilen Filen hittas ej Generellt I/O-fel. Du kanske fick slut på plats? + Applikationen som du använde för att välja den här bilden tillhandahöll inte tillräckligt med rättigheter för att läsa filen.\n\nVar god och använd en annan filhanterare för att välja en bild. + Applikationen du använde för att dela den här filen tillhandahöll inte tillräckligt med behörigheter. Okänd Tillfälligt inaktiverad Online @@ -152,10 +165,13 @@ Användarnamn används redan Registrering klar Registrering stöds ej av server + Ogiltigt registreringstoken TLS-förhandling misslyckades + Domänen kan inte verifieras Kränkning av policy Inkompatibel server Strömningsfel + Fel vid öppning av ström Okrypterat OTR OpenPGP @@ -166,14 +182,17 @@ Publicera OpenPGP publik nyckel Ta bort OpenPGP publik nyckel Är du säker på att du vill ta bort din OpenPGP publik nyckel från din tillgänglighetsuppdatering?\nDina kontakter kommer inte längre att kunna skicka dig OpenPGP-krypterade meddelande. + OpenPGP-nyckel har publicerats. Aktivera konto Är du säker? + Om du tar bort ditt konto raderas hela din konversationshistorik Spela in röst XMPP-adress Blockera XMPP-adress användarnamn@exempel.se Lösenord Detta är inte en giltig XMPP-adress + Slut på minne. Bilden är för stor Vill du lägga till %s i din enhets kontakter? Server-info XEP-0313: Message Archive @@ -200,6 +219,8 @@ OpenPGP-nyckel-ID OMEMO-fingeravtryck v\\OMEMO-fingeravtryck + OMEMO-fingeravtryck (meddelandets ursprung) + v\\OMEMO-fingeravtryck (meddelandets ursprung) Andra enheter Lita på OMEMO-fingeravtryck Hämtar nycklar... @@ -216,33 +237,50 @@ Välj Kontakten finns redan Gå med + rum@konferens.exempel.se/användarnamn + rum@konferens.exempel.se Spara som bokmärke Ta bort bokmärke + Förstör gruppchat + Förstör kanal + Är du säker på att du vill förstöra den här gruppchatten?\n\nVarning: Gruppchatten kommer att tas bort helt från servern. + Är du säker på att du vill förstöra den här publika chattgruppen?\n\nVarning: Den här gruppchatten kommer att tas bort helt från servern. + Det gick inte att ta bort gruppchatten + Det gick inte att ta bort kanalen + Redigera ämnet för gruppchatten Ämne Går med i gruppchatt... Lämna Kontakten lade till dig i sin kontaktlista Addera tillbaka %s har läst hit + %s har läst till den här punkten + %1$s +%2$d andra har läst till den här punkten Alla har läst fram till hit Publicera + Tryck på visningsbilden för att välja en bild från galleriet Publicerar… Servern kunde inte publicera + Det gick inte att konvertera din bild Kunde inte spara avatarbild till disk (Eller tryck länge för att få tillbaks förvald) + Din server stödjer inte publicering av visningsbilder privat meddelande till %s Skicka privat meddelande till %s Anslut Detta konto finns redan Nästa + Session etablerad Hoppa över Inaktivera notifieringar Aktivera Gruppchatten kräver lösenord Fyll i lösenord + Var god begär närvarouppdateringar från din kontakt först.\n\nDetta kommer att användas för att avgöra vilken chattapplikationen din kontakt använder. Begär nu Ignorera + Varning: Att skicka detta utan ömsesidiga närvarouppdateringar kan orsaka oväntade problem.\n\nGå till \"Kontaktuppgifter\" för att verifiera dina närvaroprenumerationer. Säkerhet Tillåt korrigeringar av meddelanden Tillåt att dina kontakter kan ändra sina meddelanden i efterhand @@ -256,9 +294,16 @@ Notifieringar kommer vara tysta under tysta timmar Annat Synkronisera med bokmärken + Gå med i gruppchattar automatiskt om bokmärket säger det + OMEMO-fingeravtryck kopierat till urklipp + Du är avstängd från denna gruppchatt + Denna gruppchatt är endast för medlemmar Resursbegränsning + Du har blivit sparkad från den här gruppchatten Gruppchatten stängdes ner + Du är inte längre med i denna gruppchatt använder konto %s + huseras hos %s Kontrollerar %s på webbserver Du är inte ansluten. Försök igen senare Kontrollera filstorleken på %s @@ -279,6 +324,7 @@ Kontodetaljer Bekräfta Försök igen + Förgrundstjänst Förehindrar operativsystemet att ta ner uppkopplingen Skapa säkerhetskopia Säkerhetskopians filer lagras i %s @@ -295,14 +341,27 @@ fil Öppna %s skickar (%1$d%% klart) + Förbereder för delning av fil %s erbjuden för nedladdning Avbryt överföring + det gick inte att dela fil + filöverföring avbruten + Fil borttagen + Ingen applikation som kunde öppna filen hittades + Ingen applikation som kunde öppna länken hittades + Ingen applikation som kunde visa kontakten hittades + Dynamiska etiketter Visa skrivskyddade taggar under kontakter Aktivera notifieringar + Ingen gruppchattserver hittades + Det gick inte att skapa gruppchatten Kontots avatarbild Kopiera OMEMO-fingeravtryck till urklipp Regenerera OMEMO-nyckel Rensa enheter + Är du säker på att du vill ta bort alla andra enheter från OMEMO-tillkännagivandet? Nästa gång dina enheter ansluter, kommer de att tillkännage sig själva igen, men de kanske inte får meddelanden som skickas under tiden. + Det finns inga användbara nycklar tillgängliga för den här kontakten.\nDet gick inte att hämta nya nycklar från servern. Kanske är det något fel på din kontakts server? + Det finns inga användbara nycklar tillgängliga för den här kontakten.\nSe till att ni båda har närvaroprenumeration. Något gick fel Hämtar historik från server Ingen mer historik på server @@ -312,6 +371,7 @@ Byt lösenord Nuvarande lösenord Nytt lösenord + Lösenord kan inte vara tomma Aktivera alla konton Deaktivera alla konton Utför åtgärden med @@ -320,26 +380,42 @@ Utstött Medlem Avancerat läge + Bevilja medlemsprivilegier + Återkalla medlemsprivilegier Bevilja administratörsbehörighet Återkalla administratörsbehörighet + Bevilja ägarprivilegier + Återkalla ägarprivilegier Ta bort från gruppchatt Ta bort från kanal Kunde inte ändra tillhörigheten för %s + Förbjud från gruppchatt + Förbjud från kanal + Du försöker ta bort %s från en offentlig kanal. Det enda sättet att göra det är att förbjuda den användaren för alltid. Bannlys nu Kunde inte ändra rollen för %s + Privat gruppchattskonfiguration + Publik kanalkonfiguration Privat, medlemsskap krävs Gör XMPP-adresser synliga för alla + Gör kanalen modererad Du deltar ej + Ändrade gruppchattalternativ! + Det gick inte att ändra alternativ för gruppchatt Aldrig Tills vidare + Snooza + Svara Läsmarkera Input Skicka med enter + Använd Enter-tangenten för att skicka meddelandet. Du kan alltid använda Ctrl+Enter för att skicka meddelandet, även om det här alternativet är inaktiverat. Visa enter-knappen Byt ut emoticons-tangenten mot en enter-tangent ljud video bild + vektorgrafik PDF-dokument Android-app Kontakt @@ -355,8 +431,11 @@ Låt dina kontakter veta när du skriver meddelande till dem Skicka position Visa position + Ingen applikation hittades för att visa platsdata Position Konversation stängd + Lämnade privat gruppchatt + Lämnade publik kanal Lita inte på systemets CAs Alla certifikat måste manuellt godkännas Ta bort certifikat @@ -369,11 +448,15 @@ %d certifikat borttaget %d certifikat borttagna + Ersätt \"Skicka\"-knappen med snabbåtgärd Snabbfunktion Ingen Senast använd Välj snabbfunktion + Sök kontakter + Sök bokmärken Skicka privat meddelande + %1$s har lämnat gruppchatten Användarnamn Användarnamn Inte ett giltigt användanamn @@ -383,16 +466,28 @@ Nerladdning gick fel: Kunde inte skriva fil Tor-nätverk ej tillgängligt Bind-fel + Den här servern ansvarar inte för den här domänen Sönder Tillgänglighet + Frånvarande när enheten är låst + Visa som frånvarande när enheten är låst + Upptagen i ljudlöst läge + Visa som Upptagen i ljudlöst läge Hantera vibrationsläge som tyst läge + Visa som Upptagen när enheten är satt på att endast vibrera Utökade anslutningsinställningar Visa val av servernamn och port vid inställning av konto xmpp.example.com + Logga in med certifikat + Det gick inte att analysera certifikatet Arkiveringsinställningar Arkiveringsinställningar på servern Hämtar arkiveringsinställningar, vänta... + Det gick inte att hämta arkiveringsinställningar + CAPTCHA behövs Skriv i texten från bilden ovan + Otillförlitlig certifikatkedja + XMPP-adressen matchar inte certifikatet Förnya certifikat Misslyckades med att hämta OMEMO-nyckel! Verifierade OMEMO-nyckel med certifikat! @@ -402,6 +497,7 @@ Tunnla alla anslutningar genom Tor-nätverket. Kräver Orbot Servernamn Port + Server- eller .onion-adress Inte ett giltigt portnummer Inte ett giltigt servernamn %1$d av %2$d konton anslutna @@ -410,20 +506,36 @@ %d meddelanden Ladda fler meddelanden + Fil delad med %s + Bild delad med %s + Bilder som delats med %s + Text som delats med %s + Ge %1$s åtkomst till extern lagring + Ge %1$s åtkomst till kameran Synkronisera med kontakter + %1$s vill ha behörighet att komma åt din adressbok för att matcha den med din XMPP-kontaktlista.\nDetta visar dina kontakters fullständiga namn och visningsbilder.\n\n%1$s kommer bara att läsa din adressbok och matcha den lokalt, utan att ladda upp något till din server. +
Vi kommer inte att lagra någon kopia av dessa telefonnummer.\n\nLäs vår integritetspolicy för mer information.

Du kommer nu bli ombedd att ge åtkomst till dina kontakter.]]>
Notifiera för alla meddelanden + Notis endast vid omnämnande Notifieringar deaktiverade Notifieringar pausade Bildkomprimering + Tips: Använd \"Välj fil\" istället för \"Välj bild\" för att skicka enskilda bilder okomprimerade, oavsett denna inställning. Alltid + Endast stora bilder Batterioptimeringar aktiverade + Din enhet använder kraftiga batterioptimeringar för %1$s, vilket kan leda till försenade aviseringar eller till och med förlust av meddelanden.\nVi rekommenderar att du inaktiverar dem. + Din enhet använder kraftiga batterioptimeringar för %1$s, vilket kan leda till försenade aviseringar eller till och med förlust av meddelanden.\nDu kommer nu att bli ombedd att inaktivera dem. Deaktivera The valda området är för stort (Inget konto aktiverat) Detta fält måste fyllas i Korrigera meddelanden Skicka korrigerat meddelande + Du har redan validerat den här personens fingeravtryck säkert för att bekräfta förtroendet. Genom att välja \"Klar\" bekräftar du bara att %s är en del av den här gruppchatten. Du har deaktiverat detta konto + Säkerhetsfel: Ogiltig filåtkomst! + Ingen applikation hittades för att dela URI Dela URI med... Din fullständiga XMPP-adress kommer att vara: %s Skapa konto @@ -450,8 +562,11 @@ Tema Välj färgschema Automatisk + Ljus + Mörk Grön bakgrund Använd grön bakgrund för mottagna meddelanden + Det gick inte att ansluta till OpenKeychain Denna enhet används inte längre Dator Mobiltelefon @@ -459,20 +574,26 @@ Webbläsare Konsoll Betalning krävs + Ge behörighet till att använda Internet Jag Kontakt ber om tillgänglighetsuppdateringar Tillåt Saknar rättigheter för access till %s Fjärrserver hittas inte + Timeout för fjärrserver Kunde inte uppdatera konto + Rapportera den här XMPP-adressen för spam. Ta bort OMEMO identiteter + Återskapa dina OMEMO-nycklar. Alla dina kontakter måste verifiera dig igen. Använd endast det här som en sista utväg. Ta bort valda nycklar Du måste vara ansluten för att publicera din avatarbild Visa felmeddelande Felmeddelande Databesparing + Det gick inte att skapa en tillfällig fil Denna enhet har verifierats Kopiera fingeravtryck + Du har verifierat alla OMEMO-nycklar i din ägo Streckkoden innehåller inte fingeravtryck för denna konversation. Verifierade fingeravtryck Använd kameran för att scanna en kontakts streckkod @@ -481,8 +602,11 @@ Dela som XMPP URI Dela som HTTP länk Blint förtroende före verifiering + Lita på nya enheter från icke-verifierade kontakter, men begär manuell bekräftelse av nya enheter för verifierade kontakter. + Att blint lita på OMEMO-nycklar, innebär att det skulle kunna vara någon annan eller att någon annan har fått åtkomst. Ej betrodd Ogiltig 2D-streckkod + Töm cache-mapp (används av kameraapplikationen) Rensa cache Rensa private lagring Rensa privat lagring där filer lagras (De kan om-laddas från servern) @@ -530,6 +654,10 @@ online just nu Försök dekryptera igen Sessionsfel + Öppna webbsida + Ingen applikation hittades för att kunna öppna webbsidan + Se upp-notifikationer + Visa se upp-notifikationer Idag Igår Bekräfta värdnamn med DNSSEC diff --git a/src/quicksy/res/values-bg/strings.xml b/src/quicksy/res/values-bg/strings.xml index 1854c3e3c..c41bf67c9 100644 --- a/src/quicksy/res/values-bg/strings.xml +++ b/src/quicksy/res/values-bg/strings.xml @@ -3,7 +3,7 @@ Времето, през което Quicksy няма да прави нищо, след като забележи дейност на друго устройство Изпращайки проследявания на стека, Вие помагате за непрекъснатото развитие на Quicksy Така всичките Ви контакти ще знаят кога използвате Quicksy - Ако искате да продължите да получавате известия дори когато екранът е заключен, трябва да добавите „Quicksy“ към списъка от защитени приложения. + Ако искате да продължите да получавате известия дори когато екранът е заключен, трябва да добавите Quicksy към списъка със защитени приложения. Профилна снимка за Quicksy Quicksy не може да се използва във Вашата страна. Идентичността на сървъра не може да бъде потвърдена. diff --git a/src/quicksy/res/values-fi/strings.xml b/src/quicksy/res/values-fi/strings.xml new file mode 100644 index 000000000..9a988a15d --- /dev/null +++ b/src/quicksy/res/values-fi/strings.xml @@ -0,0 +1,12 @@ + + + Kuinka kauan Quicksy pysyy hiljaa nähtyään toisella laitteellasi toimintaa + Lähettämällä virheenkorjaustietoja autat Quicksyn kehittäjiä + Kerro kaikille yhteystiedoillesi kun käytät Quicksya + Saadaksesi ilmoituksia silloinkin kun näyttö on sammutettu, Quicksy pitää lisätä suojattujen sovellusten luetteloon. + Quicksy-profiilikuva + Quicksy ei ole saatavilla maassasi. + Palvelimen identiteetin varmennus epäonnistui. + Tuntematon turvallisuusvirhe. + Palvelimeen yhdistäminen aikakatkaistiin. + From ecdb5af5473437ddc7d76cd5a5d620a8ea83db68 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 9 Feb 2022 12:26:39 +0100 Subject: [PATCH 035/394] bump agp version --- build.gradle | 15 ++++++++------- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index e4d037114..27777bde5 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.0.4' + classpath 'com.android.tools.build:gradle:7.1.1' } } @@ -250,9 +250,6 @@ android { buildTypes.release.signingConfig = signingConfigs.release } - lintOptions { - disable 'MissingTranslation', 'InvalidPackage','AppCompatResource' - } subprojects { @@ -267,11 +264,15 @@ android { } } - packagingOptions { - exclude 'META-INF/BCKEY.DSA' - exclude 'META-INF/BCKEY.SF' + resources { + excludes += ['META-INF/BCKEY.DSA', 'META-INF/BCKEY.SF'] + } } + lint { + disable 'MissingTranslation', 'InvalidPackage', 'AppCompatResource' + } + android.applicationVariants.all { variant -> variant.outputs.each { output -> diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 84fa7550a..162dd9b7f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip From d7f38a3e5aca544eb2275f04c5b9c8f40d327a57 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 12 Feb 2022 10:19:54 +0100 Subject: [PATCH 036/394] fix precondition for timeout handling --- .../eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 12ba35733..9ed4d188f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -1107,7 +1107,7 @@ private void handleIqErrorResponse(final IqPacket response) { } private void handleIqTimeoutResponse(final IqPacket response) { - Preconditions.checkArgument(response.getType() == IqPacket.TYPE.ERROR); + Preconditions.checkArgument(response.getType() == IqPacket.TYPE.TIMEOUT); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ timeout in RTP session with " + id.with + ". terminating with connectivity error"); if (isTerminated()) { Log.i(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring error because session was already terminated"); From cf9d6e5ca324092dde52b9838063886120284729 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 12 Feb 2022 10:20:07 +0100 Subject: [PATCH 037/394] version bump to 2.10.3-beta --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 27777bde5..557c582aa 100644 --- a/build.gradle +++ b/build.gradle @@ -92,8 +92,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 29 - versionCode 42023 - versionName "2.10.2" + versionCode 42024 + versionName "2.10.3-beta" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId From fecc34431cd321131bedce0085d3aea291170fdf Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 13 Feb 2022 10:19:06 +0100 Subject: [PATCH 038/394] bump dependencies --- build.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 557c582aa..2a070856a 100644 --- a/build.gradle +++ b/build.gradle @@ -33,15 +33,15 @@ configurations { dependencies { implementation 'androidx.viewpager:viewpager:1.0.0' - playstoreImplementation('com.google.firebase:firebase-messaging:22.0.0') { + playstoreImplementation('com.google.firebase:firebase-messaging:23.0.0') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' } conversationsPlaystoreCompatImplementation("com.android.installreferrer:installreferrer:2.2") conversationsPlaystoreSystemImplementation("com.android.installreferrer:installreferrer:2.2") - quicksyPlaystoreCompatImplementation 'com.google.android.gms:play-services-auth-api-phone:17.5.1' - quicksyPlaystoreSystemImplementation 'com.google.android.gms:play-services-auth-api-phone:17.5.1' + quicksyPlaystoreCompatImplementation 'com.google.android.gms:play-services-auth-api-phone:18.0.1' + quicksyPlaystoreSystemImplementation 'com.google.android.gms:play-services-auth-api-phone:18.0.1' implementation 'org.sufficientlysecure:openpgp-api:10.0' implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0' implementation 'androidx.appcompat:appcompat:1.3.1' @@ -73,7 +73,7 @@ dependencies { implementation "com.squareup.retrofit2:retrofit:2.9.0" implementation "com.squareup.retrofit2:converter-gson:2.9.0" - implementation "com.squareup.okhttp3:okhttp:4.9.2" + implementation "com.squareup.okhttp3:okhttp:4.9.3" implementation 'com.google.guava:guava:30.1.1-android' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.36' From 12463911f171792f17138b276c047de347feb225 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 13 Feb 2022 10:22:31 +0100 Subject: [PATCH 039/394] allow verification of own omemo keys via uri --- src/main/AndroidManifest.xml | 1 + .../persistance/FileBackend.java | 14 +++--- .../conversations/ui/EditAccountActivity.java | 43 ++++++++++++++++--- src/main/res/values/strings.xml | 2 + 4 files changed, 47 insertions(+), 13 deletions(-) diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index c46fad8b1..ff41c07c2 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -194,6 +194,7 @@ android:launchMode="singleTop" /> { + if (isTrustedSource.isChecked()) { + processFingerprintVerification(xmppUri, false); + } else { + finish(); + } + }); + builder.setNegativeButton(R.string.cancel, (dialog, which) -> finish()); + AlertDialog dialog = builder.create(); + dialog.setCanceledOnTouchOutside(false); + dialog.setOnCancelListener(d -> finish()); + dialog.show(); + } + @Override public void onNewIntent(final Intent intent) { super.onNewIntent(intent); @@ -749,7 +780,7 @@ public void onNewIntent(final Intent intent) { } @Override - public void onSaveInstanceState(final Bundle savedInstanceState) { + public void onSaveInstanceState(@NonNull final Bundle savedInstanceState) { if (mAccount != null) { savedInstanceState.putString("account", mAccount.getJid().asBareJid().toEscapedString()); savedInstanceState.putBoolean("initMode", mInitMode); diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index ff1894533..8b5e67eb2 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -622,6 +622,8 @@ Clean private storage where files are kept (They can be re-downloaded from the server) I followed this link from a trusted source You are about to verify the OMEMO keys of %1$s after clicking a link. This is only secure if you followed this link from a trusted source where only %2$s could have published this link. + You are about to verify the OMEMO keys of your own account. This is only secure if you followed this link from a trusted source where only you could have published this link. + Continue Verify OMEMO keys Show inactive Hide inactive From 364ef2543d55e6f6424fd84518ce04798f76bc79 Mon Sep 17 00:00:00 2001 From: Lockywolf Date: Fri, 11 Feb 2022 15:20:18 +0800 Subject: [PATCH 040/394] Clarify build instructions. --- README.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1696e43d3..689af33d5 100644 --- a/README.md +++ b/README.md @@ -385,20 +385,77 @@ you can get access to the the latest beta version by signing up using [this link #### How do I build Conversations -**Note:** Starting with version 2.8.0 you will need to compile libwebrtc. -[Instructions](https://webrtc.github.io/webrtc-org/native-code/android/) can be found on the WebRTC -website. Place the resulting libwebrtc.aar in the `libs/` directory. The PlayStore release currently +##### Compiling WebRTC. + +WebRTC is a standard for Internet audio and video communication. libwebrtc, also used in the Google Chrome web browser, implementing the WebRTC standard. + +**Note:** Starting with version 2.8.0 you will need to compile libwebrtc from source because there are no fresh binary releases available to download. + +[Instructions](https://webrtc.github.io/webrtc-org/native-code/android/) can be found on the WebRTC website, however, there build method used by Conversations developers is slightly different. + +``` +mkdir -p ~/Prerequisites-for-Conversations +cd ~/Prerequisites-for-Conversations +git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git +export PATH=~/Prerequisites-for-Conversations/depot_tools:$PATH +mkdir webrtc +cd webrtc +fetch --nohooks webrtc_android +# ...wait for 20Gb of stuff... +gclient sync +# ...wait for more 5Gb of stuff... +cd src +unset _JAVA_OPTS +./tools_webrtc/android/build_aar.py +``` + +It will take some time and build webrtc for all popular Android architectures. +The result will be the file `./libwebrtc.aar` + + +##### Building Conversations itself + +Place the resulting libwebrtc.aar in the `libs/` directory. The PlayStore release currently uses the stable M90 release and renamed the file name to `libwebrtc-m90.aar` put potentially you can -reference any file name by modifying `build.gradle`. +reference any file name by modifying `build.gradle`. Search for `libwebrtc-m90.aar`, and replace it with `libwebrtc.aar`. + Make sure to have ANDROID_HOME point to your Android SDK. Use the Android SDK Manager to install missing dependencies. +Alternatively (and to avoid thinking about environment variables), create a file called local.properties, in the root of the Conversations build tree, +with the following contents: + +``` +## This file must *NOT* be checked into Version Control Systems, +# as it contains information specific to your local configuration. +# +# Location of the SDK. This is only used by Gradle. +# For customization when using a Version Control System, please read the +# header note. +#Wed May 20 16:21:35 CST 2020 +ndk.dir=Path-To-Ndk +sdk.dir=Path-To-Sdk +``` + +Then issue the following commands in order to build the apk. + git clone https://github.com/inputmice/Conversations.git cd Conversations ./gradlew assembleConversationsFreeSystemDebug There are two build flavors available. *free* and *playstore*. Unless you know what you are doing you only need *free*. +You will find the apks in the `./build/outputs/apk/conversationsFreeSystem/debug/` directory. + +Be careful, the resulting apks will not install unless you delete your existing Conversations installation (which will delete all the messages from your phone, and if you have used OMEMO, you will not be able to restore them from the server). +Do it at your own risk. + +You, though, can make your own build a "test build", that can be installed alongside the normal (F-Droid or Google Play) Conversations: + +In the file `build.gradle`, find the line `applicationId "eu.siacs.conversations"` , and replace it with `applicationId "my.conversations.fork"`, also below replace "Conversations" appName with "MyCFork". +Then the resulting APK can be installed ALONGSIDE normal Conversations. And have a different name so it's not confusing + +WARNING: DO NOT REPLACE ANYTHING ELSE ANYWHERE ELSE, DO NOT REPLACE THIS PROJECT WIDE. JUST 2 strings in THAT specific file! [![Build Status](https://travis-ci.org/inputmice/Conversations.svg?branch=development)](https://travis-ci.org/inputmice/Conversations) From 2553895300bfccf9b9e593acb034e4a679feacd1 Mon Sep 17 00:00:00 2001 From: Millesimus Date: Sat, 11 Dec 2021 15:53:53 +0100 Subject: [PATCH 041/394] Fix #4249. --- .../java/eu/siacs/conversations/ui/util/QuoteHelper.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java b/src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java index 4beee8a22..cf49be767 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java +++ b/src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java @@ -30,14 +30,18 @@ public static boolean isPositionAltQuoteEndCharacter(CharSequence body, int pos) } public static boolean isPositionAltQuoteStart(CharSequence body, int pos) { - return isPositionAltQuoteCharacter(body, pos) && !isPositionFollowedByAltQuoteEnd(body, pos); + return isPositionAltQuoteCharacter(body, pos) + && isPositionPrecededByPreQuote(body, pos) + && !isPositionFollowedByAltQuoteEnd(body, pos); } public static boolean isPositionFollowedByQuoteChar(CharSequence body, int pos) { return body.length() > pos + 1 && isPositionQuoteCharacter(body, pos + 1); } - // 'Prequote' means anything we require or can accept in front of a QuoteChar + /** + * 'Prequote' means anything we require or can accept in front of a QuoteChar. + */ public static boolean isPositionPrecededByPreQuote(CharSequence body, int pos) { return UIHelper.isPositionPrecededByLineStart(body, pos); } From cdc239b040678348b26cb19a4de39d13efc9f313 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 14 Feb 2022 10:27:12 +0100 Subject: [PATCH 042/394] code clean up in TagWriter --- .../eu/siacs/conversations/xml/TagWriter.java | 215 +++++++++--------- 1 file changed, 105 insertions(+), 110 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xml/TagWriter.java b/src/main/java/eu/siacs/conversations/xml/TagWriter.java index 0e03fc1e8..4f429377a 100644 --- a/src/main/java/eu/siacs/conversations/xml/TagWriter.java +++ b/src/main/java/eu/siacs/conversations/xml/TagWriter.java @@ -14,114 +14,109 @@ public class TagWriter { - private OutputStreamWriter outputStream; - private boolean finished = false; - private final LinkedBlockingQueue writeQueue = new LinkedBlockingQueue(); - private CountDownLatch stanzaWriterCountDownLatch = null; - - private final Thread asyncStanzaWriter = new Thread() { - - @Override - public void run() { - stanzaWriterCountDownLatch = new CountDownLatch(1); - while (!isInterrupted()) { - if (finished && writeQueue.size() == 0) { - break; - } - try { - AbstractStanza output = writeQueue.take(); - outputStream.write(output.toString()); - if (writeQueue.size() == 0) { - outputStream.flush(); - } - } catch (Exception e) { - break; - } - } - stanzaWriterCountDownLatch.countDown(); - } - - }; - - public TagWriter() { - } - - public synchronized void setOutputStream(OutputStream out) throws IOException { - if (out == null) { - throw new IOException(); - } - this.outputStream = new OutputStreamWriter(out); - } - - public TagWriter beginDocument() throws IOException { - if (outputStream == null) { - throw new IOException("output stream was null"); - } - outputStream.write(""); - outputStream.flush(); - return this; - } - - public synchronized TagWriter writeTag(Tag tag) throws IOException { - if (outputStream == null) { - throw new IOException("output stream was null"); - } - outputStream.write(tag.toString()); - outputStream.flush(); - return this; - } - - public synchronized TagWriter writeElement(Element element) throws IOException { - if (outputStream == null) { - throw new IOException("output stream was null"); - } - outputStream.write(element.toString()); - outputStream.flush(); - return this; - } - - public TagWriter writeStanzaAsync(AbstractStanza stanza) { - if (finished) { - Log.d(Config.LOGTAG,"attempting to write stanza to finished TagWriter"); - return this; - } else { - if (!asyncStanzaWriter.isAlive()) { - try { - asyncStanzaWriter.start(); - } catch (IllegalThreadStateException e) { - // already started - } - } - writeQueue.add(stanza); - return this; - } - } - - public void finish() { - this.finished = true; - } - - public boolean await(long timeout, TimeUnit timeunit) throws InterruptedException { - if (stanzaWriterCountDownLatch == null) { - return true; - } else { - return stanzaWriterCountDownLatch.await(timeout, timeunit); - } - } - - public boolean isActive() { - return outputStream != null; - } - - public synchronized void forceClose() { - asyncStanzaWriter.interrupt(); - if (outputStream != null) { - try { - outputStream.close(); - } catch (IOException e) { - //ignoring - } - } - outputStream = null; - } + private OutputStreamWriter outputStream; + private boolean finished = false; + private final LinkedBlockingQueue writeQueue = new LinkedBlockingQueue(); + private CountDownLatch stanzaWriterCountDownLatch = null; + + private final Thread asyncStanzaWriter = new Thread() { + + @Override + public void run() { + stanzaWriterCountDownLatch = new CountDownLatch(1); + while (!isInterrupted()) { + if (finished && writeQueue.size() == 0) { + break; + } + try { + AbstractStanza output = writeQueue.take(); + outputStream.write(output.toString()); + if (writeQueue.size() == 0) { + outputStream.flush(); + } + } catch (Exception e) { + break; + } + } + stanzaWriterCountDownLatch.countDown(); + } + + }; + + public TagWriter() { + } + + public synchronized void setOutputStream(OutputStream out) throws IOException { + if (out == null) { + throw new IOException(); + } + this.outputStream = new OutputStreamWriter(out); + } + + public void beginDocument() throws IOException { + if (outputStream == null) { + throw new IOException("output stream was null"); + } + outputStream.write(""); + outputStream.flush(); + } + + public synchronized void writeTag(Tag tag) throws IOException { + if (outputStream == null) { + throw new IOException("output stream was null"); + } + outputStream.write(tag.toString()); + outputStream.flush(); + } + + public synchronized void writeElement(Element element) throws IOException { + if (outputStream == null) { + throw new IOException("output stream was null"); + } + outputStream.write(element.toString()); + outputStream.flush(); + } + + public void writeStanzaAsync(AbstractStanza stanza) { + if (finished) { + Log.d(Config.LOGTAG, "attempting to write stanza to finished TagWriter"); + } else { + if (!asyncStanzaWriter.isAlive()) { + try { + asyncStanzaWriter.start(); + } catch (IllegalThreadStateException e) { + // already started + } + } + writeQueue.add(stanza); + } + } + + public void finish() { + this.finished = true; + } + + public boolean await(long timeout, TimeUnit timeunit) throws InterruptedException { + if (stanzaWriterCountDownLatch == null) { + return true; + } else { + return stanzaWriterCountDownLatch.await(timeout, timeunit); + } + } + + public boolean isActive() { + return outputStream != null; + } + + public synchronized void forceClose() { + asyncStanzaWriter.interrupt(); + if (outputStream != null) { + try { + outputStream.close(); + } catch (IOException e) { + //ignoring + } + } + outputStream = null; + } } From 6bd552f6a32ca93826cb491f9b4bd757f9698227 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 14 Feb 2022 11:46:57 +0100 Subject: [PATCH 043/394] flush stanzas in batches --- .../eu/siacs/conversations/xml/TagWriter.java | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xml/TagWriter.java b/src/main/java/eu/siacs/conversations/xml/TagWriter.java index 4f429377a..2c2b8ac2c 100644 --- a/src/main/java/eu/siacs/conversations/xml/TagWriter.java +++ b/src/main/java/eu/siacs/conversations/xml/TagWriter.java @@ -8,12 +8,15 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import eu.siacs.conversations.Config; import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; public class TagWriter { + private static final int FLUSH_DELAY = 400; + private OutputStreamWriter outputStream; private boolean finished = false; private final LinkedBlockingQueue writeQueue = new LinkedBlockingQueue(); @@ -21,6 +24,8 @@ public class TagWriter { private final Thread asyncStanzaWriter = new Thread() { + private final AtomicInteger batchStanzaCount = new AtomicInteger(0); + @Override public void run() { stanzaWriterCountDownLatch = new CountDownLatch(1); @@ -29,12 +34,21 @@ public void run() { break; } try { - AbstractStanza output = writeQueue.take(); - outputStream.write(output.toString()); - if (writeQueue.size() == 0) { + final AbstractStanza stanza = writeQueue.poll(FLUSH_DELAY, TimeUnit.MILLISECONDS); + if (stanza != null) { + batchStanzaCount.incrementAndGet(); + outputStream.write(stanza.toString()); + } else { + final int batch = batchStanzaCount.getAndSet(0); + if (batch > 1) { + Log.d(Config.LOGTAG, "flushing " + batch + " stanzas"); + } outputStream.flush(); + final AbstractStanza nextStanza = writeQueue.take(); + batchStanzaCount.incrementAndGet(); + outputStream.write(nextStanza.toString()); } - } catch (Exception e) { + } catch (final Exception e) { break; } } From 60617618b819b1f427b6e639e6cbd05832e79796 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 22 Feb 2022 12:29:40 +0100 Subject: [PATCH 044/394] rename method that expand filename --- .../entities/DownloadableFile.java | 4 + .../persistance/FileBackend.java | 441 ++++++++++++------ .../conversations/ui/RecordingActivity.java | 6 +- 3 files changed, 310 insertions(+), 141 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java b/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java index c0b3512a3..072b4fd06 100644 --- a/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java +++ b/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java @@ -16,6 +16,10 @@ public class DownloadableFile extends File { private byte[] aeskey; private byte[] iv; + public DownloadableFile(final File parent, final String file) { + super(parent, file); + } + public DownloadableFile(String path) { super(path); } diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 2cef93b00..2b8f53430 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -33,6 +33,7 @@ import androidx.core.content.FileProvider; import androidx.exifinterface.media.ExifInterface; +import com.google.common.base.Strings; import com.google.common.io.ByteStreams; import java.io.ByteArrayOutputStream; @@ -76,7 +77,8 @@ public class FileBackend { private static final Object THUMBNAIL_LOCK = new Object(); - private static final SimpleDateFormat IMAGE_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); + private static final SimpleDateFormat IMAGE_DATE_FORMAT = + new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); private static final String FILE_PROVIDER = ".files"; private static final float IGNORE_PADDING = 0.15f; @@ -91,8 +93,8 @@ private static boolean isInDirectoryThatShouldNotBeScanned(Context context, File } public static boolean isInDirectoryThatShouldNotBeScanned(Context context, String path) { - for (String type : new String[]{RecordingActivity.STORAGE_DIRECTORY_TYPE_NAME, "Files"}) { - if (path.startsWith(getConversationsDirectory(context, type))) { + for (String type : new String[] {RecordingActivity.STORAGE_DIRECTORY_TYPE_NAME, "Files"}) { + if (path.startsWith(getLegacyStorageLocation(context, type).getAbsolutePath())) { return true; } } @@ -114,11 +116,14 @@ public static long getFileSize(Context context, Uri uri) { } } - public static boolean allFilesUnderSize(Context context, List attachments, long max) { - final boolean compressVideo = !AttachFileToConversationRunnable.getVideoCompression(context).equals("uncompressed"); + public static boolean allFilesUnderSize( + Context context, List attachments, long max) { + final boolean compressVideo = + !AttachFileToConversationRunnable.getVideoCompression(context) + .equals("uncompressed"); if (max <= 0) { Log.d(Config.LOGTAG, "server did not report max file size for http upload"); - return true; //exception to be compatible with HTTP Upload < v0.2 + return true; // exception to be compatible with HTTP Upload < v0.2 } for (Attachment attachment : attachments) { if (attachment.getType() != Attachment.Type.FILE) { @@ -127,33 +132,50 @@ public static boolean allFilesUnderSize(Context context, List attach String mime = attachment.getMime(); if (mime != null && mime.startsWith("video/") && compressVideo) { try { - Dimensions dimensions = FileBackend.getVideoDimensions(context, attachment.getUri()); + Dimensions dimensions = + FileBackend.getVideoDimensions(context, attachment.getUri()); if (dimensions.getMin() > 720) { - Log.d(Config.LOGTAG, "do not consider video file with min width larger than 720 for size check"); + Log.d( + Config.LOGTAG, + "do not consider video file with min width larger than 720 for size check"); continue; } } catch (NotAVideoFile notAVideoFile) { - //ignore and fall through + // ignore and fall through } } if (FileBackend.getFileSize(context, attachment.getUri()) > max) { - Log.d(Config.LOGTAG, "not all files are under " + max + " bytes. suggesting falling back to jingle"); + Log.d( + Config.LOGTAG, + "not all files are under " + + max + + " bytes. suggesting falling back to jingle"); return false; } } return true; } - public static String getConversationsDirectory(Context context, final String type) { + public static File getLegacyStorageLocation(Context context, final String type) { if (Config.ONLY_INTERNAL_STORAGE) { - return context.getFilesDir().getAbsolutePath() + "/" + type + "/"; + return new File(context.getFilesDir(), type); } else { - return getAppMediaDirectory(context) + context.getString(R.string.app_name) + " " + type + "/"; + final File appDirectory = + new File( + Environment.getExternalStorageDirectory(), + context.getString(R.string.app_name)); + final File appMediaDirectory = new File(appDirectory, "Media"); + final String locationName = + String.format("%s %s", context.getString(R.string.app_name), type); + return new File(appMediaDirectory, locationName); } } - public static String getAppMediaDirectory(Context context) { - return Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + context.getString(R.string.app_name) + "/Media/"; + private static String getAppMediaDirectory(Context context) { + return Environment.getExternalStorageDirectory().getAbsolutePath() + + "/" + + context.getString(R.string.app_name) + + "/Media/"; } public static String getBackupDirectory(Context context) { @@ -180,7 +202,8 @@ private static Bitmap rotate(final Bitmap bitmap, final int degree) { } public static boolean isPathBlacklisted(String path) { - final String androidDataPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/Android/data/"; + final String androidDataPath = + Environment.getExternalStorageDirectory().getAbsolutePath() + "/Android/data/"; return path.startsWith(androidDataPath); } @@ -193,7 +216,8 @@ private static Paint createAntiAliasingPaint() { } private static String getTakePhotoPath() { - return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + "/Camera/"; + return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + + "/Camera/"; } public static Uri getUriForUri(Context context, Uri uri) { @@ -246,7 +270,6 @@ private static int calcSampleSize(File image, int size) { return calcSampleSize(options, size); } - private static int calcSampleSize(BitmapFactory.Options options, int size) { int height = options.outHeight; int width = options.outWidth; @@ -256,8 +279,7 @@ private static int calcSampleSize(BitmapFactory.Options options, int size) { int halfHeight = height / 2; int halfWidth = width / 2; - while ((halfHeight / inSampleSize) > size - && (halfWidth / inSampleSize) > size) { + while ((halfHeight / inSampleSize) > size && (halfWidth / inSampleSize) > size) { inSampleSize *= 2; } } @@ -274,7 +296,8 @@ private static Dimensions getVideoDimensions(Context context, Uri uri) throws No return getVideoDimensions(mediaMetadataRetriever); } - private static Dimensions getVideoDimensionsOfFrame(MediaMetadataRetriever mediaMetadataRetriever) { + private static Dimensions getVideoDimensionsOfFrame( + MediaMetadataRetriever mediaMetadataRetriever) { Bitmap bitmap = null; try { bitmap = mediaMetadataRetriever.getFrameAtTime(); @@ -288,8 +311,10 @@ private static Dimensions getVideoDimensionsOfFrame(MediaMetadataRetriever media } } - private static Dimensions getVideoDimensions(MediaMetadataRetriever metadataRetriever) throws NotAVideoFile { - String hasVideo = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO); + private static Dimensions getVideoDimensions(MediaMetadataRetriever metadataRetriever) + throws NotAVideoFile { + String hasVideo = + metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO); if (hasVideo == null) { throw new NotAVideoFile(); } @@ -301,14 +326,18 @@ private static Dimensions getVideoDimensions(MediaMetadataRetriever metadataRetr boolean rotated = rotation == 90 || rotation == 270; int height; try { - String h = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT); + String h = + metadataRetriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT); height = Integer.parseInt(h); } catch (Exception e) { height = -1; } int width; try { - String w = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH); + String w = + metadataRetriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH); width = Integer.parseInt(w); } catch (Exception e) { width = -1; @@ -319,7 +348,9 @@ private static Dimensions getVideoDimensions(MediaMetadataRetriever metadataRetr } private static int extractRotationFromMediaRetriever(MediaMetadataRetriever metadataRetriever) { - String r = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION); + String r = + metadataRetriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION); try { return Integer.parseInt(r); } catch (Exception e) { @@ -368,9 +399,8 @@ public static boolean weOwnFile(Context context, Uri uri) { } /** - * This is more than hacky but probably way better than doing nothing - * Further 'optimizations' might contain to get the parents of CacheDir and NoBackupDir - * and check against those as well + * This is more than hacky but probably way better than doing nothing Further 'optimizations' + * might contain to get the parents of CacheDir and NoBackupDir and check against those as well */ private static boolean fileIsInFilesDir(Context context, Uri uri) { try { @@ -386,7 +416,9 @@ private static boolean fileIsInFilesDir(Context context, Uri uri) { private static boolean weOwnFileLollipop(Uri uri) { try { File file = new File(uri.getPath()); - FileDescriptor fd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY).getFileDescriptor(); + FileDescriptor fd = + ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY) + .getFileDescriptor(); StructStat st = Os.fstat(fd); return st.st_uid == android.os.Process.myUid(); } catch (FileNotFoundException e) { @@ -400,18 +432,22 @@ public static Uri getMediaUri(Context context, File file) { final String filePath = file.getAbsolutePath(); final Cursor cursor; try { - cursor = context.getContentResolver().query( - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - new String[]{MediaStore.Images.Media._ID}, - MediaStore.Images.Media.DATA + "=? ", - new String[]{filePath}, null); + cursor = + context.getContentResolver() + .query( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + new String[] {MediaStore.Images.Media._ID}, + MediaStore.Images.Media.DATA + "=? ", + new String[] {filePath}, + null); } catch (SecurityException e) { return null; } if (cursor != null && cursor.moveToFirst()) { final int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID)); cursor.close(); - return Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, String.valueOf(id)); + return Uri.withAppendedPath( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, String.valueOf(id)); } else { return null; } @@ -433,15 +469,30 @@ public Bitmap getPreviewForUri(Attachment attachment, int size, boolean cacheOnl final String mime = attachment.getMime(); if ("application/pdf".equals(mime) && Compatibility.runsTwentyOne()) { bitmap = cropCenterSquarePdf(attachment.getUri(), size); - drawOverlay(bitmap, paintOverlayBlackPdf(bitmap) ? R.drawable.open_pdf_black : R.drawable.open_pdf_white, 0.75f); + drawOverlay( + bitmap, + paintOverlayBlackPdf(bitmap) + ? R.drawable.open_pdf_black + : R.drawable.open_pdf_white, + 0.75f); } else if (mime != null && mime.startsWith("video/")) { bitmap = cropCenterSquareVideo(attachment.getUri(), size); - drawOverlay(bitmap, paintOverlayBlack(bitmap) ? R.drawable.play_video_black : R.drawable.play_video_white, 0.75f); + drawOverlay( + bitmap, + paintOverlayBlack(bitmap) + ? R.drawable.play_video_black + : R.drawable.play_video_white, + 0.75f); } else { bitmap = cropCenterSquare(attachment.getUri(), size); if (bitmap != null && "image/gif".equals(mime)) { Bitmap withGifOverlay = bitmap.copy(Bitmap.Config.ARGB_8888, true); - drawOverlay(withGifOverlay, paintOverlayBlack(withGifOverlay) ? R.drawable.play_gif_black : R.drawable.play_gif_white, 1.0f); + drawOverlay( + withGifOverlay, + paintOverlayBlack(withGifOverlay) + ? R.drawable.play_gif_black + : R.drawable.play_gif_white, + 1.0f); bitmap.recycle(); bitmap = withGifOverlay; } @@ -471,29 +522,32 @@ public void updateMediaScanner(File file) { public void updateMediaScanner(File file, final Runnable callback) { if (!isInDirectoryThatShouldNotBeScanned(mXmppConnectionService, file)) { - MediaScannerConnection.scanFile(mXmppConnectionService, new String[]{file.getAbsolutePath()}, null, new MediaScannerConnection.MediaScannerConnectionClient() { - @Override - public void onMediaScannerConnected() { - - } - - @Override - public void onScanCompleted(String path, Uri uri) { - if (callback != null && file.getAbsolutePath().equals(path)) { - callback.run(); - } else { - Log.d(Config.LOGTAG, "media scanner scanned wrong file"); - if (callback != null) { - callback.run(); + MediaScannerConnection.scanFile( + mXmppConnectionService, + new String[] {file.getAbsolutePath()}, + null, + new MediaScannerConnection.MediaScannerConnectionClient() { + @Override + public void onMediaScannerConnected() {} + + @Override + public void onScanCompleted(String path, Uri uri) { + if (callback != null && file.getAbsolutePath().equals(path)) { + callback.run(); + } else { + Log.d(Config.LOGTAG, "media scanner scanned wrong file"); + if (callback != null) { + callback.run(); + } + } } - } - } - }); + }); return; /*Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); intent.setData(Uri.fromFile(file)); mXmppConnectionService.sendBroadcast(intent);*/ - } else if (file.getAbsolutePath().startsWith(getAppMediaDirectory(mXmppConnectionService))) { + } else if (file.getAbsolutePath() + .startsWith(getAppMediaDirectory(mXmppConnectionService))) { createNoMedia(file.getParentFile()); } if (callback != null) { @@ -515,25 +569,30 @@ public DownloadableFile getFile(Message message) { return getFile(message, true); } - public DownloadableFile getFileForPath(String path) { - return getFileForPath(path, MimeUtils.guessMimeTypeFromExtension(MimeUtils.extractRelevantExtension(path))); + return getFileForPath( + path, + MimeUtils.guessMimeTypeFromExtension(MimeUtils.extractRelevantExtension(path))); } - public DownloadableFile getFileForPath(String path, String mime) { - final DownloadableFile file; + public DownloadableFile getFileForPath(final String path, final String mime) { if (path.startsWith("/")) { - file = new DownloadableFile(path); + return new DownloadableFile(path); } else { - if (mime != null && mime.startsWith("image/")) { - file = new DownloadableFile(getConversationsDirectory("Images") + path); - } else if (mime != null && mime.startsWith("video/")) { - file = new DownloadableFile(getConversationsDirectory("Videos") + path); - } else { - file = new DownloadableFile(getConversationsDirectory("Files") + path); - } + return getLegacyFileForFilename(path, mime); + } + } + + public DownloadableFile getLegacyFileForFilename(final String filename, final String mime) { + if (Strings.isNullOrEmpty(mime)) { + return new DownloadableFile(getLegacyStorageLocation("Files"), filename); + } else if (mime.startsWith("image/")) { + return new DownloadableFile(getLegacyStorageLocation("Images"), filename); + } else if (mime.startsWith("video/")) { + return new DownloadableFile(getLegacyStorageLocation("Videos"), filename); + } else { + return new DownloadableFile(getLegacyStorageLocation("Files"), filename); } - return file; } public boolean isInternalFile(final File file) { @@ -542,33 +601,37 @@ public boolean isInternalFile(final File file) { } public DownloadableFile getFile(Message message, boolean decrypted) { - final boolean encrypted = !decrypted - && (message.getEncryption() == Message.ENCRYPTION_PGP - || message.getEncryption() == Message.ENCRYPTION_DECRYPTED); + final boolean encrypted = + !decrypted + && (message.getEncryption() == Message.ENCRYPTION_PGP + || message.getEncryption() == Message.ENCRYPTION_DECRYPTED); String path = message.getRelativeFilePath(); if (path == null) { path = message.getUuid(); } final DownloadableFile file = getFileForPath(path, message.getMimeType()); if (encrypted) { - return new DownloadableFile(getConversationsDirectory("Files") + file.getName() + ".pgp"); + return new DownloadableFile(getLegacyStorageLocation("Files"), file.getName() + ".pgp"); } else { return file; } } public List convertToAttachments(List relativeFilePaths) { - List attachments = new ArrayList<>(); - for (DatabaseBackend.FilePath relativeFilePath : relativeFilePaths) { - final String mime = MimeUtils.guessMimeTypeFromExtension(MimeUtils.extractRelevantExtension(relativeFilePath.path)); + final List attachments = new ArrayList<>(); + for (final DatabaseBackend.FilePath relativeFilePath : relativeFilePaths) { + final String mime = + MimeUtils.guessMimeTypeFromExtension( + MimeUtils.extractRelevantExtension(relativeFilePath.path)); final File file = getFileForPath(relativeFilePath.path, mime); attachments.add(Attachment.of(relativeFilePath.uuid, file, mime)); } return attachments; } - private String getConversationsDirectory(final String type) { - return getConversationsDirectory(mXmppConnectionService, type); + // TODO remove static method. use direct instance access + private File getLegacyStorageLocation(final String type) { + return getLegacyStorageLocation(mXmppConnectionService, type); } private Bitmap resize(final Bitmap originalBitmap, int size) throws IOException { @@ -586,7 +649,8 @@ private Bitmap resize(final Bitmap originalBitmap, int size) throws IOException scalledW = size; scalledH = Math.max((int) (h / ((double) w / size)), 1); } - final Bitmap result = Bitmap.createScaledBitmap(originalBitmap, scalledW, scalledH, true); + final Bitmap result = + Bitmap.createScaledBitmap(originalBitmap, scalledW, scalledH, true); if (!originalBitmap.isRecycled()) { originalBitmap.recycle(); } @@ -603,19 +667,26 @@ public boolean useImageAsIs(final Uri uri) { } final File file = new File(path); long size = file.length(); - if (size == 0 || size >= mXmppConnectionService.getResources().getInteger(R.integer.auto_accept_filesize)) { + if (size == 0 + || size + >= mXmppConnectionService + .getResources() + .getInteger(R.integer.auto_accept_filesize)) { return false; } BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; try { - final InputStream inputStream = mXmppConnectionService.getContentResolver().openInputStream(uri); + final InputStream inputStream = + mXmppConnectionService.getContentResolver().openInputStream(uri); BitmapFactory.decodeStream(inputStream, null, options); close(inputStream); if (options.outMimeType == null || options.outHeight <= 0 || options.outWidth <= 0) { return false; } - return (options.outWidth <= Config.IMAGE_SIZE && options.outHeight <= Config.IMAGE_SIZE && options.outMimeType.contains(Config.IMAGE_FORMAT.name().toLowerCase())); + return (options.outWidth <= Config.IMAGE_SIZE + && options.outHeight <= Config.IMAGE_SIZE + && options.outMimeType.contains(Config.IMAGE_FORMAT.name().toLowerCase())); } catch (FileNotFoundException e) { Log.d(Config.LOGTAG, "unable to get image dimensions", e); return false; @@ -627,7 +698,9 @@ public String getOriginalPath(Uri uri) { } private void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyException { - Log.d(Config.LOGTAG, "copy file (" + uri.toString() + ") to private storage " + file.getAbsolutePath()); + Log.d( + Config.LOGTAG, + "copy file (" + uri.toString() + ") to private storage " + file.getAbsolutePath()); file.getParentFile().mkdirs(); try { file.createNewFile(); @@ -635,7 +708,8 @@ private void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyExcepti throw new FileCopyException(R.string.error_unable_to_create_temporary_file); } try (final OutputStream os = new FileOutputStream(file); - final InputStream is = mXmppConnectionService.getContentResolver().openInputStream(uri)) { + final InputStream is = + mXmppConnectionService.getContentResolver().openInputStream(uri)) { if (is == null) { throw new FileCopyException(R.string.error_file_not_found); } @@ -664,7 +738,8 @@ private void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyExcepti } } - public void copyFileToPrivateStorage(Message message, Uri uri, String type) throws FileCopyException { + public void copyFileToPrivateStorage(Message message, Uri uri, String type) + throws FileCopyException { String mime = MimeUtils.guessMimeTypeFromUriAndMime(mXmppConnectionService, uri, type); Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage (mime=" + mime + ")"); String extension = MimeUtils.guessExtensionFromMimeType(mime); @@ -684,7 +759,10 @@ private String getExtensionFromUri(Uri uri) { String filename = null; Cursor cursor; try { - cursor = mXmppConnectionService.getContentResolver().query(uri, projection, null, null, null); + cursor = + mXmppConnectionService + .getContentResolver() + .query(uri, projection, null, null, null); } catch (IllegalArgumentException e) { cursor = null; } @@ -709,7 +787,8 @@ private String getExtensionFromUri(Uri uri) { return pos > 0 ? filename.substring(pos + 1) : null; } - private void copyImageToPrivateStorage(File file, Uri image, int sampleSize) throws FileCopyException, ImageCompressionException { + private void copyImageToPrivateStorage(File file, Uri image, int sampleSize) + throws FileCopyException, ImageCompressionException { final File parent = file.getParentFile(); if (parent != null && parent.mkdirs()) { Log.d(Config.LOGTAG, "created parent directory"); @@ -743,7 +822,10 @@ private void copyImageToPrivateStorage(File file, Uri image, int sampleSize) thr scaledBitmap = rotate(scaledBitmap, rotation); boolean targetSizeReached = false; int quality = Config.IMAGE_QUALITY; - final int imageMaxSize = mXmppConnectionService.getResources().getInteger(R.integer.auto_accept_filesize); + final int imageMaxSize = + mXmppConnectionService + .getResources() + .getInteger(R.integer.auto_accept_filesize); while (!targetSizeReached) { os = new FileOutputStream(file); Log.d(Config.LOGTAG, "compressing image with quality " + quality); @@ -788,12 +870,19 @@ private static void cleanup(final File file) { } } - public void copyImageToPrivateStorage(File file, Uri image) throws FileCopyException, ImageCompressionException { - Log.d(Config.LOGTAG, "copy image (" + image.toString() + ") to private storage " + file.getAbsolutePath()); + public void copyImageToPrivateStorage(File file, Uri image) + throws FileCopyException, ImageCompressionException { + Log.d( + Config.LOGTAG, + "copy image (" + + image.toString() + + ") to private storage " + + file.getAbsolutePath()); copyImageToPrivateStorage(file, image, 0); } - public void copyImageToPrivateStorage(Message message, Uri image) throws FileCopyException, ImageCompressionException { + public void copyImageToPrivateStorage(Message message, Uri image) + throws FileCopyException, ImageCompressionException { switch (Config.IMAGE_FORMAT) { case JPEG: message.setRelativeFilePath(message.getUuid() + ".jpg"); @@ -813,7 +902,8 @@ public boolean unusualBounds(final Uri image) { try { final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; - final InputStream inputStream = mXmppConnectionService.getContentResolver().openInputStream(image); + final InputStream inputStream = + mXmppConnectionService.getContentResolver().openInputStream(image); BitmapFactory.decodeStream(inputStream, null, options); close(inputStream); float ratio = (float) options.outHeight / options.outWidth; @@ -833,7 +923,8 @@ private int getRotation(final File file) { } private int getRotation(final Uri image) { - try (final InputStream is = mXmppConnectionService.getContentResolver().openInputStream(image)) { + try (final InputStream is = + mXmppConnectionService.getContentResolver().openInputStream(image)) { return is == null ? 0 : getRotation(is); } catch (final Exception e) { return 0; @@ -842,7 +933,9 @@ private int getRotation(final Uri image) { private static int getRotation(final InputStream inputStream) throws IOException { final ExifInterface exif = new ExifInterface(inputStream); - final int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED); + final int orientation = + exif.getAttributeInt( + ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED); switch (orientation) { case ExifInterface.ORIENTATION_ROTATE_180: return 180; @@ -880,7 +973,12 @@ public Bitmap getThumbnail(Message message, int size, boolean cacheOnly) throws thumbnail = rotate(thumbnail, getRotation(file)); if (mime.equals("image/gif")) { Bitmap withGifOverlay = thumbnail.copy(Bitmap.Config.ARGB_8888, true); - drawOverlay(withGifOverlay, paintOverlayBlack(withGifOverlay) ? R.drawable.play_gif_black : R.drawable.play_gif_white, 1.0f); + drawOverlay( + withGifOverlay, + paintOverlayBlack(withGifOverlay) + ? R.drawable.play_gif_black + : R.drawable.play_gif_white, + 1.0f); thumbnail.recycle(); thumbnail = withGifOverlay; } @@ -903,27 +1001,36 @@ private Bitmap getFullSizeImagePreview(File file, int size) { } private void drawOverlay(Bitmap bitmap, int resource, float factor) { - Bitmap overlay = BitmapFactory.decodeResource(mXmppConnectionService.getResources(), resource); + Bitmap overlay = + BitmapFactory.decodeResource(mXmppConnectionService.getResources(), resource); Canvas canvas = new Canvas(bitmap); float targetSize = Math.min(canvas.getWidth(), canvas.getHeight()) * factor; - Log.d(Config.LOGTAG, "target size overlay: " + targetSize + " overlay bitmap size was " + overlay.getHeight()); + Log.d( + Config.LOGTAG, + "target size overlay: " + + targetSize + + " overlay bitmap size was " + + overlay.getHeight()); float left = (canvas.getWidth() - targetSize) / 2.0f; float top = (canvas.getHeight() - targetSize) / 2.0f; RectF dst = new RectF(left, top, left + targetSize - 1, top + targetSize - 1); canvas.drawBitmap(overlay, null, dst, createAntiAliasingPaint()); } - /** - * https://stackoverflow.com/a/3943023/210897 - */ + /** https://stackoverflow.com/a/3943023/210897 */ private boolean paintOverlayBlack(final Bitmap bitmap) { final int h = bitmap.getHeight(); final int w = bitmap.getWidth(); int record = 0; for (int y = Math.round(h * IGNORE_PADDING); y < h - Math.round(h * IGNORE_PADDING); ++y) { - for (int x = Math.round(w * IGNORE_PADDING); x < w - Math.round(w * IGNORE_PADDING); ++x) { + for (int x = Math.round(w * IGNORE_PADDING); + x < w - Math.round(w * IGNORE_PADDING); + ++x) { int pixel = bitmap.getPixel(x, y); - if ((Color.red(pixel) * 0.299 + Color.green(pixel) * 0.587 + Color.blue(pixel) * 0.114) > 186) { + if ((Color.red(pixel) * 0.299 + + Color.green(pixel) * 0.587 + + Color.blue(pixel) * 0.114) + > 186) { --record; } else { ++record; @@ -940,7 +1047,10 @@ private boolean paintOverlayBlackPdf(final Bitmap bitmap) { for (int y = 0; y < h; ++y) { for (int x = 0; x < w; ++x) { int pixel = bitmap.getPixel(x, y); - if ((Color.red(pixel) * 0.299 + Color.green(pixel) * 0.587 + Color.blue(pixel) * 0.114) > 186) { + if ((Color.red(pixel) * 0.299 + + Color.green(pixel) * 0.587 + + Color.blue(pixel) * 0.114) + > 186) { white++; } } @@ -975,16 +1085,27 @@ private Bitmap getVideoPreview(final File file, final int size) { frame = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); frame.eraseColor(0xff000000); } - drawOverlay(frame, paintOverlayBlack(frame) ? R.drawable.play_video_black : R.drawable.play_video_white, 0.75f); + drawOverlay( + frame, + paintOverlayBlack(frame) + ? R.drawable.play_video_black + : R.drawable.play_video_white, + 0.75f); return frame; } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) private Bitmap getPdfDocumentPreview(final File file, final int size) { try { - final ParcelFileDescriptor fileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); + final ParcelFileDescriptor fileDescriptor = + ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); final Bitmap rendered = renderPdfDocument(fileDescriptor, size, true); - drawOverlay(rendered, paintOverlayBlackPdf(rendered) ? R.drawable.open_pdf_black : R.drawable.open_pdf_white, 0.75f); + drawOverlay( + rendered, + paintOverlayBlackPdf(rendered) + ? R.drawable.open_pdf_black + : R.drawable.open_pdf_white, + 0.75f); return rendered; } catch (final IOException | SecurityException e) { Log.d(Config.LOGTAG, "unable to render PDF document preview", e); @@ -994,11 +1115,11 @@ private Bitmap getPdfDocumentPreview(final File file, final int size) { } } - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) private Bitmap cropCenterSquarePdf(final Uri uri, final int size) { try { - ParcelFileDescriptor fileDescriptor = mXmppConnectionService.getContentResolver().openFileDescriptor(uri, "r"); + ParcelFileDescriptor fileDescriptor = + mXmppConnectionService.getContentResolver().openFileDescriptor(uri, "r"); final Bitmap bitmap = renderPdfDocument(fileDescriptor, size, false); return cropCenterSquare(bitmap, size); } catch (Exception e) { @@ -1009,11 +1130,15 @@ private Bitmap cropCenterSquarePdf(final Uri uri, final int size) { } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - private Bitmap renderPdfDocument(ParcelFileDescriptor fileDescriptor, int targetSize, boolean fit) throws IOException { + private Bitmap renderPdfDocument( + ParcelFileDescriptor fileDescriptor, int targetSize, boolean fit) throws IOException { final PdfRenderer pdfRenderer = new PdfRenderer(fileDescriptor); final PdfRenderer.Page page = pdfRenderer.openPage(0); - final Dimensions dimensions = scalePdfDimensions(new Dimensions(page.getHeight(), page.getWidth()), targetSize, fit); - final Bitmap rendered = Bitmap.createBitmap(dimensions.width, dimensions.height, Bitmap.Config.ARGB_8888); + final Dimensions dimensions = + scalePdfDimensions( + new Dimensions(page.getHeight(), page.getWidth()), targetSize, fit); + final Bitmap rendered = + Bitmap.createBitmap(dimensions.width, dimensions.height, Bitmap.Config.ARGB_8888); rendered.eraseColor(0xffffffff); page.render(rendered, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY); page.close(); @@ -1025,9 +1150,17 @@ private Bitmap renderPdfDocument(ParcelFileDescriptor fileDescriptor, int target public Uri getTakePhotoUri() { File file; if (Config.ONLY_INTERNAL_STORAGE) { - file = new File(mXmppConnectionService.getCacheDir().getAbsolutePath(), "Camera/IMG_" + IMAGE_DATE_FORMAT.format(new Date()) + ".jpg"); + file = + new File( + mXmppConnectionService.getCacheDir().getAbsolutePath(), + "Camera/IMG_" + IMAGE_DATE_FORMAT.format(new Date()) + ".jpg"); } else { - file = new File(getTakePhotoPath() + "IMG_" + IMAGE_DATE_FORMAT.format(new Date()) + ".jpg"); + file = + new File( + getTakePhotoPath() + + "IMG_" + + IMAGE_DATE_FORMAT.format(new Date()) + + ".jpg"); } file.getParentFile().mkdirs(); return getUriForFile(mXmppConnectionService, file); @@ -1036,11 +1169,15 @@ public Uri getTakePhotoUri() { public Avatar getPepAvatar(Uri image, int size, Bitmap.CompressFormat format) { final Avatar uncompressAvatar = getUncompressedAvatar(image); - if (uncompressAvatar != null && uncompressAvatar.image.length() <= Config.AVATAR_CHAR_LIMIT) { + if (uncompressAvatar != null + && uncompressAvatar.image.length() <= Config.AVATAR_CHAR_LIMIT) { return uncompressAvatar; } if (uncompressAvatar != null) { - Log.d(Config.LOGTAG, "uncompressed avatar exceeded char limit by " + (uncompressAvatar.image.length() - Config.AVATAR_CHAR_LIMIT)); + Log.d( + Config.LOGTAG, + "uncompressed avatar exceeded char limit by " + + (uncompressAvatar.image.length() - Config.AVATAR_CHAR_LIMIT)); } Bitmap bm = cropCenterSquare(image, size); @@ -1059,7 +1196,9 @@ public Avatar getPepAvatar(Uri image, int size, Bitmap.CompressFormat format) { private Avatar getUncompressedAvatar(Uri uri) { Bitmap bitmap = null; try { - bitmap = BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(uri)); + bitmap = + BitmapFactory.decodeStream( + mXmppConnectionService.getContentResolver().openInputStream(uri)); return getPepAvatar(bitmap, Bitmap.CompressFormat.PNG, 100); } catch (Exception e) { return null; @@ -1073,18 +1212,24 @@ private Avatar getUncompressedAvatar(Uri uri) { private Avatar getPepAvatar(Bitmap bitmap, Bitmap.CompressFormat format, int quality) { try { ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream(); - Base64OutputStream mBase64OutputStream = new Base64OutputStream(mByteArrayOutputStream, Base64.DEFAULT); + Base64OutputStream mBase64OutputStream = + new Base64OutputStream(mByteArrayOutputStream, Base64.DEFAULT); MessageDigest digest = MessageDigest.getInstance("SHA-1"); - DigestOutputStream mDigestOutputStream = new DigestOutputStream(mBase64OutputStream, digest); + DigestOutputStream mDigestOutputStream = + new DigestOutputStream(mBase64OutputStream, digest); if (!bitmap.compress(format, quality, mDigestOutputStream)) { return null; } mDigestOutputStream.flush(); mDigestOutputStream.close(); long chars = mByteArrayOutputStream.size(); - if (format != Bitmap.CompressFormat.PNG && quality >= 50 && chars >= Config.AVATAR_CHAR_LIMIT) { + if (format != Bitmap.CompressFormat.PNG + && quality >= 50 + && chars >= Config.AVATAR_CHAR_LIMIT) { int q = quality - 2; - Log.d(Config.LOGTAG, "avatar char length was " + chars + " reducing quality to " + q); + Log.d( + Config.LOGTAG, + "avatar char length was " + chars + " reducing quality to " + q); return getPepAvatar(bitmap, format, q); } Log.d(Config.LOGTAG, "settled on char length " + chars + " with quality=" + quality); @@ -1123,7 +1268,8 @@ public Avatar getStoredPepAvatar(String hash) { BitmapFactory.decodeFile(file.getAbsolutePath(), options); is = new FileInputStream(file); ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream(); - Base64OutputStream mBase64OutputStream = new Base64OutputStream(mByteArrayOutputStream, Base64.DEFAULT); + Base64OutputStream mBase64OutputStream = + new Base64OutputStream(mByteArrayOutputStream, Base64.DEFAULT); MessageDigest digest = MessageDigest.getInstance("SHA-1"); DigestOutputStream os = new DigestOutputStream(mBase64OutputStream, digest); byte[] buffer = new byte[4096]; @@ -1157,14 +1303,20 @@ public boolean save(final Avatar avatar) { file = getAvatarFile(avatar.getFilename()); avatar.size = file.length(); } else { - file = new File(mXmppConnectionService.getCacheDir().getAbsolutePath() + "/" + UUID.randomUUID().toString()); + file = + new File( + mXmppConnectionService.getCacheDir().getAbsolutePath() + + "/" + + UUID.randomUUID().toString()); if (file.getParentFile().mkdirs()) { Log.d(Config.LOGTAG, "created cache directory"); } OutputStream os = null; try { if (!file.createNewFile()) { - Log.d(Config.LOGTAG, "unable to create temporary file " + file.getAbsolutePath()); + Log.d( + Config.LOGTAG, + "unable to create temporary file " + file.getAbsolutePath()); } os = new FileOutputStream(file); MessageDigest digest = MessageDigest.getInstance("SHA-1"); @@ -1182,7 +1334,9 @@ public boolean save(final Avatar avatar) { } final File avatarFile = getAvatarFile(avatar.getFilename()); if (!file.renameTo(avatarFile)) { - Log.d(Config.LOGTAG, "unable to rename " + file.getAbsolutePath() + " to " + outputFile); + Log.d( + Config.LOGTAG, + "unable to rename " + file.getAbsolutePath() + " to " + outputFile); return false; } } else { @@ -1294,7 +1448,7 @@ public Bitmap cropCenter(Uri image, int newHeight, int newWidth) { } return dest; } catch (SecurityException e) { - return null; //android 6.0 with revoked permissions for example + return null; // android 6.0 with revoked permissions for example } catch (FileNotFoundException e) { return null; } finally { @@ -1323,10 +1477,12 @@ public Bitmap cropCenterSquare(Bitmap input, int size) { return output; } - private int calcSampleSize(Uri image, int size) throws FileNotFoundException, SecurityException { + private int calcSampleSize(Uri image, int size) + throws FileNotFoundException, SecurityException { final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; - final InputStream inputStream = mXmppConnectionService.getContentResolver().openInputStream(image); + final InputStream inputStream = + mXmppConnectionService.getContentResolver().openInputStream(image); BitmapFactory.decodeStream(inputStream, null, options); close(inputStream); return calcSampleSize(options, size); @@ -1340,7 +1496,9 @@ public void updateFileParams(Message message, String url) { DownloadableFile file = getFile(message); final String mime = file.getMimeType(); final boolean privateMessage = message.isPrivateMessage(); - final boolean image = message.getType() == Message.TYPE_IMAGE || (mime != null && mime.startsWith("image/")); + final boolean image = + message.getType() == Message.TYPE_IMAGE + || (mime != null && mime.startsWith("image/")); final boolean video = mime != null && mime.startsWith("video/"); final boolean audio = mime != null && mime.startsWith("audio/"); final boolean pdf = "application/pdf".equals(mime); @@ -1363,22 +1521,29 @@ public void updateFileParams(Message message, String url) { body.append('|').append(dimensions.width).append('|').append(dimensions.height); } } catch (NotAVideoFile notAVideoFile) { - Log.d(Config.LOGTAG, "file with mime type " + file.getMimeType() + " was not a video file"); - //fall threw + Log.d( + Config.LOGTAG, + "file with mime type " + file.getMimeType() + " was not a video file"); + // fall threw } } else if (audio) { body.append("|0|0|").append(getMediaRuntime(file)); } message.setBody(body.toString()); message.setDeleted(false); - message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : (image ? Message.TYPE_IMAGE : Message.TYPE_FILE)); + message.setType( + privateMessage + ? Message.TYPE_PRIVATE_FILE + : (image ? Message.TYPE_IMAGE : Message.TYPE_FILE)); } private int getMediaRuntime(File file) { try { MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); mediaMetadataRetriever.setDataSource(file.toString()); - return Integer.parseInt(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)); + return Integer.parseInt( + mediaMetadataRetriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_DURATION)); } catch (RuntimeException e) { return 0; } @@ -1431,12 +1596,14 @@ private Dimensions getPdfDocumentDimensions(final File file) { } private Dimensions scalePdfDimensions(Dimensions in) { - final DisplayMetrics displayMetrics = mXmppConnectionService.getResources().getDisplayMetrics(); + final DisplayMetrics displayMetrics = + mXmppConnectionService.getResources().getDisplayMetrics(); final int target = (int) (displayMetrics.density * 288); return scalePdfDimensions(in, target, true); } - private static Dimensions scalePdfDimensions(final Dimensions in, final int target, final boolean fit) { + private static Dimensions scalePdfDimensions( + final Dimensions in, final int target, final boolean fit) { final int w, h; if (fit == (in.width <= in.height)) { w = Math.max((int) (in.width / ((double) in.height / target)), 1); @@ -1491,7 +1658,6 @@ public static class ImageCompressionException extends Exception { } } - public static class FileCopyException extends Exception { private final int resId; @@ -1499,8 +1665,7 @@ private FileCopyException(@StringRes int resId) { this.resId = resId; } - public @StringRes - int getResId() { + public @StringRes int getResId() { return resId; } } diff --git a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java index 6146c4ae7..4fd41f36a 100644 --- a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java @@ -154,9 +154,9 @@ protected void stopRecording(final boolean saveFile) { } private static File generateOutputFilename(Context context) { - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US); - String filename = "RECORDING_" + dateFormat.format(new Date()) + ".m4a"; - return new File(FileBackend.getConversationsDirectory(context, STORAGE_DIRECTORY_TYPE_NAME) + "/" + filename); + final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US); + final String filename = "RECORDING_" + dateFormat.format(new Date()) + ".m4a"; + return new File(FileBackend.getLegacyStorageLocation(context, STORAGE_DIRECTORY_TYPE_NAME), filename); } private void setupOutputFile() { From 8abacd23e8d29a75d0abd497dc842de4663e2a84 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 22 Feb 2022 15:14:00 +0100 Subject: [PATCH 045/394] use new storage location for backup and recordings --- .../services/ImportBackupService.java | 11 +- .../persistance/FileBackend.java | 111 +++++++----------- .../services/ExportBackupService.java | 6 +- .../ui/ConversationFragment.java | 4 +- .../conversations/ui/RecordingActivity.java | 22 ++-- .../conversations/ui/SettingsActivity.java | 2 +- 6 files changed, 61 insertions(+), 95 deletions(-) diff --git a/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java b/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java index 9c6ebaafd..a1b5f9e77 100644 --- a/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java +++ b/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java @@ -128,16 +128,19 @@ public void loadBackupFiles(final OnBackupFilesLoaded onBackupFilesLoaded) { final List accounts = mDatabaseBackend.getAccountJids(false); final ArrayList backupFiles = new ArrayList<>(); final Set apps = new HashSet<>(Arrays.asList("Conversations", "Quicksy", getString(R.string.app_name))); - for (String app : apps) { - final File directory = new File(FileBackend.getBackupDirectory(app)); + final List directories = new ArrayList<>(); + for (final String app : apps) { + directories.add(FileBackend.getLegacyBackupDirectory(app)); + } + directories.add(FileBackend.getBackupDirectory(this)); + for (final File directory : directories) { if (!directory.exists() || !directory.isDirectory()) { Log.d(Config.LOGTAG, "directory not found: " + directory.getAbsolutePath()); continue; } final File[] files = directory.listFiles(); if (files == null) { - onBackupFilesLoaded.onBackupFilesLoaded(backupFiles); - return; + continue; } for (final File file : files) { if (file.isFile() && file.getName().endsWith(".ceb")) { diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 2b8f53430..ce7c1f8dd 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -64,7 +64,6 @@ import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.services.AttachFileToConversationRunnable; import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.ui.RecordingActivity; import eu.siacs.conversations.ui.util.Attachment; import eu.siacs.conversations.utils.Compatibility; import eu.siacs.conversations.utils.CryptoHelper; @@ -88,19 +87,6 @@ public FileBackend(XmppConnectionService service) { this.mXmppConnectionService = service; } - private static boolean isInDirectoryThatShouldNotBeScanned(Context context, File file) { - return isInDirectoryThatShouldNotBeScanned(context, file.getAbsolutePath()); - } - - public static boolean isInDirectoryThatShouldNotBeScanned(Context context, String path) { - for (String type : new String[] {RecordingActivity.STORAGE_DIRECTORY_TYPE_NAME, "Files"}) { - if (path.startsWith(getLegacyStorageLocation(context, type).getAbsolutePath())) { - return true; - } - } - return false; - } - public static long getFileSize(Context context, Uri uri) { try { final Cursor cursor = context.getContentResolver().query(uri, null, null, null, null); @@ -156,34 +142,18 @@ public static boolean allFilesUnderSize( return true; } - public static File getLegacyStorageLocation(Context context, final String type) { - if (Config.ONLY_INTERNAL_STORAGE) { - return new File(context.getFilesDir(), type); - } else { - final File appDirectory = - new File( - Environment.getExternalStorageDirectory(), - context.getString(R.string.app_name)); - final File appMediaDirectory = new File(appDirectory, "Media"); - final String locationName = - String.format("%s %s", context.getString(R.string.app_name), type); - return new File(appMediaDirectory, locationName); - } - } - - private static String getAppMediaDirectory(Context context) { - return Environment.getExternalStorageDirectory().getAbsolutePath() - + "/" - + context.getString(R.string.app_name) - + "/Media/"; - } - - public static String getBackupDirectory(Context context) { - return getBackupDirectory(context.getString(R.string.app_name)); + public static File getBackupDirectory(final Context context) { + final File conversationsDownloadDirectory = + new File( + Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOWNLOADS), + context.getString(R.string.app_name)); + return new File(conversationsDownloadDirectory, "Backup"); } - public static String getBackupDirectory(String app) { - return Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + app + "/Backup/"; + public static File getLegacyBackupDirectory(final String app) { + final File appDirectory = new File(Environment.getExternalStorageDirectory(), app); + return new File(appDirectory, "Backup"); } private static Bitmap rotate(final Bitmap bitmap, final int degree) { @@ -521,38 +491,26 @@ public void updateMediaScanner(File file) { } public void updateMediaScanner(File file, final Runnable callback) { - if (!isInDirectoryThatShouldNotBeScanned(mXmppConnectionService, file)) { - MediaScannerConnection.scanFile( - mXmppConnectionService, - new String[] {file.getAbsolutePath()}, - null, - new MediaScannerConnection.MediaScannerConnectionClient() { - @Override - public void onMediaScannerConnected() {} - - @Override - public void onScanCompleted(String path, Uri uri) { - if (callback != null && file.getAbsolutePath().equals(path)) { + MediaScannerConnection.scanFile( + mXmppConnectionService, + new String[] {file.getAbsolutePath()}, + null, + new MediaScannerConnection.MediaScannerConnectionClient() { + @Override + public void onMediaScannerConnected() {} + + @Override + public void onScanCompleted(String path, Uri uri) { + if (callback != null && file.getAbsolutePath().equals(path)) { + callback.run(); + } else { + Log.d(Config.LOGTAG, "media scanner scanned wrong file"); + if (callback != null) { callback.run(); - } else { - Log.d(Config.LOGTAG, "media scanner scanned wrong file"); - if (callback != null) { - callback.run(); - } } } - }); - return; - /*Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); - intent.setData(Uri.fromFile(file)); - mXmppConnectionService.sendBroadcast(intent);*/ - } else if (file.getAbsolutePath() - .startsWith(getAppMediaDirectory(mXmppConnectionService))) { - createNoMedia(file.getParentFile()); - } - if (callback != null) { - callback.run(); - } + } + }); } public boolean deleteFile(Message message) { @@ -629,9 +587,20 @@ public List convertToAttachments(List rela return attachments; } - // TODO remove static method. use direct instance access private File getLegacyStorageLocation(final String type) { - return getLegacyStorageLocation(mXmppConnectionService, type); + if (Config.ONLY_INTERNAL_STORAGE) { + return new File(mXmppConnectionService.getFilesDir(), type); + } else { + final File appDirectory = + new File( + Environment.getExternalStorageDirectory(), + mXmppConnectionService.getString(R.string.app_name)); + final File appMediaDirectory = new File(appDirectory, "Media"); + final String locationName = + String.format( + "%s %s", mXmppConnectionService.getString(R.string.app_name), type); + return new File(appMediaDirectory, locationName); + } } private Bitmap resize(final Bitmap originalBitmap, int size) throws IOException { diff --git a/src/main/java/eu/siacs/conversations/services/ExportBackupService.java b/src/main/java/eu/siacs/conversations/services/ExportBackupService.java index 95584ae23..f89434897 100644 --- a/src/main/java/eu/siacs/conversations/services/ExportBackupService.java +++ b/src/main/java/eu/siacs/conversations/services/ExportBackupService.java @@ -291,7 +291,7 @@ private List export() throws Exception { secureRandom.nextBytes(salt); final BackupFileHeader backupFileHeader = new BackupFileHeader(getString(R.string.app_name), account.getJid(), System.currentTimeMillis(), IV, salt); final Progress progress = new Progress(mBuilder, max, count); - final File file = new File(FileBackend.getBackupDirectory(this) + account.getJid().asBareJid().toEscapedString() + ".ceb"); + final File file = new File(FileBackend.getBackupDirectory(this), account.getJid().asBareJid().toEscapedString() + ".ceb"); files.add(file); final File directory = file.getParentFile(); if (directory != null && directory.mkdirs()) { @@ -335,7 +335,7 @@ private void mediaScannerScanFile(final File file) { } private void notifySuccess(final List files) { - final String path = FileBackend.getBackupDirectory(this); + final String path = FileBackend.getBackupDirectory(this).getAbsolutePath(); PendingIntent openFolderIntent = null; @@ -363,7 +363,7 @@ private void notifySuccess(final List files) { NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup"); mBuilder.setContentTitle(getString(R.string.notification_backup_created_title)) .setContentText(getString(R.string.notification_backup_created_subtitle, path)) - .setStyle(new NotificationCompat.BigTextStyle().bigText(getString(R.string.notification_backup_created_subtitle, FileBackend.getBackupDirectory(this)))) + .setStyle(new NotificationCompat.BigTextStyle().bigText(getString(R.string.notification_backup_created_subtitle, FileBackend.getBackupDirectory(this).getAbsolutePath()))) .setAutoCancel(true) .setContentIntent(openFolderIntent) .setSmallIcon(R.drawable.ic_archive_white_24dp); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 771c99e9c..b7215c22d 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1183,8 +1183,8 @@ private void populateContextMenu(ContextMenu menu) { cancelTransmission.setVisible(true); } if (m.isFileOrImage() && !deleted && !cancelable) { - String path = m.getRelativeFilePath(); - if (path == null || !path.startsWith("/") || FileBackend.isInDirectoryThatShouldNotBeScanned(getActivity(), path)) { + final String path = m.getRelativeFilePath(); + if (path == null || !path.startsWith("/")) { deleteFile.setVisible(true); deleteFile.setTitle(activity.getString(R.string.delete_x_file, UIHelper.getFileDescriptionString(activity, m))); } diff --git a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java index 4fd41f36a..c42a36d57 100644 --- a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java @@ -6,6 +6,7 @@ import android.media.MediaRecorder; import android.net.Uri; import android.os.Bundle; +import android.os.Environment; import android.os.FileObserver; import android.os.Handler; import android.os.SystemClock; @@ -153,28 +154,21 @@ protected void stopRecording(final boolean saveFile) { } } - private static File generateOutputFilename(Context context) { + private File generateOutputFilename() { final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US); final String filename = "RECORDING_" + dateFormat.format(new Date()) + ".m4a"; - return new File(FileBackend.getLegacyStorageLocation(context, STORAGE_DIRECTORY_TYPE_NAME), filename); + //TODO once we target 31 use DIRECTORY_RECORDINGS + final File parentDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + final File conversationsDirectory = new File(parentDirectory, getString(R.string.app_name)); + return new File(conversationsDirectory, filename); } private void setupOutputFile() { - mOutputFile = generateOutputFilename(this); - File parentDirectory = mOutputFile.getParentFile(); + mOutputFile = generateOutputFilename(); + final File parentDirectory = mOutputFile.getParentFile(); if (parentDirectory.mkdirs()) { Log.d(Config.LOGTAG, "created " + parentDirectory.getAbsolutePath()); } - File noMedia = new File(parentDirectory, ".nomedia"); - if (!noMedia.exists()) { - try { - if (noMedia.createNewFile()) { - Log.d(Config.LOGTAG, "created nomedia file in " + parentDirectory.getAbsolutePath()); - } - } catch (IOException e) { - Log.d(Config.LOGTAG, "unable to create nomedia file in " + parentDirectory.getAbsolutePath(), e); - } - } setupFileObserver(parentDirectory); } diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java index 7f4e59d1a..7073b881d 100644 --- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java @@ -224,7 +224,7 @@ public void onStart() { final Preference createBackupPreference = mSettingsFragment.findPreference("create_backup"); if (createBackupPreference != null) { - createBackupPreference.setSummary(getString(R.string.pref_create_backup_summary, FileBackend.getBackupDirectory(this))); + createBackupPreference.setSummary(getString(R.string.pref_create_backup_summary, FileBackend.getBackupDirectory(this).getAbsolutePath())); createBackupPreference.setOnPreferenceClickListener(preference -> { if (hasStoragePermission(REQUEST_CREATE_BACKUP)) { createBackup(); From d6be6ddd18335d6a4cf437a8e4f2b425c57914f1 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 22 Feb 2022 16:05:02 +0100 Subject: [PATCH 046/394] use full file name for all new files --- .../crypto/PgpDecryptionService.java | 9 +++-- .../http/HttpDownloadConnection.java | 9 ++--- .../persistance/FileBackend.java | 40 ++++++++++++++++--- .../AttachFileToConversationRunnable.java | 2 +- .../siacs/conversations/ui/util/ViewUtil.java | 14 +++---- .../jingle/JingleFileTransferConnection.java | 8 ++-- 6 files changed, 53 insertions(+), 29 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java b/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java index 0ad103155..9a2288884 100644 --- a/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java +++ b/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java @@ -9,6 +9,7 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; @@ -194,9 +195,9 @@ private void executeApi(Message message) { String originalExtension = originalFilename == null ? null : MimeUtils.extractRelevantExtension(originalFilename); if (originalExtension != null && MimeUtils.extractRelevantExtension(outputFile.getName()) == null) { Log.d(Config.LOGTAG,"detected original filename during pgp decryption"); - String mime = MimeUtils.guessMimeTypeFromExtension(originalExtension); - String path = outputFile.getName()+"."+originalExtension; - DownloadableFile fixedFile = mXmppConnectionService.getFileBackend().getFileForPath(path,mime); + final String mime = MimeUtils.guessMimeTypeFromExtension(originalExtension); + final String filename = outputFile.getName()+"."+originalExtension; + final File fixedFile = mXmppConnectionService.getFileBackend().getStorageLocation(filename,mime); if (fixedFile.getParentFile().mkdirs()) { Log.d(Config.LOGTAG,"created parent directories for "+fixedFile.getAbsolutePath()); } @@ -205,7 +206,7 @@ private void executeApi(Message message) { } if (outputFile.renameTo(fixedFile)) { Log.d(Config.LOGTAG, "renamed " + outputFile.getAbsolutePath() + " to " + fixedFile.getAbsolutePath()); - message.setRelativeFilePath(path); + message.setRelativeFilePath(fixedFile.getAbsolutePath()); } } final String url = message.getFileParams().url; diff --git a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java index 15dc6eac6..3c9ceb978 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java @@ -96,11 +96,8 @@ public void init(boolean interactive) { this.message.setEncryption(Message.ENCRYPTION_NONE); } final String ext = extension.getExtension(); - if (ext != null) { - message.setRelativeFilePath(String.format("%s.%s", message.getUuid(), ext)); - } else if (Strings.isNullOrEmpty(message.getRelativeFilePath())) { - message.setRelativeFilePath(message.getUuid()); - } + final String filename = Strings.isNullOrEmpty(ext) ? message.getUuid() : String.format("%s.%s", message.getUuid(), ext); + mXmppConnectionService.getFileBackend().setupRelativeFilePath(message, filename); setupFile(); if (this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL && this.file.getKey() == null) { this.message.setEncryption(Message.ENCRYPTION_NONE); @@ -326,7 +323,7 @@ private long retrieveFileSize() throws IOException { if (Strings.isNullOrEmpty(extension.getExtension()) && contentType != null) { final String fileExtension = MimeUtils.guessExtensionFromMimeType(contentType); if (fileExtension != null) { - message.setRelativeFilePath(String.format("%s.%s", message.getUuid(), fileExtension)); + mXmppConnectionService.getFileBackend().setupRelativeFilePath(message, String.format("%s.%s", message.getUuid(), fileExtension), contentType); Log.d(Config.LOGTAG, "rewriting name after not finding extension in url but in content type"); setupFile(); } diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index ce7c1f8dd..505a3ebb5 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -533,7 +533,7 @@ public DownloadableFile getFileForPath(String path) { MimeUtils.guessMimeTypeFromExtension(MimeUtils.extractRelevantExtension(path))); } - public DownloadableFile getFileForPath(final String path, final String mime) { + private DownloadableFile getFileForPath(final String path, final String mime) { if (path.startsWith("/")) { return new DownloadableFile(path); } else { @@ -719,7 +719,7 @@ public void copyFileToPrivateStorage(Message message, Uri uri, String type) if ("ogg".equals(extension) && type != null && type.startsWith("audio/")) { extension = "oga"; } - message.setRelativeFilePath(message.getUuid() + "." + extension); + setupRelativeFilePath(message, String.format("%s.%s", message.getUuid(), extension)); copyFileToPrivateStorage(mXmppConnectionService.getFileBackend().getFile(message), uri); } @@ -852,21 +852,51 @@ public void copyImageToPrivateStorage(File file, Uri image) public void copyImageToPrivateStorage(Message message, Uri image) throws FileCopyException, ImageCompressionException { + final String filename; switch (Config.IMAGE_FORMAT) { case JPEG: - message.setRelativeFilePath(message.getUuid() + ".jpg"); + filename = String.format("%s.%s", message.getUuid(), "jpg"); break; case PNG: - message.setRelativeFilePath(message.getUuid() + ".png"); + filename = String.format("%s.%s", message.getUuid(), "png"); break; case WEBP: - message.setRelativeFilePath(message.getUuid() + ".webp"); + filename = String.format("%s.%s", message.getUuid(), "webp"); break; + default: + throw new IllegalStateException("Unknown image format"); } + setupRelativeFilePath(message, filename); copyImageToPrivateStorage(getFile(message), image); updateFileParams(message); } + public void setupRelativeFilePath(final Message message, final String filename) { + final String extension = MimeUtils.extractRelevantExtension(filename); + final String mime = MimeUtils.guessMimeTypeFromExtension(extension); + setupRelativeFilePath(message, filename, mime); + } + + public File getStorageLocation(final String filename, final String mime) { + final File parentDirectory; + if (Strings.isNullOrEmpty(mime)) { + parentDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + } else if (mime.startsWith("image/")) { + parentDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); + } else if (mime.startsWith("video/")) { + parentDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES); + } else { + parentDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + } + final File appDirectory = new File(parentDirectory, mXmppConnectionService.getString(R.string.app_name)); + return new File(appDirectory, filename); + } + + public void setupRelativeFilePath(final Message message, final String filename, final String mime) { + final File file = getStorageLocation(filename, mime); + message.setRelativeFilePath(file.getAbsolutePath()); + } + public boolean unusualBounds(final Uri image) { try { final BitmapFactory.Options options = new BitmapFactory.Options(); diff --git a/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java b/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java index db879799d..1ddee27b5 100644 --- a/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java +++ b/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java @@ -91,7 +91,7 @@ private void processAsFile() { private void processAsVideo() throws FileNotFoundException { Log.d(Config.LOGTAG, "processing file as video"); mXmppConnectionService.startForcingForegroundNotification(); - message.setRelativeFilePath(message.getUuid() + ".mp4"); + mXmppConnectionService.getFileBackend().setupRelativeFilePath(message, String.format("%s.%s", message.getUuid(), "mp4")); final DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message); if (Objects.requireNonNull(file.getParentFile()).mkdirs()) { Log.d(Config.LOGTAG, "created parent directory for video file"); diff --git a/src/main/java/eu/siacs/conversations/ui/util/ViewUtil.java b/src/main/java/eu/siacs/conversations/ui/util/ViewUtil.java index 013927c4e..1cc630ad3 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/ViewUtil.java +++ b/src/main/java/eu/siacs/conversations/ui/util/ViewUtil.java @@ -37,9 +37,10 @@ public static void view (Context context, DownloadableFile file) { view(context, file, mime); } - public static void view(Context context, File file, String mime) { - Intent openIntent = new Intent(Intent.ACTION_VIEW); - Uri uri; + private static void view(Context context, File file, String mime) { + Log.d(Config.LOGTAG,"viewing "+file.getAbsolutePath()+" "+mime); + final Intent openIntent = new Intent(Intent.ACTION_VIEW); + final Uri uri; try { uri = FileBackend.getUriForFile(context, file); } catch (SecurityException e) { @@ -49,14 +50,9 @@ public static void view(Context context, File file, String mime) { } openIntent.setDataAndType(uri, mime); openIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - PackageManager manager = context.getPackageManager(); - List info = manager.queryIntentActivities(openIntent, 0); - if (info.size() == 0) { - openIntent.setDataAndType(uri, "*/*"); - } try { context.startActivity(openIntent); - } catch (ActivityNotFoundException e) { + } catch (final ActivityNotFoundException e) { Toast.makeText(context, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT).show(); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java index 40edf0b2c..43aaa54b5 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java @@ -492,19 +492,19 @@ private void init(JinglePacket packet) { //should move to deliverPacket AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(path); if (VALID_IMAGE_EXTENSIONS.contains(extension.main)) { message.setType(Message.TYPE_IMAGE); - message.setRelativeFilePath(message.getUuid() + "." + extension.main); + xmppConnectionService.getFileBackend().setupRelativeFilePath(message, message.getUuid() + "." + extension.main); } else if (VALID_CRYPTO_EXTENSIONS.contains(extension.main)) { if (VALID_IMAGE_EXTENSIONS.contains(extension.secondary)) { message.setType(Message.TYPE_IMAGE); - message.setRelativeFilePath(message.getUuid() + "." + extension.secondary); + xmppConnectionService.getFileBackend().setupRelativeFilePath(message,message.getUuid() + "." + extension.secondary); } else { message.setType(Message.TYPE_FILE); - message.setRelativeFilePath(message.getUuid() + (extension.secondary != null ? ("." + extension.secondary) : "")); + xmppConnectionService.getFileBackend().setupRelativeFilePath(message,message.getUuid() + (extension.secondary != null ? ("." + extension.secondary) : "")); } message.setEncryption(Message.ENCRYPTION_PGP); } else { message.setType(Message.TYPE_FILE); - message.setRelativeFilePath(message.getUuid() + (extension.main != null ? ("." + extension.main) : "")); + xmppConnectionService.getFileBackend().setupRelativeFilePath(message,message.getUuid() + (extension.main != null ? ("." + extension.main) : "")); } long size = parseLong(fileSize, 0); message.setBody(Long.toString(size)); From 2cc49e5ba66785c12b283af727273187393c47f6 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 22 Feb 2022 16:05:24 +0100 Subject: [PATCH 047/394] bump targetSdk --- build.gradle | 4 ++-- src/main/AndroidManifest.xml | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 2a070856a..3df3ab8ea 100644 --- a/build.gradle +++ b/build.gradle @@ -87,11 +87,11 @@ ext { } android { - compileSdkVersion 29 + compileSdkVersion 30 defaultConfig { minSdkVersion 21 - targetSdkVersion 29 + targetSdkVersion 30 versionCode 42024 versionName "2.10.3-beta" archivesBaseName += "-$versionName" diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index ff41c07c2..8a61dece7 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -61,6 +61,7 @@ android:largeHeap="true" android:networkSecurityConfig="@xml/network_security_configuration" android:requestLegacyExternalStorage="true" + android:preserveLegacyExternalStorage="true" android:theme="@style/ConversationsTheme" tools:replace="android:label" tools:targetApi="q"> From 6fb465f91aae23db422ff2ab9277ec6b0f94a57b Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 22 Feb 2022 16:23:51 +0100 Subject: [PATCH 048/394] =?UTF-8?q?don=E2=80=99t=20query=20packages=20befo?= =?UTF-8?q?re=20attaching=20something?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../conversations/persistance/FileBackend.java | 14 ++++---------- .../conversations/ui/ConversationFragment.java | 5 +++-- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 505a3ebb5..c36011577 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -1147,19 +1147,13 @@ private Bitmap renderPdfDocument( } public Uri getTakePhotoUri() { + final String filename = String.format("IMG_%s.%s", IMAGE_DATE_FORMAT.format(new Date()),"jpg"); File file; if (Config.ONLY_INTERNAL_STORAGE) { - file = - new File( - mXmppConnectionService.getCacheDir().getAbsolutePath(), - "Camera/IMG_" + IMAGE_DATE_FORMAT.format(new Date()) + ".jpg"); + final File dcimCache = new File(mXmppConnectionService.getCacheDir(), "Camera"); + file = new File(dcimCache, filename); } else { - file = - new File( - getTakePhotoPath() - + "IMG_" - + IMAGE_DATE_FORMAT.format(new Date()) - + ".jpg"); + file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), filename); } file.getParentFile().mkdirs(); return getUriForFile(mXmppConnectionService, file); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index b7215c22d..a42fa6766 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -6,6 +6,7 @@ import android.app.Fragment; import android.app.FragmentManager; import android.app.PendingIntent; +import android.content.ActivityNotFoundException; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -1744,7 +1745,7 @@ protected void invokeAttachFileIntent(final int attachmentChoice) { if (context == null) { return; } - if (intent.resolveActivity(context.getPackageManager()) != null) { + try { if (chooser) { startActivityForResult( Intent.createChooser(intent, getString(R.string.perform_action_with)), @@ -1752,7 +1753,7 @@ protected void invokeAttachFileIntent(final int attachmentChoice) { } else { startActivityForResult(intent, attachmentChoice); } - } else { + } catch (final ActivityNotFoundException e) { Toast.makeText(context, R.string.no_application_found, Toast.LENGTH_LONG).show(); } } From 282109db01932c1be8e6231879ef5a75d67f5b74 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 22 Feb 2022 17:24:03 +0100 Subject: [PATCH 049/394] add openkeychain to queries --- src/main/AndroidManifest.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 8a61dece7..f3922675d 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -50,6 +50,10 @@ android:name="android.hardware.microphone" android:required="false" /> + + + + Date: Tue, 22 Feb 2022 17:25:48 +0100 Subject: [PATCH 050/394] write photos to DCIM/Camera --- .../persistance/FileBackend.java | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index c36011577..d6a33b640 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -880,19 +880,25 @@ public void setupRelativeFilePath(final Message message, final String filename) public File getStorageLocation(final String filename, final String mime) { final File parentDirectory; if (Strings.isNullOrEmpty(mime)) { - parentDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + parentDirectory = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); } else if (mime.startsWith("image/")) { - parentDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); + parentDirectory = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); } else if (mime.startsWith("video/")) { - parentDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES); + parentDirectory = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES); } else { - parentDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + parentDirectory = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); } - final File appDirectory = new File(parentDirectory, mXmppConnectionService.getString(R.string.app_name)); + final File appDirectory = + new File(parentDirectory, mXmppConnectionService.getString(R.string.app_name)); return new File(appDirectory, filename); } - public void setupRelativeFilePath(final Message message, final String filename, final String mime) { + public void setupRelativeFilePath( + final Message message, final String filename, final String mime) { final File file = getStorageLocation(filename, mime); message.setRelativeFilePath(file.getAbsolutePath()); } @@ -1147,14 +1153,19 @@ private Bitmap renderPdfDocument( } public Uri getTakePhotoUri() { - final String filename = String.format("IMG_%s.%s", IMAGE_DATE_FORMAT.format(new Date()),"jpg"); - File file; + final String filename = + String.format("IMG_%s.%s", IMAGE_DATE_FORMAT.format(new Date()), "jpg"); + final File directory; if (Config.ONLY_INTERNAL_STORAGE) { - final File dcimCache = new File(mXmppConnectionService.getCacheDir(), "Camera"); - file = new File(dcimCache, filename); + directory = new File(mXmppConnectionService.getCacheDir(), "Camera"); } else { - file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), filename); + directory = + new File( + Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_DCIM), + "Camera"); } + final File file = new File(directory, filename); file.getParentFile().mkdirs(); return getUriForFile(mXmppConnectionService, file); } From 0b470534f1b634261901d31a43e11b8293f5ef2c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 23 Feb 2022 09:40:38 +0100 Subject: [PATCH 051/394] store recordings and documents in their respective folders --- build.gradle | 2 +- .../persistance/FileBackend.java | 4 + .../conversations/ui/RecordingActivity.java | 92 +++++++++++-------- .../ui/adapter/MediaAdapter.java | 2 +- 4 files changed, 61 insertions(+), 39 deletions(-) diff --git a/build.gradle b/build.gradle index 3df3ab8ea..6f26dc4fc 100644 --- a/build.gradle +++ b/build.gradle @@ -87,7 +87,7 @@ ext { } android { - compileSdkVersion 30 + compileSdkVersion 31 defaultConfig { minSdkVersion 21 diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index d6a33b640..1ee5191db 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -64,6 +64,7 @@ import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.services.AttachFileToConversationRunnable; import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.ui.adapter.MediaAdapter; import eu.siacs.conversations.ui.util.Attachment; import eu.siacs.conversations.utils.Compatibility; import eu.siacs.conversations.utils.CryptoHelper; @@ -888,6 +889,9 @@ public File getStorageLocation(final String filename, final String mime) { } else if (mime.startsWith("video/")) { parentDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES); + } else if (MediaAdapter.DOCUMENT_MIMES.contains(mime)) { + parentDirectory = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS); } else { parentDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); diff --git a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java index c42a36d57..bc9972316 100644 --- a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java @@ -1,10 +1,10 @@ package eu.siacs.conversations.ui; import android.app.Activity; -import android.content.Context; import android.content.Intent; import android.media.MediaRecorder; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.os.FileObserver; @@ -18,25 +18,22 @@ import androidx.databinding.DataBindingUtil; import java.io.File; -import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; +import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivityRecordingBinding; -import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.ui.util.SettingsUtils; import eu.siacs.conversations.utils.ThemeHelper; import eu.siacs.conversations.utils.TimeFrameUtils; public class RecordingActivity extends Activity implements View.OnClickListener { - public static String STORAGE_DIRECTORY_TYPE_NAME = "Recordings"; - private ActivityRecordingBinding binding; private MediaRecorder mRecorder; @@ -45,13 +42,14 @@ public class RecordingActivity extends Activity implements View.OnClickListener private final CountDownLatch outputFileWrittenLatch = new CountDownLatch(1); private final Handler mHandler = new Handler(); - private final Runnable mTickExecutor = new Runnable() { - @Override - public void run() { - tick(); - mHandler.postDelayed(mTickExecutor, 100); - } - }; + private final Runnable mTickExecutor = + new Runnable() { + @Override + public void run() { + tick(); + mHandler.postDelayed(mTickExecutor, 100); + } + }; private File mOutputFile; @@ -69,7 +67,7 @@ protected void onCreate(Bundle savedInstanceState) { } @Override - protected void onResume(){ + protected void onResume() { super.onResume(); SettingsUtils.applyScreenshotPreventionSetting(this); } @@ -138,27 +136,44 @@ protected void stopRecording(final boolean saveFile) { } } if (saveFile) { - new Thread(() -> { - try { - if (!outputFileWrittenLatch.await(2, TimeUnit.SECONDS)) { - Log.d(Config.LOGTAG, "time out waiting for output file to be written"); - } - } catch (InterruptedException e) { - Log.d(Config.LOGTAG, "interrupted while waiting for output file to be written", e); - } - runOnUiThread(() -> { - setResult(Activity.RESULT_OK, new Intent().setData(Uri.fromFile(mOutputFile))); - finish(); - }); - }).start(); + new Thread( + () -> { + try { + if (!outputFileWrittenLatch.await(2, TimeUnit.SECONDS)) { + Log.d( + Config.LOGTAG, + "time out waiting for output file to be written"); + } + } catch (InterruptedException e) { + Log.d( + Config.LOGTAG, + "interrupted while waiting for output file to be written", + e); + } + runOnUiThread( + () -> { + setResult( + Activity.RESULT_OK, + new Intent() + .setData(Uri.fromFile(mOutputFile))); + finish(); + }); + }) + .start(); } } private File generateOutputFilename() { final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US); final String filename = "RECORDING_" + dateFormat.format(new Date()) + ".m4a"; - //TODO once we target 31 use DIRECTORY_RECORDINGS - final File parentDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + final File parentDirectory; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + parentDirectory = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_RECORDINGS); + } else { + parentDirectory = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + } final File conversationsDirectory = new File(parentDirectory, getString(R.string.app_name)); return new File(conversationsDirectory, filename); } @@ -166,21 +181,24 @@ private File generateOutputFilename() { private void setupOutputFile() { mOutputFile = generateOutputFilename(); final File parentDirectory = mOutputFile.getParentFile(); - if (parentDirectory.mkdirs()) { + if (Objects.requireNonNull(parentDirectory).mkdirs()) { Log.d(Config.LOGTAG, "created " + parentDirectory.getAbsolutePath()); } setupFileObserver(parentDirectory); } private void setupFileObserver(File directory) { - mFileObserver = new FileObserver(directory.getAbsolutePath()) { - @Override - public void onEvent(int event, String s) { - if (s != null && s.equals(mOutputFile.getName()) && event == FileObserver.CLOSE_WRITE) { - outputFileWrittenLatch.countDown(); - } - } - }; + mFileObserver = + new FileObserver(directory.getAbsolutePath()) { + @Override + public void onEvent(int event, String s) { + if (s != null + && s.equals(mOutputFile.getName()) + && event == FileObserver.CLOSE_WRITE) { + outputFileWrittenLatch.countDown(); + } + } + }; mFileObserver.startWatching(); } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java index 21473dafd..2733e7b8b 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java @@ -34,7 +34,7 @@ public class MediaAdapter extends RecyclerView.Adapter { - private static final List DOCUMENT_MIMES = Arrays.asList( + public static final List DOCUMENT_MIMES = Arrays.asList( "application/pdf", "application/vnd.oasis.opendocument.text", "application/msword", From 4129ca6af8f22fc64cca891052615d3d2755ad79 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 23 Feb 2022 09:40:47 +0100 Subject: [PATCH 052/394] fix rare npe --- .../conversations/ui/EnterJidDialog.java | 477 +++++++++--------- 1 file changed, 247 insertions(+), 230 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java index 2f8c98d76..15ebdb0b7 100644 --- a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java +++ b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java @@ -22,6 +22,7 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.EnterJidDialogBinding; +import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.adapter.KnownHostsAdapter; import eu.siacs.conversations.ui.interfaces.OnBackendConnected; import eu.siacs.conversations.ui.util.DelayedHintHelper; @@ -29,234 +30,250 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected, TextWatcher { - - private static final List SUSPICIOUS_DOMAINS = Arrays.asList("conference","muc","room","rooms","chat"); - - private OnEnterJidDialogPositiveListener mListener = null; - - private static final String TITLE_KEY = "title"; - private static final String POSITIVE_BUTTON_KEY = "positive_button"; - private static final String PREFILLED_JID_KEY = "prefilled_jid"; - private static final String ACCOUNT_KEY = "account"; - private static final String ALLOW_EDIT_JID_KEY = "allow_edit_jid"; - private static final String ACCOUNTS_LIST_KEY = "activated_accounts_list"; - private static final String SANITY_CHECK_JID = "sanity_check_jid"; - - private KnownHostsAdapter knownHostsAdapter; - private Collection whitelistedDomains = Collections.emptyList(); - - private EnterJidDialogBinding binding; - private AlertDialog dialog; - private boolean sanityCheckJid = false; - - - private boolean issuedWarning = false; - - public static EnterJidDialog newInstance(final List activatedAccounts, - final String title, final String positiveButton, - final String prefilledJid, final String account, - boolean allowEditJid, final boolean sanity_check_jid) { - EnterJidDialog dialog = new EnterJidDialog(); - Bundle bundle = new Bundle(); - bundle.putString(TITLE_KEY, title); - bundle.putString(POSITIVE_BUTTON_KEY, positiveButton); - bundle.putString(PREFILLED_JID_KEY, prefilledJid); - bundle.putString(ACCOUNT_KEY, account); - bundle.putBoolean(ALLOW_EDIT_JID_KEY, allowEditJid); - bundle.putStringArrayList(ACCOUNTS_LIST_KEY, (ArrayList) activatedAccounts); - bundle.putBoolean(SANITY_CHECK_JID, sanity_check_jid); - dialog.setArguments(bundle); - return dialog; - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - setRetainInstance(true); - } - - @Override - public void onStart() { - super.onStart(); - final Activity activity = getActivity(); - if (activity instanceof XmppActivity && ((XmppActivity) activity).xmppConnectionService != null) { - refreshKnownHosts(); - } - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setTitle(getArguments().getString(TITLE_KEY)); - binding = DataBindingUtil.inflate(getActivity().getLayoutInflater(), R.layout.enter_jid_dialog, null, false); - this.knownHostsAdapter = new KnownHostsAdapter(getActivity(), R.layout.simple_list_item); - binding.jid.setAdapter(this.knownHostsAdapter); - binding.jid.addTextChangedListener(this); - String prefilledJid = getArguments().getString(PREFILLED_JID_KEY); - if (prefilledJid != null) { - binding.jid.append(prefilledJid); - if (!getArguments().getBoolean(ALLOW_EDIT_JID_KEY)) { - binding.jid.setFocusable(false); - binding.jid.setFocusableInTouchMode(false); - binding.jid.setClickable(false); - binding.jid.setCursorVisible(false); - } - } - sanityCheckJid = getArguments().getBoolean(SANITY_CHECK_JID, false); - - DelayedHintHelper.setHint(R.string.account_settings_example_jabber_id, binding.jid); - - String account = getArguments().getString(ACCOUNT_KEY); - if (account == null) { - StartConversationActivity.populateAccountSpinner(getActivity(), getArguments().getStringArrayList(ACCOUNTS_LIST_KEY), binding.account); - } else { - ArrayAdapter adapter = new ArrayAdapter<>(getActivity(), - R.layout.simple_list_item, - new String[]{account}); - binding.account.setEnabled(false); - adapter.setDropDownViewResource(R.layout.simple_list_item); - binding.account.setAdapter(adapter); - } - - - - builder.setView(binding.getRoot()); - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(getArguments().getString(POSITIVE_BUTTON_KEY), null); - this.dialog = builder.create(); - - View.OnClickListener dialogOnClick = v -> { - handleEnter(binding, account); - }; - - binding.jid.setOnEditorActionListener((v, actionId, event) -> { - handleEnter(binding, account); - return true; - }); - - dialog.show(); - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(dialogOnClick); - return dialog; - } - - private void handleEnter(EnterJidDialogBinding binding, String account) { - final Jid accountJid; - if (!binding.account.isEnabled() && account == null) { - return; - } - try { - if (Config.DOMAIN_LOCK != null) { - accountJid = Jid.ofEscaped((String) binding.account.getSelectedItem(), Config.DOMAIN_LOCK, null); - } else { - accountJid = Jid.ofEscaped((String) binding.account.getSelectedItem()); - } - } catch (final IllegalArgumentException e) { - return; - } - final Jid contactJid; - try { - contactJid = Jid.ofEscaped(binding.jid.getText().toString()); - } catch (final IllegalArgumentException e) { - binding.jidLayout.setError(getActivity().getString(R.string.invalid_jid)); - return; - } - - if (!issuedWarning && sanityCheckJid) { - if (contactJid.isDomainJid()) { - binding.jidLayout.setError(getActivity().getString(R.string.this_looks_like_a_domain)); - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add_anway); - issuedWarning = true; - return; - } - if (suspiciousSubDomain(contactJid.getDomain().toEscapedString())) { - binding.jidLayout.setError(getActivity().getString(R.string.this_looks_like_channel)); - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add_anway); - issuedWarning = true; - return; - } - } - - if (mListener != null) { - try { - if (mListener.onEnterJidDialogPositive(accountJid, contactJid)) { - dialog.dismiss(); - } - } catch (JidError error) { - binding.jidLayout.setError(error.toString()); - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add); - issuedWarning = false; - } - } - } - - public void setOnEnterJidDialogPositiveListener(OnEnterJidDialogPositiveListener listener) { - this.mListener = listener; - } - - @Override - public void onBackendConnected() { - refreshKnownHosts(); - } - - private void refreshKnownHosts() { - Activity activity = getActivity(); - if (activity instanceof XmppActivity) { - Collection hosts = ((XmppActivity) activity).xmppConnectionService.getKnownHosts(); - this.knownHostsAdapter.refresh(hosts); - this.whitelistedDomains = hosts; - } - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - - } - - @Override - public void afterTextChanged(Editable s) { - if (issuedWarning) { - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add); - binding.jidLayout.setError(null); - issuedWarning = false; - } - } - - public interface OnEnterJidDialogPositiveListener { - boolean onEnterJidDialogPositive(Jid account, Jid contact) throws EnterJidDialog.JidError; - } - - public static class JidError extends Exception { - final String msg; - - public JidError(final String msg) { - this.msg = msg; - } - - public String toString() { - return msg; - } - } - - @Override - public void onDestroyView() { - Dialog dialog = getDialog(); - if (dialog != null && getRetainInstance()) { - dialog.setDismissMessage(null); - } - super.onDestroyView(); - } - - private boolean suspiciousSubDomain(String domain) { - if (this.whitelistedDomains.contains(domain)) { - return false; - } - final String[] parts = domain.split("\\."); - return parts.length >= 3 && SUSPICIOUS_DOMAINS.contains(parts[0]); - } + private static final List SUSPICIOUS_DOMAINS = + Arrays.asList("conference", "muc", "room", "rooms", "chat"); + + private OnEnterJidDialogPositiveListener mListener = null; + + private static final String TITLE_KEY = "title"; + private static final String POSITIVE_BUTTON_KEY = "positive_button"; + private static final String PREFILLED_JID_KEY = "prefilled_jid"; + private static final String ACCOUNT_KEY = "account"; + private static final String ALLOW_EDIT_JID_KEY = "allow_edit_jid"; + private static final String ACCOUNTS_LIST_KEY = "activated_accounts_list"; + private static final String SANITY_CHECK_JID = "sanity_check_jid"; + + private KnownHostsAdapter knownHostsAdapter; + private Collection whitelistedDomains = Collections.emptyList(); + + private EnterJidDialogBinding binding; + private AlertDialog dialog; + private boolean sanityCheckJid = false; + + private boolean issuedWarning = false; + + public static EnterJidDialog newInstance( + final List activatedAccounts, + final String title, + final String positiveButton, + final String prefilledJid, + final String account, + boolean allowEditJid, + final boolean sanity_check_jid) { + EnterJidDialog dialog = new EnterJidDialog(); + Bundle bundle = new Bundle(); + bundle.putString(TITLE_KEY, title); + bundle.putString(POSITIVE_BUTTON_KEY, positiveButton); + bundle.putString(PREFILLED_JID_KEY, prefilledJid); + bundle.putString(ACCOUNT_KEY, account); + bundle.putBoolean(ALLOW_EDIT_JID_KEY, allowEditJid); + bundle.putStringArrayList(ACCOUNTS_LIST_KEY, (ArrayList) activatedAccounts); + bundle.putBoolean(SANITY_CHECK_JID, sanity_check_jid); + dialog.setArguments(bundle); + return dialog; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + setRetainInstance(true); + } + + @Override + public void onStart() { + super.onStart(); + final Activity activity = getActivity(); + if (activity instanceof XmppActivity + && ((XmppActivity) activity).xmppConnectionService != null) { + refreshKnownHosts(); + } + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle(getArguments().getString(TITLE_KEY)); + binding = + DataBindingUtil.inflate( + getActivity().getLayoutInflater(), R.layout.enter_jid_dialog, null, false); + this.knownHostsAdapter = new KnownHostsAdapter(getActivity(), R.layout.simple_list_item); + binding.jid.setAdapter(this.knownHostsAdapter); + binding.jid.addTextChangedListener(this); + String prefilledJid = getArguments().getString(PREFILLED_JID_KEY); + if (prefilledJid != null) { + binding.jid.append(prefilledJid); + if (!getArguments().getBoolean(ALLOW_EDIT_JID_KEY)) { + binding.jid.setFocusable(false); + binding.jid.setFocusableInTouchMode(false); + binding.jid.setClickable(false); + binding.jid.setCursorVisible(false); + } + } + sanityCheckJid = getArguments().getBoolean(SANITY_CHECK_JID, false); + + DelayedHintHelper.setHint(R.string.account_settings_example_jabber_id, binding.jid); + + String account = getArguments().getString(ACCOUNT_KEY); + if (account == null) { + StartConversationActivity.populateAccountSpinner( + getActivity(), + getArguments().getStringArrayList(ACCOUNTS_LIST_KEY), + binding.account); + } else { + ArrayAdapter adapter = + new ArrayAdapter<>( + getActivity(), R.layout.simple_list_item, new String[] {account}); + binding.account.setEnabled(false); + adapter.setDropDownViewResource(R.layout.simple_list_item); + binding.account.setAdapter(adapter); + } + + builder.setView(binding.getRoot()); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(getArguments().getString(POSITIVE_BUTTON_KEY), null); + this.dialog = builder.create(); + + View.OnClickListener dialogOnClick = + v -> { + handleEnter(binding, account); + }; + + binding.jid.setOnEditorActionListener( + (v, actionId, event) -> { + handleEnter(binding, account); + return true; + }); + + dialog.show(); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(dialogOnClick); + return dialog; + } + + private void handleEnter(EnterJidDialogBinding binding, String account) { + final Jid accountJid; + if (!binding.account.isEnabled() && account == null) { + return; + } + try { + if (Config.DOMAIN_LOCK != null) { + accountJid = + Jid.ofEscaped( + (String) binding.account.getSelectedItem(), + Config.DOMAIN_LOCK, + null); + } else { + accountJid = Jid.ofEscaped((String) binding.account.getSelectedItem()); + } + } catch (final IllegalArgumentException e) { + return; + } + final Jid contactJid; + try { + contactJid = Jid.ofEscaped(binding.jid.getText().toString()); + } catch (final IllegalArgumentException e) { + binding.jidLayout.setError(getActivity().getString(R.string.invalid_jid)); + return; + } + + if (!issuedWarning && sanityCheckJid) { + if (contactJid.isDomainJid()) { + binding.jidLayout.setError( + getActivity().getString(R.string.this_looks_like_a_domain)); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add_anway); + issuedWarning = true; + return; + } + if (suspiciousSubDomain(contactJid.getDomain().toEscapedString())) { + binding.jidLayout.setError( + getActivity().getString(R.string.this_looks_like_channel)); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add_anway); + issuedWarning = true; + return; + } + } + + if (mListener != null) { + try { + if (mListener.onEnterJidDialogPositive(accountJid, contactJid)) { + dialog.dismiss(); + } + } catch (JidError error) { + binding.jidLayout.setError(error.toString()); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add); + issuedWarning = false; + } + } + } + + public void setOnEnterJidDialogPositiveListener(OnEnterJidDialogPositiveListener listener) { + this.mListener = listener; + } + + @Override + public void onBackendConnected() { + refreshKnownHosts(); + } + + private void refreshKnownHosts() { + final Activity activity = getActivity(); + if (activity instanceof XmppActivity) { + final XmppConnectionService service = ((XmppActivity) activity).xmppConnectionService; + if (service == null) { + return; + } + final Collection hosts = service.getKnownHosts(); + this.knownHostsAdapter.refresh(hosts); + this.whitelistedDomains = hosts; + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + if (issuedWarning) { + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add); + binding.jidLayout.setError(null); + issuedWarning = false; + } + } + + public interface OnEnterJidDialogPositiveListener { + boolean onEnterJidDialogPositive(Jid account, Jid contact) throws EnterJidDialog.JidError; + } + + public static class JidError extends Exception { + final String msg; + + public JidError(final String msg) { + this.msg = msg; + } + + @NonNull + public String toString() { + return msg; + } + } + + @Override + public void onDestroyView() { + Dialog dialog = getDialog(); + if (dialog != null && getRetainInstance()) { + dialog.setDismissMessage(null); + } + super.onDestroyView(); + } + + private boolean suspiciousSubDomain(String domain) { + if (this.whitelistedDomains.contains(domain)) { + return false; + } + final String[] parts = domain.split("\\."); + return parts.length >= 3 && SUSPICIOUS_DOMAINS.contains(parts[0]); + } } From ad493938a03cadb4747613040bad9c28cf244789 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 23 Feb 2022 10:37:03 +0100 Subject: [PATCH 053/394] bump appcompat, migrate to emoji2 and get rid of emoji flavor --- build.gradle | 80 +++++-------------- .../ui/widget/EmojiWrapperEditText.java | 18 ----- .../conversations/utils/EmojiWrapper.java | 47 ----------- .../services/EmojiInitializationService.java | 14 ++++ .../ui/service/EmojiService.java | 27 ------- .../ui/ConferenceDetailsActivity.java | 7 +- .../ui/ConversationsActivity.java | 3 +- .../siacs/conversations/ui/XmppActivity.java | 4 +- .../ui/adapter/ConversationAdapter.java | 7 +- .../ui/adapter/ListItemAdapter.java | 3 +- .../ui/adapter/MessageAdapter.java | 5 +- .../conversations/ui/widget/EditMessage.java | 3 +- src/main/res/layout/activity_muc_details.xml | 4 +- .../res/layout/create_conference_dialog.xml | 2 +- .../layout/create_public_channel_dialog.xml | 2 +- src/main/res/layout/dialog_quickedit.xml | 4 +- .../services/EmojiInitializationService.java | 10 +++ .../ui/service/EmojiService.java | 55 ------------- src/playstoreCompat/res/values/font_certs.xml | 6 -- .../ui/service/EmojiService.java | 14 ---- .../ui/widget/EmojiWrapperEditText.java | 16 ---- .../conversations/utils/EmojiWrapper.java | 47 ----------- 22 files changed, 63 insertions(+), 315 deletions(-) delete mode 100644 src/compat/java/eu/siacs/conversations/ui/widget/EmojiWrapperEditText.java delete mode 100644 src/compat/java/eu/siacs/conversations/utils/EmojiWrapper.java create mode 100644 src/free/java/eu/siacs/conversations/services/EmojiInitializationService.java delete mode 100644 src/freeCompat/java/eu/siacs/conversations/ui/service/EmojiService.java create mode 100644 src/playstore/java/eu/siacs/conversations/services/EmojiInitializationService.java delete mode 100644 src/playstoreCompat/java/eu/siacs/conversations/ui/service/EmojiService.java delete mode 100644 src/playstoreCompat/res/values/font_certs.xml delete mode 100644 src/system/java/eu/siacs/conversations/ui/service/EmojiService.java delete mode 100644 src/system/java/eu/siacs/conversations/ui/widget/EmojiWrapperEditText.java delete mode 100644 src/system/java/eu/siacs/conversations/utils/EmojiWrapper.java diff --git a/build.gradle b/build.gradle index 6f26dc4fc..210fd1f61 100644 --- a/build.gradle +++ b/build.gradle @@ -20,13 +20,13 @@ repositories { configurations { playstoreImplementation - compatImplementation - conversationsFreeCompatImplementation - conversationsPlaystoreCompatImplementation - conversationsPlaystoreSystemImplementation - quicksyPlaystoreCompatImplementation - quicksyPlaystoreSystemImplementation - quicksyFreeCompatImplementation + freeImplementation + conversationsFreeImplementation + conversationsPlaystorImplementation + conversationsPlaystoreImplementation + quicksyPlaystoreImplementation + quicksyPlaystoreImplementation + quicksyFreeImplementation quicksyImplementation } @@ -38,21 +38,19 @@ dependencies { exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' } - conversationsPlaystoreCompatImplementation("com.android.installreferrer:installreferrer:2.2") - conversationsPlaystoreSystemImplementation("com.android.installreferrer:installreferrer:2.2") - quicksyPlaystoreCompatImplementation 'com.google.android.gms:play-services-auth-api-phone:18.0.1' - quicksyPlaystoreSystemImplementation 'com.google.android.gms:play-services-auth-api-phone:18.0.1' + conversationsPlaystoreImplementation("com.android.installreferrer:installreferrer:2.2") + quicksyPlaystoreImplementation 'com.google.android.gms:play-services-auth-api-phone:18.0.1' implementation 'org.sufficientlysecure:openpgp-api:10.0' implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0' - implementation 'androidx.appcompat:appcompat:1.3.1' + implementation 'androidx.appcompat:appcompat:1.4.1' implementation 'androidx.exifinterface:exifinterface:1.3.3' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' - implementation 'androidx.emoji:emoji:1.1.0' implementation 'com.google.android.material:material:1.4.0' - compatImplementation 'androidx.emoji:emoji-appcompat:1.1.0' - conversationsFreeCompatImplementation 'androidx.emoji:emoji-bundled:1.1.0' - quicksyFreeCompatImplementation 'androidx.emoji:emoji-bundled:1.1.0' + + implementation "androidx.emoji2:emoji2:1.1.0-rc01" + freeImplementation "androidx.emoji2:emoji2-bundled:1.1.0-rc01" + implementation 'org.bouncycastle:bcmail-jdk15on:1.64' //zxing stopped supporting Java 7 so we have to stick with 3.3.3 //https://github.com/zxing/zxing/issues/1170 @@ -122,7 +120,7 @@ android { targetCompatibility JavaVersion.VERSION_1_8 } - flavorDimensions("mode", "distribution", "emoji") + flavorDimensions("mode", "distribution") productFlavors { @@ -148,39 +146,15 @@ android { dimension "distribution" versionNameSuffix "+f" } - system { - dimension "emoji" - versionNameSuffix "s" - } - compat { - dimension "emoji" - versionNameSuffix "c" - } } sourceSets { - quicksyFreeSystem { + quicksyFree { java { srcDir 'src/quicksyFree/java' } } - quicksyFreeCompat { - java { - srcDir 'src/freeCompat/java' - srcDir 'src/quicksyFree/java' - } - } - quicksyPlaystoreCompat { - java { - srcDir 'src/playstoreCompat/java' - srcDir 'src/quicksyPlaystore/java' - } - res { - srcDir 'src/playstoreCompat/res' - srcDir 'src/quicksyPlaystore/res' - } - } - quicksyPlaystoreSystem { + quicksyPlaystore { java { srcDir 'src/quicksyPlaystore/java' } @@ -188,28 +162,12 @@ android { srcDir 'src/quicksyPlaystore/res' } } - conversationsFreeCompat { + conversationsFree { java { - srcDir 'src/freeCompat/java' srcDir 'src/conversationsFree/java' } } - conversationsFreeSystem { - java { - srcDir 'src/conversationsFree/java' - } - } - conversationsPlaystoreCompat { - java { - srcDir 'src/playstoreCompat/java' - srcDir 'src/conversationsPlaystore/java' - } - res { - srcDir 'src/playstoreCompat/res' - srcDir 'src/conversationsPlaystore/res' - } - } - conversationsPlaystoreSystem { + conversationsPlaystore { java { srcDir 'src/conversationsPlaystore/java' } diff --git a/src/compat/java/eu/siacs/conversations/ui/widget/EmojiWrapperEditText.java b/src/compat/java/eu/siacs/conversations/ui/widget/EmojiWrapperEditText.java deleted file mode 100644 index 01905e376..000000000 --- a/src/compat/java/eu/siacs/conversations/ui/widget/EmojiWrapperEditText.java +++ /dev/null @@ -1,18 +0,0 @@ -package eu.siacs.conversations.ui.widget; - -import android.content.Context; -import android.util.AttributeSet; - -import androidx.emoji.widget.EmojiAppCompatEditText; - -public class EmojiWrapperEditText extends EmojiAppCompatEditText { - - public EmojiWrapperEditText(Context context) { - super(context); - } - - public EmojiWrapperEditText(Context context, AttributeSet attrs) { - super(context, attrs); - } - -} \ No newline at end of file diff --git a/src/compat/java/eu/siacs/conversations/utils/EmojiWrapper.java b/src/compat/java/eu/siacs/conversations/utils/EmojiWrapper.java deleted file mode 100644 index 3b6cf71e1..000000000 --- a/src/compat/java/eu/siacs/conversations/utils/EmojiWrapper.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2017, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.utils; - -import androidx.emoji.text.EmojiCompat; - -public class EmojiWrapper { - - public static CharSequence transform(CharSequence input) { - try { - if (EmojiCompat.get().getLoadState() == EmojiCompat.LOAD_STATE_SUCCEEDED) { - return EmojiCompat.get().process(input); - } else { - return input; - } - } catch (IllegalStateException e) { - return input; - } - } -} diff --git a/src/free/java/eu/siacs/conversations/services/EmojiInitializationService.java b/src/free/java/eu/siacs/conversations/services/EmojiInitializationService.java new file mode 100644 index 000000000..2618d3809 --- /dev/null +++ b/src/free/java/eu/siacs/conversations/services/EmojiInitializationService.java @@ -0,0 +1,14 @@ +package eu.siacs.conversations.services; + +import android.content.Context; + +import androidx.emoji2.bundled.BundledEmojiCompatConfig; +import androidx.emoji2.text.EmojiCompat; + +public class EmojiInitializationService { + + public static void execute(final Context context) { + EmojiCompat.init(new BundledEmojiCompatConfig(context).setReplaceAll(true)); + } + +} diff --git a/src/freeCompat/java/eu/siacs/conversations/ui/service/EmojiService.java b/src/freeCompat/java/eu/siacs/conversations/ui/service/EmojiService.java deleted file mode 100644 index 1f60368bb..000000000 --- a/src/freeCompat/java/eu/siacs/conversations/ui/service/EmojiService.java +++ /dev/null @@ -1,27 +0,0 @@ -package eu.siacs.conversations.ui.service; - -import android.content.Context; -import android.os.Build; -import androidx.emoji.text.EmojiCompat; -import androidx.emoji.text.FontRequestEmojiCompatConfig; -import androidx.emoji.bundled.BundledEmojiCompatConfig; - -public class EmojiService { - - private final Context context; - - public EmojiService(Context context) { - this.context = context; - } - - public void init() { - BundledEmojiCompatConfig config = new BundledEmojiCompatConfig(context); - //On recent Androids we assume to have the latest emojis - //there are some annoying bugs with emoji compat that make it a safer choice not to use it when possible - // a) the text preview has annoying glitches when the cut of text contains emojis (the emoji will be half visible) - // b) can trigger a hardware rendering bug https://issuetracker.google.com/issues/67102093 - config.setReplaceAll(Build.VERSION.SDK_INT < Build.VERSION_CODES.O); - EmojiCompat.init(config); - } - -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index d35d4808c..fb716044c 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -46,7 +46,6 @@ import eu.siacs.conversations.ui.util.SoftKeyboardUtils; import eu.siacs.conversations.utils.AccountUtils; import eu.siacs.conversations.utils.Compatibility; -import eu.siacs.conversations.utils.EmojiWrapper; import eu.siacs.conversations.utils.StringUtils; import eu.siacs.conversations.utils.StylingHelper; import eu.siacs.conversations.utils.XmppUri; @@ -471,11 +470,11 @@ private void updateView() { String subject = mucOptions.getSubject(); final boolean hasTitle; if (printableValue(roomName)) { - this.binding.mucTitle.setText(EmojiWrapper.transform(roomName)); + this.binding.mucTitle.setText(roomName); this.binding.mucTitle.setVisibility(View.VISIBLE); hasTitle = true; } else if (!printableValue(subject)) { - this.binding.mucTitle.setText(EmojiWrapper.transform(mConversation.getName())); + this.binding.mucTitle.setText(mConversation.getName()); hasTitle = true; this.binding.mucTitle.setVisibility(View.VISIBLE); } else { @@ -486,7 +485,7 @@ private void updateView() { SpannableStringBuilder spannable = new SpannableStringBuilder(subject); StylingHelper.format(spannable, this.binding.mucSubject.getCurrentTextColor()); MyLinkify.addLinks(spannable, false); - this.binding.mucSubject.setText(EmojiWrapper.transform(spannable)); + this.binding.mucSubject.setText(spannable); this.binding.mucSubject.setTextAppearance(this, subject.length() > (hasTitle ? 128 : 196) ? R.style.TextAppearance_Conversations_Body1_Linkified : R.style.TextAppearance_Conversations_Subhead); this.binding.mucSubject.setAutoLinkMask(0); this.binding.mucSubject.setVisibility(View.VISIBLE); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java index cc46ed33f..308e139fc 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java @@ -81,7 +81,6 @@ import eu.siacs.conversations.ui.util.ConversationMenuConfigurator; import eu.siacs.conversations.ui.util.MenuDoubleTabUtil; import eu.siacs.conversations.ui.util.PendingItem; -import eu.siacs.conversations.utils.EmojiWrapper; import eu.siacs.conversations.utils.ExceptionHelper; import eu.siacs.conversations.utils.SignupUtils; import eu.siacs.conversations.utils.XmppUri; @@ -625,7 +624,7 @@ private void invalidateActionBarTitle() { if (mainFragment instanceof ConversationFragment) { final Conversation conversation = ((ConversationFragment) mainFragment).getConversation(); if (conversation != null) { - actionBar.setTitle(EmojiWrapper.transform(conversation.getName())); + actionBar.setTitle(conversation.getName()); actionBar.setDisplayHomeAsUpEnabled(true); ActionBarUtil.setActionBarOnClickListener( binding.toolbar, diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 4b5382b44..4ca49fa50 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -71,10 +71,10 @@ import eu.siacs.conversations.entities.Presences; import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.BarcodeProvider; +import eu.siacs.conversations.services.EmojiInitializationService; import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder; -import eu.siacs.conversations.ui.service.EmojiService; import eu.siacs.conversations.ui.util.MenuDoubleTabUtil; import eu.siacs.conversations.ui.util.PresenceSelector; import eu.siacs.conversations.ui.util.SoftKeyboardUtils; @@ -408,7 +408,7 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); metrics = getResources().getDisplayMetrics(); ExceptionHelper.init(getApplicationContext()); - new EmojiService(this).init(); + EmojiInitializationService.execute(this); this.isCameraFeatureAvailable = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); this.mTheme = findTheme(); setTheme(this.mTheme); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java index 049703597..662120d84 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java @@ -23,7 +23,6 @@ import eu.siacs.conversations.ui.XmppActivity; import eu.siacs.conversations.ui.util.AvatarWorkerTask; import eu.siacs.conversations.ui.util.StyledAttributes; -import eu.siacs.conversations.utils.EmojiWrapper; import eu.siacs.conversations.utils.IrregularUnicodeDetector; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.Jid; @@ -57,7 +56,7 @@ public void onBindViewHolder(@NonNull ConversationViewHolder viewHolder, int pos if (name instanceof Jid) { viewHolder.binding.conversationName.setText(IrregularUnicodeDetector.style(activity, (Jid) name)); } else { - viewHolder.binding.conversationName.setText(EmojiWrapper.transform(name)); + viewHolder.binding.conversationName.setText(name); } if (conversation == ConversationFragment.getConversation(activity)) { @@ -85,7 +84,7 @@ public void onBindViewHolder(@NonNull ConversationViewHolder viewHolder, int pos if (draft != null) { viewHolder.binding.conversationLastmsgImg.setVisibility(View.GONE); - viewHolder.binding.conversationLastmsg.setText(EmojiWrapper.transform(draft.getMessage())); + viewHolder.binding.conversationLastmsg.setText(draft.getMessage()); viewHolder.binding.senderName.setText(R.string.draft); viewHolder.binding.senderName.setVisibility(View.VISIBLE); viewHolder.binding.conversationLastmsg.setTypeface(null, Typeface.NORMAL); @@ -128,7 +127,7 @@ public void onBindViewHolder(@NonNull ConversationViewHolder viewHolder, int pos } final Pair preview = UIHelper.getMessagePreview(activity, message, viewHolder.binding.conversationLastmsg.getCurrentTextColor()); if (showPreviewText) { - viewHolder.binding.conversationLastmsg.setText(EmojiWrapper.transform(UIHelper.shorten(preview.first))); + viewHolder.binding.conversationLastmsg.setText(UIHelper.shorten(preview.first)); } else { viewHolder.binding.conversationLastmsgImg.setContentDescription(preview.first); } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java index 4e7213380..5d6d72684 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java @@ -22,7 +22,6 @@ import eu.siacs.conversations.ui.XmppActivity; import eu.siacs.conversations.ui.util.AvatarWorkerTask; import eu.siacs.conversations.ui.util.StyledAttributes; -import eu.siacs.conversations.utils.EmojiWrapper; import eu.siacs.conversations.utils.IrregularUnicodeDetector; import eu.siacs.conversations.xmpp.Jid; @@ -85,7 +84,7 @@ public View getView(int position, View view, ViewGroup parent) { } else { viewHolder.jid.setVisibility(View.GONE); } - viewHolder.name.setText(EmojiWrapper.transform(item.getDisplayName())); + viewHolder.name.setText(item.getDisplayName()); AvatarWorkerTask.loadAvatar(item, viewHolder.avatar, R.dimen.avatar); return view; } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index ccb40418a..a5ba05819 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -63,7 +63,6 @@ import eu.siacs.conversations.ui.util.ViewUtil; import eu.siacs.conversations.ui.widget.ClickableMovementMethod; import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.utils.EmojiWrapper; import eu.siacs.conversations.utils.Emoticons; import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.MessageUtils; @@ -335,7 +334,7 @@ private void displayEmojiMessage(final ViewHolder viewHolder, final String body, Spannable span = new SpannableString(body); float size = Emoticons.isEmoji(body) ? 3.0f : 2.0f; span.setSpan(new RelativeSizeSpan(size), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - viewHolder.messageBody.setText(EmojiWrapper.transform(span)); + viewHolder.messageBody.setText(span); } private void applyQuoteSpan(SpannableStringBuilder body, int start, int end, boolean darkBackground) { @@ -494,7 +493,7 @@ private void displayTextMessage(final ViewHolder viewHolder, final Message messa } MyLinkify.addLinks(body, true); viewHolder.messageBody.setAutoLinkMask(0); - viewHolder.messageBody.setText(EmojiWrapper.transform(body)); + viewHolder.messageBody.setText(body); viewHolder.messageBody.setMovementMethod(ClickableMovementMethod.getInstance()); } else { viewHolder.messageBody.setText(""); diff --git a/src/main/java/eu/siacs/conversations/ui/widget/EditMessage.java b/src/main/java/eu/siacs/conversations/ui/widget/EditMessage.java index eba833c9b..e890e5984 100644 --- a/src/main/java/eu/siacs/conversations/ui/widget/EditMessage.java +++ b/src/main/java/eu/siacs/conversations/ui/widget/EditMessage.java @@ -15,6 +15,7 @@ import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; +import androidx.appcompat.widget.AppCompatEditText; import androidx.core.view.inputmethod.EditorInfoCompat; import androidx.core.view.inputmethod.InputConnectionCompat; import androidx.core.view.inputmethod.InputContentInfoCompat; @@ -26,7 +27,7 @@ import eu.siacs.conversations.R; import eu.siacs.conversations.ui.util.QuoteHelper; -public class EditMessage extends EmojiWrapperEditText { +public class EditMessage extends AppCompatEditText { private static final InputFilter SPAN_FILTER = (source, start, end, dest, dstart, dend) -> source instanceof Spanned ? source.toString() : source; private final ExecutorService executor = Executors.newSingleThreadExecutor(); diff --git a/src/main/res/layout/activity_muc_details.xml b/src/main/res/layout/activity_muc_details.xml index d19bf93e9..f2b4b00d7 100644 --- a/src/main/res/layout/activity_muc_details.xml +++ b/src/main/res/layout/activity_muc_details.xml @@ -106,7 +106,7 @@ app:errorTextAppearance="@style/TextAppearance.Conversations.Design.Error" app:hintTextAppearance="@style/TextAppearance.Conversations.Design.Hint"> - - - - - - + diff --git a/src/playstore/java/eu/siacs/conversations/services/EmojiInitializationService.java b/src/playstore/java/eu/siacs/conversations/services/EmojiInitializationService.java new file mode 100644 index 000000000..b5a57d374 --- /dev/null +++ b/src/playstore/java/eu/siacs/conversations/services/EmojiInitializationService.java @@ -0,0 +1,10 @@ +package eu.siacs.conversations.services; + +import android.content.Context; + +public class EmojiInitializationService { + + public static void execute(final Context context) { + + } +} diff --git a/src/playstoreCompat/java/eu/siacs/conversations/ui/service/EmojiService.java b/src/playstoreCompat/java/eu/siacs/conversations/ui/service/EmojiService.java deleted file mode 100644 index 5ed8c100a..000000000 --- a/src/playstoreCompat/java/eu/siacs/conversations/ui/service/EmojiService.java +++ /dev/null @@ -1,55 +0,0 @@ -package eu.siacs.conversations.ui.service; - -import android.content.Context; -import android.os.Build; -import android.util.Log; - -import androidx.core.provider.FontRequest; -import androidx.emoji.text.EmojiCompat; -import androidx.emoji.text.FontRequestEmojiCompatConfig; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; - -public class EmojiService { - - - private final EmojiCompat.InitCallback initCallback = new EmojiCompat.InitCallback() { - @Override - public void onInitialized() { - super.onInitialized(); - Log.d(Config.LOGTAG, "EmojiService succeeded in loading fonts"); - - } - - @Override - public void onFailed(Throwable throwable) { - super.onFailed(throwable); - Log.d(Config.LOGTAG, "EmojiService failed to load fonts", throwable); - } - }; - - private final Context context; - - public EmojiService(Context context) { - this.context = context; - } - - public void init() { - final FontRequest fontRequest = new FontRequest( - "com.google.android.gms.fonts", - "com.google.android.gms", - "Noto Color Emoji Compat", - R.array.font_certs); - FontRequestEmojiCompatConfig fontRequestEmojiCompatConfig = new FontRequestEmojiCompatConfig(context, fontRequest); - fontRequestEmojiCompatConfig.registerInitCallback(initCallback); - //On recent Androids we assume to have the latest emojis - //there are some annoying bugs with emoji compat that make it a safer choice not to use it when possible - // a) when using the ondemand emoji font (play store) flags don’t work - // b) the text preview has annoying glitches when the cut of text contains emojis (the emoji will be half visible) - // c) can trigger a hardware rendering bug https://issuetracker.google.com/issues/67102093 - fontRequestEmojiCompatConfig.setReplaceAll(Build.VERSION.SDK_INT < Build.VERSION_CODES.O); - EmojiCompat.init(fontRequestEmojiCompatConfig); - } - -} \ No newline at end of file diff --git a/src/playstoreCompat/res/values/font_certs.xml b/src/playstoreCompat/res/values/font_certs.xml deleted file mode 100644 index cc0ad3b3c..000000000 --- a/src/playstoreCompat/res/values/font_certs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK - - \ No newline at end of file diff --git a/src/system/java/eu/siacs/conversations/ui/service/EmojiService.java b/src/system/java/eu/siacs/conversations/ui/service/EmojiService.java deleted file mode 100644 index 6ca66fd62..000000000 --- a/src/system/java/eu/siacs/conversations/ui/service/EmojiService.java +++ /dev/null @@ -1,14 +0,0 @@ -package eu.siacs.conversations.ui.service; - -import android.content.Context; - -public class EmojiService { - - public EmojiService(Context context) { - //nop - } - - public void init() { - //nop - } -} \ No newline at end of file diff --git a/src/system/java/eu/siacs/conversations/ui/widget/EmojiWrapperEditText.java b/src/system/java/eu/siacs/conversations/ui/widget/EmojiWrapperEditText.java deleted file mode 100644 index 58e1ab318..000000000 --- a/src/system/java/eu/siacs/conversations/ui/widget/EmojiWrapperEditText.java +++ /dev/null @@ -1,16 +0,0 @@ -package eu.siacs.conversations.ui.widget; - -import android.content.Context; -import androidx.appcompat.widget.AppCompatEditText; -import android.util.AttributeSet; - -public class EmojiWrapperEditText extends AppCompatEditText { - - public EmojiWrapperEditText(Context context) { - super(context); - } - - public EmojiWrapperEditText(Context context, AttributeSet attrs) { - super(context, attrs); - } -} \ No newline at end of file diff --git a/src/system/java/eu/siacs/conversations/utils/EmojiWrapper.java b/src/system/java/eu/siacs/conversations/utils/EmojiWrapper.java deleted file mode 100644 index 3b6cf71e1..000000000 --- a/src/system/java/eu/siacs/conversations/utils/EmojiWrapper.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2017, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.utils; - -import androidx.emoji.text.EmojiCompat; - -public class EmojiWrapper { - - public static CharSequence transform(CharSequence input) { - try { - if (EmojiCompat.get().getLoadState() == EmojiCompat.LOAD_STATE_SUCCEEDED) { - return EmojiCompat.get().process(input); - } else { - return input; - } - } catch (IllegalStateException e) { - return input; - } - } -} From 3534c619fbd5d5e78f349ed13f8d10612af76114 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 23 Feb 2022 11:03:56 +0100 Subject: [PATCH 054/394] rename version suffix to playstore/free --- build.gradle | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 210fd1f61..67e995bea 100644 --- a/build.gradle +++ b/build.gradle @@ -140,11 +140,11 @@ android { playstore { dimension "distribution" - versionNameSuffix "+p" + versionNameSuffix "+playstore" } free { dimension "distribution" - versionNameSuffix "+f" + versionNameSuffix "+free" } } @@ -182,13 +182,11 @@ android { shrinkResources true minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - versionNameSuffix "r" } debug { shrinkResources true minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - versionNameSuffix "d" } } From 48f8c1a6a0115bd2fd9782a9a4344e29b159a7bc Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 23 Feb 2022 11:37:48 +0100 Subject: [PATCH 055/394] use try with resources. remove unused methods --- .../persistance/FileBackend.java | 50 ++++--------------- 1 file changed, 11 insertions(+), 39 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 1ee5191db..0648aaa62 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -186,11 +186,6 @@ private static Paint createAntiAliasingPaint() { return paint; } - private static String getTakePhotoPath() { - return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) - + "/Camera/"; - } - public static Uri getUriForUri(Context context, Uri uri) { if ("file".equals(uri.getScheme())) { return getUriForFile(context, new File(uri.getPath())); @@ -474,19 +469,6 @@ public Bitmap getPreviewForUri(Attachment attachment, int size, boolean cacheOnl return bitmap; } - private void createNoMedia(File diretory) { - final File noMedia = new File(diretory, ".nomedia"); - if (!noMedia.exists()) { - try { - if (!noMedia.createNewFile()) { - Log.d(Config.LOGTAG, "created nomedia file " + noMedia.getAbsolutePath()); - } - } catch (Exception e) { - Log.d(Config.LOGTAG, "could not create nomedia file"); - } - } - } - public void updateMediaScanner(File file) { updateMediaScanner(file, null); } @@ -724,28 +706,18 @@ public void copyFileToPrivateStorage(Message message, Uri uri, String type) copyFileToPrivateStorage(mXmppConnectionService.getFileBackend().getFile(message), uri); } - private String getExtensionFromUri(Uri uri) { - String[] projection = {MediaStore.MediaColumns.DATA}; + private String getExtensionFromUri(final Uri uri) { + final String[] projection = {MediaStore.MediaColumns.DATA}; String filename = null; - Cursor cursor; - try { - cursor = - mXmppConnectionService - .getContentResolver() - .query(uri, projection, null, null, null); - } catch (IllegalArgumentException e) { - cursor = null; - } - if (cursor != null) { - try { - if (cursor.moveToFirst()) { - filename = cursor.getString(0); - } - } catch (Exception e) { - filename = null; - } finally { - cursor.close(); + try (final Cursor cursor = + mXmppConnectionService + .getContentResolver() + .query(uri, projection, null, null, null)) { + if (cursor != null && cursor.moveToFirst()) { + filename = cursor.getString(0); } + } catch (final SecurityException | IllegalArgumentException e) { + filename = null; } if (filename == null) { final List segments = uri.getPathSegments(); @@ -753,7 +725,7 @@ private String getExtensionFromUri(Uri uri) { filename = segments.get(segments.size() - 1); } } - int pos = filename == null ? -1 : filename.lastIndexOf('.'); + final int pos = filename == null ? -1 : filename.lastIndexOf('.'); return pos > 0 ? filename.substring(pos + 1) : null; } From 35c54f0ae968ba76b78fed6c7081d52d2404c9ef Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 23 Feb 2022 12:16:49 +0100 Subject: [PATCH 056/394] delete pre lolipop weOwnFile() --- .../persistance/FileBackend.java | 21 ++----------------- .../ui/ConversationFragment.java | 4 ++-- 2 files changed, 4 insertions(+), 21 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 0648aaa62..a5a904c75 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -354,32 +354,15 @@ public static void close(final ServerSocket socket) { } } - public static boolean weOwnFile(Context context, Uri uri) { + public static boolean weOwnFile(final Uri uri) { if (uri == null || !ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { return false; - } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - return fileIsInFilesDir(context, uri); } else { return weOwnFileLollipop(uri); } } - /** - * This is more than hacky but probably way better than doing nothing Further 'optimizations' - * might contain to get the parents of CacheDir and NoBackupDir and check against those as well - */ - private static boolean fileIsInFilesDir(Context context, Uri uri) { - try { - final String haystack = context.getFilesDir().getParentFile().getCanonicalPath(); - final String needle = new File(uri.getPath()).getCanonicalPath(); - return needle.startsWith(haystack); - } catch (IOException e) { - return false; - } - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - private static boolean weOwnFileLollipop(Uri uri) { + private static boolean weOwnFileLollipop(final Uri uri) { try { File file = new File(uri.getPath()); FileDescriptor fd = diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index a42fa6766..073e77cc3 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -2255,10 +2255,10 @@ private List extractUris(final Bundle extras) { } private List cleanUris(final List uris) { - Iterator iterator = uris.iterator(); + final Iterator iterator = uris.iterator(); while (iterator.hasNext()) { final Uri uri = iterator.next(); - if (FileBackend.weOwnFile(getActivity(), uri)) { + if (FileBackend.weOwnFile(uri)) { iterator.remove(); Toast.makeText(getActivity(), R.string.security_violation_not_attaching_file, Toast.LENGTH_SHORT).show(); } From a3085fbf1f143149cb40cfda085199fb4fe22992 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 23 Feb 2022 15:57:29 +0100 Subject: [PATCH 057/394] do not restart wakelock if activity is finishing --- .../conversations/ui/RtpSessionActivity.java | 508 +++++++++++------- 1 file changed, 311 insertions(+), 197 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 65beae35d..a27794c30 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -69,7 +69,9 @@ import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; -public class RtpSessionActivity extends XmppActivity implements XmppConnectionService.OnJingleRtpConnectionUpdate, eu.siacs.conversations.ui.widget.SurfaceViewRenderer.OnAspectRatioChanged { +public class RtpSessionActivity extends XmppActivity + implements XmppConnectionService.OnJingleRtpConnectionUpdate, + eu.siacs.conversations.ui.widget.SurfaceViewRenderer.OnAspectRatioChanged { public static final String EXTRA_WITH = "with"; public static final String EXTRA_SESSION_ID = "session_id"; @@ -81,33 +83,31 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private static final int CALL_DURATION_UPDATE_INTERVAL = 333; - private static final List END_CARD = Arrays.asList( - RtpEndUserState.APPLICATION_ERROR, - RtpEndUserState.SECURITY_ERROR, - RtpEndUserState.DECLINED_OR_BUSY, - RtpEndUserState.CONNECTIVITY_ERROR, - RtpEndUserState.CONNECTIVITY_LOST_ERROR, - RtpEndUserState.RETRACTED - ); - private static final List STATES_SHOWING_HELP_BUTTON = Arrays.asList( - RtpEndUserState.APPLICATION_ERROR, - RtpEndUserState.CONNECTIVITY_ERROR, - RtpEndUserState.SECURITY_ERROR - ); - private static final List STATES_SHOWING_SWITCH_TO_CHAT = Arrays.asList( - RtpEndUserState.CONNECTING, - RtpEndUserState.CONNECTED, - RtpEndUserState.RECONNECTING - ); - private static final List STATES_CONSIDERED_CONNECTED = Arrays.asList( - RtpEndUserState.CONNECTED, - RtpEndUserState.RECONNECTING - ); - private static final List STATES_SHOWING_PIP_PLACEHOLDER = Arrays.asList( - RtpEndUserState.ACCEPTING_CALL, - RtpEndUserState.CONNECTING, - RtpEndUserState.RECONNECTING - ); + private static final List END_CARD = + Arrays.asList( + RtpEndUserState.APPLICATION_ERROR, + RtpEndUserState.SECURITY_ERROR, + RtpEndUserState.DECLINED_OR_BUSY, + RtpEndUserState.CONNECTIVITY_ERROR, + RtpEndUserState.CONNECTIVITY_LOST_ERROR, + RtpEndUserState.RETRACTED); + private static final List STATES_SHOWING_HELP_BUTTON = + Arrays.asList( + RtpEndUserState.APPLICATION_ERROR, + RtpEndUserState.CONNECTIVITY_ERROR, + RtpEndUserState.SECURITY_ERROR); + private static final List STATES_SHOWING_SWITCH_TO_CHAT = + Arrays.asList( + RtpEndUserState.CONNECTING, + RtpEndUserState.CONNECTED, + RtpEndUserState.RECONNECTING); + private static final List STATES_CONSIDERED_CONNECTED = + Arrays.asList(RtpEndUserState.CONNECTED, RtpEndUserState.RECONNECTING); + private static final List STATES_SHOWING_PIP_PLACEHOLDER = + Arrays.asList( + RtpEndUserState.ACCEPTING_CALL, + RtpEndUserState.CONNECTING, + RtpEndUserState.RECONNECTING); private static final String PROXIMITY_WAKE_LOCK_TAG = "conversations:in-rtp-session"; private static final int REQUEST_ACCEPT_CALL = 0x1111; private WeakReference rtpConnectionReference; @@ -116,13 +116,14 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private PowerManager.WakeLock mProximityWakeLock; private final Handler mHandler = new Handler(); - private final Runnable mTickExecutor = new Runnable() { - @Override - public void run() { - updateCallDuration(); - mHandler.postDelayed(mTickExecutor, CALL_DURATION_UPDATE_INTERVAL); - } - }; + private final Runnable mTickExecutor = + new Runnable() { + @Override + public void run() { + updateCallDuration(); + mHandler.postDelayed(mTickExecutor, CALL_DURATION_UPDATE_INTERVAL); + } + }; private static Set actionToMedia(final String action) { if (ACTION_MAKE_VIDEO_CALL.equals(action)) { @@ -132,21 +133,27 @@ private static Set actionToMedia(final String action) { } } - private static void addSink(final VideoTrack videoTrack, final SurfaceViewRenderer surfaceViewRenderer) { + private static void addSink( + final VideoTrack videoTrack, final SurfaceViewRenderer surfaceViewRenderer) { try { videoTrack.addSink(surfaceViewRenderer); } catch (final IllegalStateException e) { - Log.e(Config.LOGTAG, "possible race condition on trying to display video track. ignoring", e); + Log.e( + Config.LOGTAG, + "possible race condition on trying to display video track. ignoring", + e); } } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON - | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD - | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED - | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); + getWindow() + .addFlags( + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD + | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); this.binding = DataBindingUtil.setContentView(this, R.layout.activity_rtp_session); setSupportActionBar(binding.toolbar); } @@ -178,7 +185,8 @@ private boolean isHelpButtonVisible() { return STATES_SHOWING_HELP_BUTTON.contains(requireRtpConnection().getEndUserState()); } catch (IllegalStateException e) { final Intent intent = getIntent(); - final String state = intent != null ? intent.getStringExtra(EXTRA_LAST_REPORTED_STATE) : null; + final String state = + intent != null ? intent.getStringExtra(EXTRA_LAST_REPORTED_STATE) : null; if (state != null) { return STATES_SHOWING_HELP_BUTTON.contains(RtpEndUserState.valueOf(state)); } else { @@ -188,13 +196,17 @@ private boolean isHelpButtonVisible() { } private boolean isSwitchToConversationVisible() { - final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; - return connection != null && STATES_SHOWING_SWITCH_TO_CHAT.contains(connection.getEndUserState()); + final JingleRtpConnection connection = + this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; + return connection != null + && STATES_SHOWING_SWITCH_TO_CHAT.contains(connection.getEndUserState()); } private void switchToConversation() { final Contact contact = getWith(); - final Conversation conversation = xmppConnectionService.findOrCreateConversation(contact.getAccount(), contact.getJid(), false, true); + final Conversation conversation = + xmppConnectionService.findOrCreateConversation( + contact.getAccount(), contact.getJid(), false, true); switchToConversation(conversation); } @@ -215,7 +227,8 @@ private void launchHelpInBrowser() { try { startActivity(intent); } catch (final ActivityNotFoundException e) { - Toast.makeText(this, R.string.no_application_found_to_open_link, Toast.LENGTH_LONG).show(); + Toast.makeText(this, R.string.no_application_found_to_open_link, Toast.LENGTH_LONG) + .show(); } } @@ -238,10 +251,15 @@ private void retractSessionProposal() { final Account account = extractAccount(intent); final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH)); final String state = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE); - if (!Intent.ACTION_VIEW.equals(action) || state == null || !END_CARD.contains(RtpEndUserState.valueOf(state))) { - resetIntent(account, with, RtpEndUserState.RETRACTED, actionToMedia(intent.getAction())); + if (!Intent.ACTION_VIEW.equals(action) + || state == null + || !END_CARD.contains(RtpEndUserState.valueOf(state))) { + resetIntent( + account, with, RtpEndUserState.RETRACTED, actionToMedia(intent.getAction())); } - xmppConnectionService.getJingleConnectionManager().retractSessionProposal(account, with.asBareJid()); + xmppConnectionService + .getJingleConnectionManager() + .retractSessionProposal(account, with.asBareJid()); } private void rejectCall(View view) { @@ -256,7 +274,8 @@ private void acceptCall(View view) { private void requestPermissionsAndAcceptCall() { final List permissions; if (getMedia().contains(Media.VIDEO)) { - permissions = ImmutableList.of(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO); + permissions = + ImmutableList.of(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO); } else { permissions = ImmutableList.of(Manifest.permission.RECORD_AUDIO); } @@ -267,7 +286,7 @@ private void requestPermissionsAndAcceptCall() { } private void checkRecorderAndAcceptCall() { - checkMicrophoneAvailability(); + checkMicrophoneAvailabilityAsync(); try { requireRtpConnection().acceptCall(); } catch (final IllegalStateException e) { @@ -275,18 +294,22 @@ private void checkRecorderAndAcceptCall() { } } + private void checkMicrophoneAvailabilityAsync() { + new Thread(this::checkMicrophoneAvailability).start(); + } + private void checkMicrophoneAvailability() { - new Thread(() -> { - final long start = SystemClock.elapsedRealtime(); - final boolean isMicrophoneAvailable = AppRTCAudioManager.isMicrophoneAvailable(); - final long stop = SystemClock.elapsedRealtime(); - Log.d(Config.LOGTAG, "checking microphone availability took " + (stop - start) + "ms"); - if (isMicrophoneAvailable) { - return; - } - runOnUiThread(() -> Toast.makeText(this, R.string.microphone_unavailable, Toast.LENGTH_LONG).show()); + final long start = SystemClock.elapsedRealtime(); + final boolean isMicrophoneAvailable = AppRTCAudioManager.isMicrophoneAvailable(); + final long stop = SystemClock.elapsedRealtime(); + Log.d(Config.LOGTAG, "checking microphone availability took " + (stop - start) + "ms"); + if (isMicrophoneAvailable) { + return; } - ).start(); + runOnUiThread( + () -> + Toast.makeText(this, R.string.microphone_unavailable, Toast.LENGTH_LONG) + .show()); } private void putScreenInCallMode() { @@ -296,9 +319,13 @@ private void putScreenInCallMode() { private void putScreenInCallMode(final Set media) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); if (!media.contains(Media.VIDEO)) { - final JingleRtpConnection rtpConnection = rtpConnectionReference != null ? rtpConnectionReference.get() : null; - final AppRTCAudioManager audioManager = rtpConnection == null ? null : rtpConnection.getAudioManager(); - if (audioManager == null || audioManager.getSelectedAudioDevice() == AppRTCAudioManager.AudioDevice.EARPIECE) { + final JingleRtpConnection rtpConnection = + rtpConnectionReference != null ? rtpConnectionReference.get() : null; + final AppRTCAudioManager audioManager = + rtpConnection == null ? null : rtpConnection.getAudioManager(); + if (audioManager == null + || audioManager.getSelectedAudioDevice() + == AppRTCAudioManager.AudioDevice.EARPIECE) { acquireProximityWakeLock(); } } @@ -311,30 +338,31 @@ private void acquireProximityWakeLock() { Log.e(Config.LOGTAG, "power manager not available"); return; } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - if (this.mProximityWakeLock == null) { - this.mProximityWakeLock = powerManager.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, PROXIMITY_WAKE_LOCK_TAG); - } - if (!this.mProximityWakeLock.isHeld()) { - Log.d(Config.LOGTAG, "acquiring proximity wake lock"); - this.mProximityWakeLock.acquire(); - } + if (isFinishing()) { + Log.e(Config.LOGTAG, "do not acquire wakelock. activity is finishing"); + return; + } + if (this.mProximityWakeLock == null) { + this.mProximityWakeLock = + powerManager.newWakeLock( + PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, PROXIMITY_WAKE_LOCK_TAG); + } + if (!this.mProximityWakeLock.isHeld()) { + Log.d(Config.LOGTAG, "acquiring proximity wake lock"); + this.mProximityWakeLock.acquire(); } } private void releaseProximityWakeLock() { if (this.mProximityWakeLock != null && mProximityWakeLock.isHeld()) { Log.d(Config.LOGTAG, "releasing proximity wake lock"); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - this.mProximityWakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY); - } else { - this.mProximityWakeLock.release(); - } + this.mProximityWakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY); this.mProximityWakeLock = null; } } - private void putProximityWakeLockInProperState(final AppRTCAudioManager.AudioDevice audioDevice) { + private void putProximityWakeLockInProperState( + final AppRTCAudioManager.AudioDevice audioDevice) { if (audioDevice == AppRTCAudioManager.AudioDevice.EARPIECE) { acquireProximityWakeLock(); } else { @@ -343,9 +371,7 @@ private void putProximityWakeLockInProperState(final AppRTCAudioManager.AudioDev } @Override - protected void refreshUiReal() { - - } + protected void refreshUiReal() {} @Override public void onNewIntent(final Intent intent) { @@ -353,7 +379,9 @@ public void onNewIntent(final Intent intent) { super.onNewIntent(intent); setIntent(intent); if (xmppConnectionService == null) { - Log.d(Config.LOGTAG, "RtpSessionActivity: background service wasn't bound in onNewIntent()"); + Log.d( + Config.LOGTAG, + "RtpSessionActivity: background service wasn't bound in onNewIntent()"); return; } final Account account = extractAccount(intent); @@ -399,7 +427,8 @@ void onBackendConnected() { binding.with.setText(account.getRoster().getContact(with).getDisplayName()); } else if (Intent.ACTION_VIEW.equals(action)) { final String extraLastState = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE); - final RtpEndUserState state = extraLastState == null ? null : RtpEndUserState.valueOf(extraLastState); + final RtpEndUserState state = + extraLastState == null ? null : RtpEndUserState.valueOf(extraLastState); if (state != null) { Log.d(Config.LOGTAG, "restored last state from intent extra"); updateButtonConfiguration(state); @@ -409,10 +438,15 @@ void onBackendConnected() { invalidateOptionsMenu(); } binding.with.setText(account.getRoster().getContact(with).getDisplayName()); - if (xmppConnectionService.getJingleConnectionManager().fireJingleRtpConnectionStateUpdates()) { + if (xmppConnectionService + .getJingleConnectionManager() + .fireJingleRtpConnectionStateUpdates()) { return; } - if (END_CARD.contains(state) || xmppConnectionService.getJingleConnectionManager().hasMatchingProposal(account, with)) { + if (END_CARD.contains(state) + || xmppConnectionService + .getJingleConnectionManager() + .hasMatchingProposal(account, with)) { return; } Log.d(Config.LOGTAG, "restored state (" + state + ") was not an end card. finishing"); @@ -420,12 +454,18 @@ void onBackendConnected() { } } - private void proposeJingleRtpSession(final Account account, final Jid with, final Set media) { - checkMicrophoneAvailability(); + private void proposeJingleRtpSession( + final Account account, final Jid with, final Set media) { + checkMicrophoneAvailabilityAsync(); if (with.isBareJid()) { - xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(account, with, media); + xmppConnectionService + .getJingleConnectionManager() + .proposeJingleRtpSession(account, with, media); } else { - final String sessionId = xmppConnectionService.getJingleConnectionManager().initializeRtpSession(account, with, media); + final String sessionId = + xmppConnectionService + .getJingleConnectionManager() + .initializeRtpSession(account, with, media); initializeActivityWithRunningRtpSession(account, with, sessionId); resetIntent(account, with, sessionId); } @@ -433,7 +473,8 @@ private void proposeJingleRtpSession(final Account account, final Jid with, fina } @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (PermissionUtils.allGranted(grantResults)) { if (requestCode == REQUEST_ACCEPT_CALL) { @@ -449,7 +490,8 @@ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permis } else { throw new IllegalStateException("Invalid permission result request"); } - Toast.makeText(this, getString(res, getString(R.string.app_name)), Toast.LENGTH_SHORT).show(); + Toast.makeText(this, getString(res, getString(R.string.app_name)), Toast.LENGTH_SHORT) + .show(); } } @@ -467,7 +509,8 @@ public void onStop() { binding.remoteVideo.setOnAspectRatioChanged(null); binding.localVideo.release(); final WeakReference weakReference = this.rtpConnectionReference; - final JingleRtpConnection jingleRtpConnection = weakReference == null ? null : weakReference.get(); + final JingleRtpConnection jingleRtpConnection = + weakReference == null ? null : weakReference.get(); if (jingleRtpConnection != null) { releaseVideoTracks(jingleRtpConnection); } @@ -504,15 +547,18 @@ public void onUserLeaveHint() { if (switchToPictureInPicture()) { return; } - //TODO apparently this method is not getting called on Android 10 when using the task switcher + // TODO apparently this method is not getting called on Android 10 when using the task + // switcher if (emptyReference(rtpConnectionReference) && xmppConnectionService != null) { retractSessionProposal(); } } private boolean isConnected() { - final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; - return connection != null && STATES_CONSIDERED_CONNECTED.contains(connection.getEndUserState()); + final JingleRtpConnection connection = + this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; + return connection != null + && STATES_CONSIDERED_CONNECTED.contains(connection.getEndUserState()); } private boolean switchToPictureInPicture() { @@ -530,14 +576,13 @@ private void startPictureInPicture() { try { final Rational rational = this.binding.remoteVideo.getAspectRatio(); final Rational clippedRational = Rationals.clip(rational); - Log.d(Config.LOGTAG, "suggested rational " + rational + ". clipped to " + clippedRational); + Log.d( + Config.LOGTAG, + "suggested rational " + rational + ". clipped to " + clippedRational); enterPictureInPictureMode( - new PictureInPictureParams.Builder() - .setAspectRatio(clippedRational) - .build() - ); + new PictureInPictureParams.Builder().setAspectRatio(clippedRational).build()); } catch (final IllegalStateException e) { - //this sometimes happens on Samsung phones (possibly when Knox is enabled) + // this sometimes happens on Samsung phones (possibly when Knox is enabled) Log.w(Config.LOGTAG, "unable to enter picture in picture mode", e); } } @@ -546,10 +591,14 @@ private void startPictureInPicture() { public void onAspectRatioChanged(final Rational rational) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isPictureInPicture()) { final Rational clippedRational = Rationals.clip(rational); - Log.d(Config.LOGTAG, "suggested rational after aspect ratio change " + rational + ". clipped to " + clippedRational); - setPictureInPictureParams(new PictureInPictureParams.Builder() - .setAspectRatio(clippedRational) - .build()); + Log.d( + Config.LOGTAG, + "suggested rational after aspect ratio change " + + rational + + ". clipped to " + + clippedRational); + setPictureInPictureParams( + new PictureInPictureParams.Builder().setAspectRatio(clippedRational).build()); } } @@ -564,24 +613,31 @@ private boolean deviceSupportsPictureInPicture() { private boolean shouldBePictureInPicture() { try { final JingleRtpConnection rtpConnection = requireRtpConnection(); - return rtpConnection.getMedia().contains(Media.VIDEO) && Arrays.asList( - RtpEndUserState.ACCEPTING_CALL, - RtpEndUserState.CONNECTING, - RtpEndUserState.CONNECTED - ).contains(rtpConnection.getEndUserState()); + return rtpConnection.getMedia().contains(Media.VIDEO) + && Arrays.asList( + RtpEndUserState.ACCEPTING_CALL, + RtpEndUserState.CONNECTING, + RtpEndUserState.CONNECTED) + .contains(rtpConnection.getEndUserState()); } catch (final IllegalStateException e) { return false; } } - private boolean initializeActivityWithRunningRtpSession(final Account account, Jid with, String sessionId) { - final WeakReference reference = xmppConnectionService.getJingleConnectionManager() - .findJingleRtpConnection(account, with, sessionId); + private boolean initializeActivityWithRunningRtpSession( + final Account account, Jid with, String sessionId) { + final WeakReference reference = + xmppConnectionService + .getJingleConnectionManager() + .findJingleRtpConnection(account, with, sessionId); if (reference == null || reference.get() == null) { - final JingleConnectionManager.TerminatedRtpSession terminatedRtpSession = xmppConnectionService - .getJingleConnectionManager().getTerminalSessionState(with, sessionId); + final JingleConnectionManager.TerminatedRtpSession terminatedRtpSession = + xmppConnectionService + .getJingleConnectionManager() + .getTerminalSessionState(with, sessionId); if (terminatedRtpSession == null) { - throw new IllegalStateException("failed to initialize activity with running rtp session. session not found"); + throw new IllegalStateException( + "failed to initialize activity with running rtp session. session not found"); } initializeWithTerminatedSessionState(account, with, terminatedRtpSession); return true; @@ -598,7 +654,8 @@ private boolean initializeActivityWithRunningRtpSession(final Account account, J if (currentState == RtpEndUserState.INCOMING_CALL) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } - if (JingleRtpConnection.STATES_SHOWING_ONGOING_CALL.contains(requireRtpConnection().getState())) { + if (JingleRtpConnection.STATES_SHOWING_ONGOING_CALL.contains( + requireRtpConnection().getState())) { putScreenInCallMode(); } binding.with.setText(getWith().getDisplayName()); @@ -611,7 +668,10 @@ private boolean initializeActivityWithRunningRtpSession(final Account account, J return false; } - private void initializeWithTerminatedSessionState(final Account account, final Jid with, final JingleConnectionManager.TerminatedRtpSession terminatedRtpSession) { + private void initializeWithTerminatedSessionState( + final Account account, + final Jid with, + final JingleConnectionManager.TerminatedRtpSession terminatedRtpSession) { Log.d(Config.LOGTAG, "initializeWithTerminatedSessionState()"); if (terminatedRtpSession.state == RtpEndUserState.ENDED) { finish(); @@ -628,7 +688,8 @@ private void initializeWithTerminatedSessionState(final Account account, final J binding.with.setText(account.getRoster().getContact(with).getDisplayName()); } - private void reInitializeActivityWithRunningRtpSession(final Account account, Jid with, String sessionId) { + private void reInitializeActivityWithRunningRtpSession( + final Account account, Jid with, String sessionId) { runOnUiThread(() -> initializeActivityWithRunningRtpSession(account, with, sessionId)); resetIntent(account, with, sessionId); } @@ -646,7 +707,7 @@ private void ensureSurfaceViewRendererIsSetup(final SurfaceViewRenderer surfaceV try { surfaceViewRenderer.init(requireRtpConnection().getEglBaseContext(), null); } catch (final IllegalStateException e) { - //Log.d(Config.LOGTAG, "SurfaceViewRenderer was already initialized"); + // Log.d(Config.LOGTAG, "SurfaceViewRenderer was already initialized"); } surfaceViewRenderer.setEnableHardwareScaler(true); } @@ -705,9 +766,11 @@ private void updateStateDisplay(final RtpEndUserState state, final Set me setTitle(R.string.rtp_state_security_error); break; case ENDED: - throw new IllegalStateException("Activity should have called finishAndReleaseWakeLock();"); + throw new IllegalStateException( + "Activity should have called finishAndReleaseWakeLock();"); default: - throw new IllegalStateException(String.format("State %s has not been handled in UI", state)); + throw new IllegalStateException( + String.format("State %s has not been handled in UI", state)); } } @@ -729,9 +792,11 @@ private void updateProfilePicture(final RtpEndUserState state, final Contact con if (show) { binding.contactPhoto.setVisibility(View.VISIBLE); if (contact == null) { - AvatarWorkerTask.loadAvatar(getWith(), binding.contactPhoto, R.dimen.publish_avatar_size); + AvatarWorkerTask.loadAvatar( + getWith(), binding.contactPhoto, R.dimen.publish_avatar_size); } else { - AvatarWorkerTask.loadAvatar(contact, binding.contactPhoto, R.dimen.publish_avatar_size); + AvatarWorkerTask.loadAvatar( + contact, binding.contactPhoto, R.dimen.publish_avatar_size); } } else { binding.contactPhoto.setVisibility(View.GONE); @@ -776,12 +841,12 @@ private void updateButtonConfiguration(final RtpEndUserState state, final Set media) { + private void updateInCallButtonConfiguration( + final RtpEndUserState state, final Set media) { if (STATES_CONSIDERED_CONNECTED.contains(state) && !isPictureInPicture()) { Preconditions.checkArgument(media.size() > 0, "Media must not be empty"); if (media.contains(Media.VIDEO)) { final JingleRtpConnection rtpConnection = requireRtpConnection(); - updateInCallButtonConfigurationVideo(rtpConnection.isVideoEnabled(), rtpConnection.isCameraSwitchable()); + updateInCallButtonConfigurationVideo( + rtpConnection.isVideoEnabled(), rtpConnection.isCameraSwitchable()); } else { final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager(); updateInCallButtonConfigurationSpeaker( audioManager.getSelectedAudioDevice(), - audioManager.getAudioDevices().size() - ); + audioManager.getAudioDevices().size()); this.binding.inCallActionFarRight.setVisibility(View.GONE); } if (media.contains(Media.AUDIO)) { - updateInCallButtonConfigurationMicrophone(requireRtpConnection().isMicrophoneEnabled()); + updateInCallButtonConfigurationMicrophone( + requireRtpConnection().isMicrophoneEnabled()); } else { this.binding.inCallActionLeft.setVisibility(View.GONE); } @@ -842,10 +910,12 @@ private void updateInCallButtonConfiguration(final RtpEndUserState state, final } @SuppressLint("RestrictedApi") - private void updateInCallButtonConfigurationSpeaker(final AppRTCAudioManager.AudioDevice selectedAudioDevice, final int numberOfChoices) { + private void updateInCallButtonConfigurationSpeaker( + final AppRTCAudioManager.AudioDevice selectedAudioDevice, final int numberOfChoices) { switch (selectedAudioDevice) { case EARPIECE: - this.binding.inCallActionRight.setImageResource(R.drawable.ic_volume_off_black_24dp); + this.binding.inCallActionRight.setImageResource( + R.drawable.ic_volume_off_black_24dp); if (numberOfChoices >= 2) { this.binding.inCallActionRight.setOnClickListener(this::switchToSpeaker); } else { @@ -868,7 +938,8 @@ private void updateInCallButtonConfigurationSpeaker(final AppRTCAudioManager.Aud } break; case BLUETOOTH: - this.binding.inCallActionRight.setImageResource(R.drawable.ic_bluetooth_audio_black_24dp); + this.binding.inCallActionRight.setImageResource( + R.drawable.ic_bluetooth_audio_black_24dp); this.binding.inCallActionRight.setOnClickListener(null); this.binding.inCallActionRight.setClickable(false); break; @@ -877,10 +948,12 @@ private void updateInCallButtonConfigurationSpeaker(final AppRTCAudioManager.Aud } @SuppressLint("RestrictedApi") - private void updateInCallButtonConfigurationVideo(final boolean videoEnabled, final boolean isCameraSwitchable) { + private void updateInCallButtonConfigurationVideo( + final boolean videoEnabled, final boolean isCameraSwitchable) { this.binding.inCallActionRight.setVisibility(View.VISIBLE); if (isCameraSwitchable) { - this.binding.inCallActionFarRight.setImageResource(R.drawable.ic_flip_camera_android_black_24dp); + this.binding.inCallActionFarRight.setImageResource( + R.drawable.ic_flip_camera_android_black_24dp); this.binding.inCallActionFarRight.setVisibility(View.VISIBLE); this.binding.inCallActionFarRight.setOnClickListener(this::switchCamera); } else { @@ -896,18 +969,28 @@ private void updateInCallButtonConfigurationVideo(final boolean videoEnabled, fi } private void switchCamera(final View view) { - Futures.addCallback(requireRtpConnection().switchCamera(), new FutureCallback() { - @Override - public void onSuccess(@NullableDecl Boolean isFrontCamera) { - binding.localVideo.setMirror(isFrontCamera); - } - - @Override - public void onFailure(@NonNull final Throwable throwable) { - Log.d(Config.LOGTAG, "could not switch camera", Throwables.getRootCause(throwable)); - Toast.makeText(RtpSessionActivity.this, R.string.could_not_switch_camera, Toast.LENGTH_LONG).show(); - } - }, MainThreadExecutor.getInstance()); + Futures.addCallback( + requireRtpConnection().switchCamera(), + new FutureCallback() { + @Override + public void onSuccess(@NullableDecl Boolean isFrontCamera) { + binding.localVideo.setMirror(isFrontCamera); + } + + @Override + public void onFailure(@NonNull final Throwable throwable) { + Log.d( + Config.LOGTAG, + "could not switch camera", + Throwables.getRootCause(throwable)); + Toast.makeText( + RtpSessionActivity.this, + R.string.could_not_switch_camera, + Toast.LENGTH_LONG) + .show(); + } + }, + MainThreadExecutor.getInstance()); } private void enableVideo(View view) { @@ -923,7 +1006,6 @@ private void enableVideo(View view) { private void disableVideo(View view) { requireRtpConnection().setVideoEnabled(false); updateInCallButtonConfigurationVideo(false, requireRtpConnection().isCameraSwitchable()); - } @SuppressLint("RestrictedApi") @@ -939,7 +1021,8 @@ private void updateInCallButtonConfigurationMicrophone(final boolean microphoneE } private void updateCallDuration() { - final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; + final JingleRtpConnection connection = + this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; if (connection == null || connection.getMedia().contains(Media.VIDEO)) { this.binding.duration.setVisibility(View.GONE); return; @@ -947,7 +1030,8 @@ private void updateCallDuration() { if (connection.zeroDuration()) { this.binding.duration.setVisibility(View.GONE); } else { - this.binding.duration.setText(TimeFrameUtils.formatElapsedTime(connection.getCallDuration(), false)); + this.binding.duration.setText( + TimeFrameUtils.formatElapsedTime(connection.getCallDuration(), false)); this.binding.duration.setVisibility(View.VISIBLE); } } @@ -963,9 +1047,9 @@ private void updateVideoViews(final RtpEndUserState state) { binding.appBarLayout.setVisibility(View.GONE); binding.pipPlaceholder.setVisibility(View.VISIBLE); if (Arrays.asList( - RtpEndUserState.APPLICATION_ERROR, - RtpEndUserState.CONNECTIVITY_ERROR, - RtpEndUserState.SECURITY_ERROR) + RtpEndUserState.APPLICATION_ERROR, + RtpEndUserState.CONNECTIVITY_ERROR, + RtpEndUserState.SECURITY_ERROR) .contains(state)) { binding.pipWarning.setVisibility(View.VISIBLE); binding.pipWaiting.setVisibility(View.GONE); @@ -993,7 +1077,7 @@ private void updateVideoViews(final RtpEndUserState state) { final Optional localVideoTrack = getLocalVideoTrack(); if (localVideoTrack.isPresent() && !isPictureInPicture()) { ensureSurfaceViewRendererIsSetup(binding.localVideo); - //paint local view over remote view + // paint local view over remote view binding.localVideo.setZOrderMediaOverlay(true); binding.localVideo.setMirror(requireRtpConnection().isFrontCamera()); addSink(localVideoTrack.get(), binding.localVideo); @@ -1006,8 +1090,7 @@ private void updateVideoViews(final RtpEndUserState state) { addSink(remoteVideoTrack.get(), binding.remoteVideo); binding.remoteVideo.setScalingType( RendererCommon.ScalingType.SCALE_ASPECT_FILL, - RendererCommon.ScalingType.SCALE_ASPECT_FIT - ); + RendererCommon.ScalingType.SCALE_ASPECT_FIT); if (state == RtpEndUserState.CONNECTED) { binding.appBarLayout.setVisibility(View.GONE); getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); @@ -1030,7 +1113,8 @@ private void updateVideoViews(final RtpEndUserState state) { } private Optional getLocalVideoTrack() { - final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; + final JingleRtpConnection connection = + this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; if (connection == null) { return Optional.absent(); } @@ -1038,7 +1122,8 @@ private Optional getLocalVideoTrack() { } private Optional getRemoteVideoTrack() { - final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; + final JingleRtpConnection connection = + this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; if (connection == null) { return Optional.absent(); } @@ -1060,12 +1145,16 @@ private void enableMicrophone(View view) { } private void switchToEarpiece(View view) { - requireRtpConnection().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.EARPIECE); + requireRtpConnection() + .getAudioManager() + .setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.EARPIECE); acquireProximityWakeLock(); } private void switchToSpeaker(View view) { - requireRtpConnection().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE); + requireRtpConnection() + .getAudioManager() + .setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE); releaseProximityWakeLock(); } @@ -1089,12 +1178,15 @@ private void recordVoiceMail(final View view) { final Intent intent = getIntent(); final Account account = extractAccount(intent); final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH)); - final Conversation conversation = xmppConnectionService.findOrCreateConversation(account, with, false, true); + final Conversation conversation = + xmppConnectionService.findOrCreateConversation(account, with, false, true); final Intent launchIntent = new Intent(this, ConversationsActivity.class); launchIntent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION); launchIntent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid()); launchIntent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_CLEAR_TOP); - launchIntent.putExtra(ConversationsActivity.EXTRA_POST_INIT_ACTION, ConversationsActivity.POST_ACTION_RECORD_VOICE); + launchIntent.putExtra( + ConversationsActivity.EXTRA_POST_INIT_ACTION, + ConversationsActivity.POST_ACTION_RECORD_VOICE); startActivity(launchIntent); finish(); } @@ -1106,7 +1198,8 @@ private Contact getWith() { } private JingleRtpConnection requireRtpConnection() { - final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; + final JingleRtpConnection connection = + this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; if (connection == null) { throw new IllegalStateException("No RTP connection found"); } @@ -1114,12 +1207,14 @@ private JingleRtpConnection requireRtpConnection() { } @Override - public void onJingleRtpConnectionUpdate(Account account, Jid with, final String sessionId, RtpEndUserState state) { + public void onJingleRtpConnectionUpdate( + Account account, Jid with, final String sessionId, RtpEndUserState state) { Log.d(Config.LOGTAG, "onJingleRtpConnectionUpdate(" + state + ")"); if (END_CARD.contains(state)) { Log.d(Config.LOGTAG, "end card reached"); releaseProximityWakeLock(); - runOnUiThread(() -> getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)); + runOnUiThread( + () -> getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)); } if (with.isBareJid()) { updateRtpSessionProposalState(account, with, state); @@ -1130,7 +1225,7 @@ public void onJingleRtpConnectionUpdate(Account account, Jid with, final String Log.d(Config.LOGTAG, "not reinitializing session"); return; } - //this happens when going from proposed session to actual session + // this happens when going from proposed session to actual session reInitializeActivityWithRunningRtpSession(account, with, sessionId); return; } @@ -1143,14 +1238,16 @@ public void onJingleRtpConnectionUpdate(Account account, Jid with, final String finish(); return; } - runOnUiThread(() -> { - updateStateDisplay(state, media); - updateVerifiedShield(verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(state)); - updateButtonConfiguration(state, media); - updateVideoViews(state); - updateProfilePicture(state, contact); - invalidateOptionsMenu(); - }); + runOnUiThread( + () -> { + updateStateDisplay(state, media); + updateVerifiedShield( + verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(state)); + updateButtonConfiguration(state, media); + updateVideoViews(state); + updateProfilePicture(state, contact); + invalidateOptionsMenu(); + }); if (END_CARD.contains(state)) { final JingleRtpConnection rtpConnection = requireRtpConnection(); resetIntent(account, with, state, rtpConnection.getMedia()); @@ -1163,8 +1260,15 @@ public void onJingleRtpConnectionUpdate(Account account, Jid with, final String } @Override - public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices) { - Log.d(Config.LOGTAG, "onAudioDeviceChanged in activity: selected:" + selectedAudioDevice + ", available:" + availableAudioDevices); + public void onAudioDeviceChanged( + AppRTCAudioManager.AudioDevice selectedAudioDevice, + Set availableAudioDevices) { + Log.d( + Config.LOGTAG, + "onAudioDeviceChanged in activity: selected:" + + selectedAudioDevice + + ", available:" + + availableAudioDevices); try { if (getMedia().contains(Media.VIDEO)) { Log.d(Config.LOGTAG, "nothing to do; in video mode"); @@ -1175,10 +1279,11 @@ public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDev final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager(); updateInCallButtonConfigurationSpeaker( audioManager.getSelectedAudioDevice(), - audioManager.getAudioDevices().size() - ); + audioManager.getAudioDevices().size()); } else if (END_CARD.contains(endUserState)) { - Log.d(Config.LOGTAG, "onAudioDeviceChanged() nothing to do because end card has been reached"); + Log.d( + Config.LOGTAG, + "onAudioDeviceChanged() nothing to do because end card has been reached"); } else { putProximityWakeLockInProperState(selectedAudioDevice); } @@ -1187,20 +1292,23 @@ public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDev } } - private void updateRtpSessionProposalState(final Account account, final Jid with, final RtpEndUserState state) { + private void updateRtpSessionProposalState( + final Account account, final Jid with, final RtpEndUserState state) { final Intent currentIntent = getIntent(); - final String withExtra = currentIntent == null ? null : currentIntent.getStringExtra(EXTRA_WITH); + final String withExtra = + currentIntent == null ? null : currentIntent.getStringExtra(EXTRA_WITH); if (withExtra == null) { return; } if (Jid.ofEscaped(withExtra).asBareJid().equals(with)) { - runOnUiThread(() -> { - updateVerifiedShield(false); - updateStateDisplay(state); - updateButtonConfiguration(state); - updateProfilePicture(state); - invalidateOptionsMenu(); - }); + runOnUiThread( + () -> { + updateVerifiedShield(false); + updateStateDisplay(state); + updateButtonConfiguration(state); + updateProfilePicture(state); + invalidateOptionsMenu(); + }); resetIntent(account, with, state, actionToMedia(currentIntent.getAction())); } } @@ -1211,16 +1319,22 @@ private void resetIntent(final Bundle extras) { setIntent(intent); } - private void resetIntent(final Account account, Jid with, final RtpEndUserState state, final Set media) { + private void resetIntent( + final Account account, Jid with, final RtpEndUserState state, final Set media) { final Intent intent = new Intent(Intent.ACTION_VIEW); intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString()); - if (account.getRoster().getContact(with).getPresences().anySupport(Namespace.JINGLE_MESSAGE)) { + if (account.getRoster() + .getContact(with) + .getPresences() + .anySupport(Namespace.JINGLE_MESSAGE)) { intent.putExtra(EXTRA_WITH, with.asBareJid().toEscapedString()); } else { intent.putExtra(EXTRA_WITH, with.toEscapedString()); } intent.putExtra(EXTRA_LAST_REPORTED_STATE, state.toString()); - intent.putExtra(EXTRA_LAST_ACTION, media.contains(Media.VIDEO) ? ACTION_MAKE_VIDEO_CALL : ACTION_MAKE_VOICE_CALL); + intent.putExtra( + EXTRA_LAST_ACTION, + media.contains(Media.VIDEO) ? ACTION_MAKE_VIDEO_CALL : ACTION_MAKE_VOICE_CALL); setIntent(intent); } From be1fcfe4f93d9f06cae82cfe43c8c45bde591ed3 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 23 Feb 2022 16:59:40 +0100 Subject: [PATCH 058/394] store encrypted pgp files in private cache dir --- .../conversations/http/HttpDownloadConnection.java | 7 ++++--- .../siacs/conversations/persistance/FileBackend.java | 9 +++++---- .../siacs/conversations/utils/FileWriterException.java | 10 ++++++++++ 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java index 3c9ceb978..5623c0be7 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java @@ -119,7 +119,7 @@ public void init(boolean interactive) { private void setupFile() { final String reference = mUrl.fragment(); if (reference != null && AesGcmURL.IV_KEY.matcher(reference).matches()) { - this.file = new DownloadableFile(mXmppConnectionService.getCacheDir().getAbsolutePath() + "/" + message.getUuid()); + this.file = new DownloadableFile(mXmppConnectionService.getCacheDir(), message.getUuid()); this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference)); Log.d(Config.LOGTAG, "create temporary OMEMO encrypted file: " + this.file.getAbsolutePath() + "(" + message.getMimeType() + ")"); } else { @@ -416,8 +416,9 @@ private void download() throws Exception { Log.d(Config.LOGTAG, "content-length reported on GET (" + size + ") did not match Content-Length reported on HEAD (" + expected + ")"); } file.getParentFile().mkdirs(); + Log.d(Config.LOGTAG,"creating file: "+file.getAbsolutePath()); if (!file.exists() && !file.createNewFile()) { - throw new FileWriterException(); + throw new FileWriterException(file); } outputStream = AbstractConnectionManager.createOutputStream(file, false, false); } @@ -428,7 +429,7 @@ private void download() throws Exception { try { outputStream.write(buffer, 0, count); } catch (IOException e) { - throw new FileWriterException(); + throw new FileWriterException(file); } updateProgress(Math.round(((double) transmitted / expected) * 100)); } diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index a5a904c75..5c23bf0fa 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -1,6 +1,5 @@ package eu.siacs.conversations.persistance; -import android.annotation.TargetApi; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; @@ -535,7 +534,9 @@ public DownloadableFile getFile(Message message, boolean decrypted) { } final DownloadableFile file = getFileForPath(path, message.getMimeType()); if (encrypted) { - return new DownloadableFile(getLegacyStorageLocation("Files"), file.getName() + ".pgp"); + return new DownloadableFile( + mXmppConnectionService.getCacheDir(), + String.format("%s.%s", file.getName(), "pgp")); } else { return file; } @@ -651,12 +652,12 @@ private void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyExcepti try { ByteStreams.copy(is, os); } catch (IOException e) { - throw new FileWriterException(); + throw new FileWriterException(file); } try { os.flush(); } catch (IOException e) { - throw new FileWriterException(); + throw new FileWriterException(file); } } catch (final FileNotFoundException e) { cleanup(file); diff --git a/src/main/java/eu/siacs/conversations/utils/FileWriterException.java b/src/main/java/eu/siacs/conversations/utils/FileWriterException.java index f406f4197..7e41edfdc 100644 --- a/src/main/java/eu/siacs/conversations/utils/FileWriterException.java +++ b/src/main/java/eu/siacs/conversations/utils/FileWriterException.java @@ -1,4 +1,14 @@ package eu.siacs.conversations.utils; +import java.io.File; + public class FileWriterException extends Exception { + + public FileWriterException(File file) { + super(String.format("Could not write to %s", file.getAbsolutePath())); + } + + FileWriterException() { + + } } From 9b6a5709390f4550e74cceee4a20900759097143 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 24 Feb 2022 12:41:32 +0100 Subject: [PATCH 059/394] bump agp --- build.gradle | 2 +- .../eu/siacs/conversations/crypto/PgpDecryptionService.java | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 67e995bea..54186f6d2 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.1.1' + classpath 'com.android.tools.build:gradle:7.1.2' } } diff --git a/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java b/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java index 9a2288884..a676e5d5d 100644 --- a/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java +++ b/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java @@ -148,9 +148,6 @@ private void executeApi(Message message) { try { os.flush(); final String body = os.toString(); - if (body == null) { - throw new IOException("body was null"); - } message.setBody(body); message.setEncryption(Message.ENCRYPTION_DECRYPTED); final HttpConnectionManager manager = mXmppConnectionService.getHttpConnectionManager(); From d311e39569226f74f1ae8d1b948b88e6311a28d8 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 25 Feb 2022 14:44:46 +0100 Subject: [PATCH 060/394] code clean up --- .../java/eu/siacs/conversations/utils/MimeUtils.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java index c0a6f4cfd..90f27f65f 100644 --- a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java @@ -568,11 +568,15 @@ public static String guessMimeTypeFromUri(Context context, Uri uri) { } private static String getDisplayName(final Context context, final Uri uri) { - try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) { + try (final Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) { if (cursor != null && cursor.moveToFirst()) { - return cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)); + final int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); + if (index == -1) { + return null; + } + return cursor.getString(index); } - } catch (Exception e) { + } catch (final Exception e) { return null; } return null; From 1f772df74fee6a6b4cc840a07530dc2ab5347911 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 25 Feb 2022 16:24:13 +0100 Subject: [PATCH 061/394] remove security check that ensures rtp connection was properly finished this only causes race conditions --- .../conversations/ui/RtpSessionActivity.java | 1 - .../xmpp/jingle/AbstractJingleConnection.java | 3 + .../xmpp/jingle/JingleConnectionManager.java | 535 ++++--- .../xmpp/jingle/JingleRtpConnection.java | 1312 +++++++++++------ 4 files changed, 1240 insertions(+), 611 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index a27794c30..6320ef5b7 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -646,7 +646,6 @@ private boolean initializeActivityWithRunningRtpSession( final RtpEndUserState currentState = requireRtpConnection().getEndUserState(); final boolean verified = requireRtpConnection().isVerified(); if (currentState == RtpEndUserState.ENDED) { - reference.get().throwStateTransitionException(); finish(); return true; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java index 90f06fe26..d719c729e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.xmpp.jingle; +import androidx.annotation.NonNull; + import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import com.google.common.base.Preconditions; @@ -110,6 +112,7 @@ public String getSessionId() { } @Override + @NonNull public String toString() { return MoreObjects.toStringHelper(this) .add("account", account.getJid()) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index cbf4b85fd..533974a0f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -52,14 +52,16 @@ import eu.siacs.conversations.xmpp.stanzas.MessagePacket; public class JingleConnectionManager extends AbstractConnectionManager { - static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE = Executors.newSingleThreadScheduledExecutor(); + static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE = + Executors.newSingleThreadScheduledExecutor(); final ToneManager toneManager; - private final HashMap rtpSessionProposals = new HashMap<>(); - private final ConcurrentHashMap connections = new ConcurrentHashMap<>(); + private final HashMap rtpSessionProposals = + new HashMap<>(); + private final ConcurrentHashMap + connections = new ConcurrentHashMap<>(); - private final Cache terminatedSessions = CacheBuilder.newBuilder() - .expireAfterWrite(24, TimeUnit.HOURS) - .build(); + private final Cache terminatedSessions = + CacheBuilder.newBuilder().expireAfterWrite(24, TimeUnit.HOURS).build(); private final HashMap primaryCandidates = new HashMap<>(); @@ -87,17 +89,31 @@ public void deliverPacket(final Account account, final JinglePacket packet) { } else if (packet.getAction() == JinglePacket.Action.SESSION_INITIATE) { final Jid from = packet.getFrom(); final Content content = packet.getJingleContent(); - final String descriptionNamespace = content == null ? null : content.getDescriptionNamespace(); + final String descriptionNamespace = + content == null ? null : content.getDescriptionNamespace(); final AbstractJingleConnection connection; if (FileTransferDescription.NAMESPACES.contains(descriptionNamespace)) { connection = new JingleFileTransferConnection(this, id, from); - } else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace) && isUsingClearNet(account)) { - final boolean sessionEnded = this.terminatedSessions.asMap().containsKey(PersistableSessionId.of(id)); - final boolean stranger = isWithStrangerAndStrangerNotificationsAreOff(account, id.with); + } else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace) + && isUsingClearNet(account)) { + final boolean sessionEnded = + this.terminatedSessions.asMap().containsKey(PersistableSessionId.of(id)); + final boolean stranger = + isWithStrangerAndStrangerNotificationsAreOff(account, id.with); if (isBusy() || sessionEnded || stranger) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": rejected session with " + id.with + " because busy. sessionEnded=" + sessionEnded + ", stranger=" + stranger); - mXmppConnectionService.sendIqPacket(account, packet.generateResponse(IqPacket.TYPE.RESULT), null); - final JinglePacket sessionTermination = new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": rejected session with " + + id.with + + " because busy. sessionEnded=" + + sessionEnded + + ", stranger=" + + stranger); + mXmppConnectionService.sendIqPacket( + account, packet.generateResponse(IqPacket.TYPE.RESULT), null); + final JinglePacket sessionTermination = + new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); sessionTermination.setTo(id.with); sessionTermination.setReason(Reason.BUSY, null); mXmppConnectionService.sendIqPacket(account, sessionTermination, null); @@ -105,7 +121,8 @@ public void deliverPacket(final Account account, final JinglePacket packet) { } connection = new JingleRtpConnection(this, id, from); } else { - respondWithJingleError(account, packet, "unsupported-info", "feature-not-implemented", "cancel"); + respondWithJingleError( + account, packet, "unsupported-info", "feature-not-implemented", "cancel"); return; } connections.put(id, connection); @@ -136,7 +153,8 @@ public boolean isBusy() { synchronized (this.rtpSessionProposals) { return this.rtpSessionProposals.containsValue(DeviceDiscoveryState.DISCOVERED) || this.rtpSessionProposals.containsValue(DeviceDiscoveryState.SEARCHING) - || this.rtpSessionProposals.containsValue(DeviceDiscoveryState.SEARCHING_ACKNOWLEDGED); + || this.rtpSessionProposals.containsValue( + DeviceDiscoveryState.SEARCHING_ACKNOWLEDGED); } } @@ -152,14 +170,17 @@ public void notifyPhoneCallStarted() { } } - private Optional findMatchingSessionProposal(final Account account, final Jid with, final Set media) { + private Optional findMatchingSessionProposal( + final Account account, final Jid with, final Set media) { synchronized (this.rtpSessionProposals) { - for (Map.Entry entry : this.rtpSessionProposals.entrySet()) { + for (Map.Entry entry : + this.rtpSessionProposals.entrySet()) { final RtpSessionProposal proposal = entry.getKey(); final DeviceDiscoveryState state = entry.getValue(); - final boolean openProposal = state == DeviceDiscoveryState.DISCOVERED - || state == DeviceDiscoveryState.SEARCHING - || state == DeviceDiscoveryState.SEARCHING_ACKNOWLEDGED; + final boolean openProposal = + state == DeviceDiscoveryState.DISCOVERED + || state == DeviceDiscoveryState.SEARCHING + || state == DeviceDiscoveryState.SEARCHING_ACKNOWLEDGED; if (openProposal && proposal.account == account && proposal.with.equals(with.asBareJid()) @@ -171,7 +192,8 @@ private Optional findMatchingSessionProposal(final Account a return Optional.absent(); } - private boolean hasMatchingRtpSession(final Account account, final Jid with, final Set media) { + private boolean hasMatchingRtpSession( + final Account account, final Jid with, final Set media) { for (AbstractJingleConnection connection : this.connections.values()) { if (connection instanceof JingleRtpConnection) { final JingleRtpConnection rtpConnection = (JingleRtpConnection) connection; @@ -189,7 +211,8 @@ private boolean hasMatchingRtpSession(final Account account, final Jid with, fin } private boolean isWithStrangerAndStrangerNotificationsAreOff(final Account account, Jid with) { - final boolean notifyForStrangers = mXmppConnectionService.getNotificationService().notificationsFromStrangers(); + final boolean notifyForStrangers = + mXmppConnectionService.getNotificationService().notificationsFromStrangers(); if (notifyForStrangers) { return false; } @@ -197,11 +220,17 @@ private boolean isWithStrangerAndStrangerNotificationsAreOff(final Account accou return !contact.showInContactList(); } - ScheduledFuture schedule(final Runnable runnable, final long delay, final TimeUnit timeUnit) { + ScheduledFuture schedule( + final Runnable runnable, final long delay, final TimeUnit timeUnit) { return SCHEDULED_EXECUTOR_SERVICE.schedule(runnable, delay, timeUnit); } - void respondWithJingleError(final Account account, final IqPacket original, String jingleCondition, String condition, String conditionType) { + void respondWithJingleError( + final Account account, + final IqPacket original, + String jingleCondition, + String condition, + String conditionType) { final IqPacket response = original.generateResponse(IqPacket.TYPE.ERROR); final Element error = response.addChild("error"); error.setAttribute("type", conditionType); @@ -210,7 +239,14 @@ void respondWithJingleError(final Account account, final IqPacket original, Stri account.getXmppConnection().sendIqPacket(response, null); } - public void deliverMessage(final Account account, final Jid to, final Jid from, final Element message, String remoteMsgId, String serverMsgId, long timestamp) { + public void deliverMessage( + final Account account, + final Jid to, + final Jid from, + final Element message, + String remoteMsgId, + String serverMsgId, + long timestamp) { Preconditions.checkArgument(Namespace.JINGLE_MESSAGE.equals(message.getNamespace())); final String sessionId = message.getAttribute("id"); if (sessionId == null) { @@ -244,16 +280,24 @@ public void deliverMessage(final Account account, final Jid to, final Jid from, final AbstractJingleConnection existingJingleConnection = connections.get(id); if (existingJingleConnection != null) { if (existingJingleConnection instanceof JingleRtpConnection) { - ((JingleRtpConnection) existingJingleConnection).deliveryMessage(from, message, serverMsgId, timestamp); + ((JingleRtpConnection) existingJingleConnection) + .deliveryMessage(from, message, serverMsgId, timestamp); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + existingJingleConnection.getClass().getName() + " does not support jingle messages"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": " + + existingJingleConnection.getClass().getName() + + " does not support jingle messages"); } return; } if (fromSelf) { if ("proceed".equals(message.getName())) { - final Conversation c = mXmppConnectionService.findOrCreateConversation(account, id.with, false, false); + final Conversation c = + mXmppConnectionService.findOrCreateConversation( + account, id.with, false, false); final Message previousBusy = c.findRtpSession(sessionId, Message.STATUS_RECEIVED); if (previousBusy != null) { previousBusy.setBody(new RtpSessionStatus(true, 0).toString()); @@ -262,84 +306,138 @@ public void deliverMessage(final Account account, final Jid to, final Jid from, } previousBusy.setTime(timestamp); mXmppConnectionService.updateMessage(previousBusy, true); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": updated previous busy because call got picked up by another device"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": updated previous busy because call got picked up by another device"); return; } } - //TODO handle reject for cases where we don’t have carbon copies (normally reject is to be sent to own bare jid as well) - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignore jingle message from self"); + // TODO handle reject for cases where we don’t have carbon copies (normally reject is to + // be sent to own bare jid as well) + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": ignore jingle message from self"); return; } if ("propose".equals(message.getName())) { final Propose propose = Propose.upgrade(message); final List descriptions = propose.getDescriptions(); - final Collection rtpDescriptions = Collections2.transform( - Collections2.filter(descriptions, d -> d instanceof RtpDescription), - input -> (RtpDescription) input - ); - if (rtpDescriptions.size() > 0 && rtpDescriptions.size() == descriptions.size() && isUsingClearNet(account)) { - final Collection media = Collections2.transform(rtpDescriptions, RtpDescription::getMedia); + final Collection rtpDescriptions = + Collections2.transform( + Collections2.filter(descriptions, d -> d instanceof RtpDescription), + input -> (RtpDescription) input); + if (rtpDescriptions.size() > 0 + && rtpDescriptions.size() == descriptions.size() + && isUsingClearNet(account)) { + final Collection media = + Collections2.transform(rtpDescriptions, RtpDescription::getMedia); if (media.contains(Media.UNKNOWN)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": encountered unknown media in session proposal. " + propose); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": encountered unknown media in session proposal. " + + propose); return; } - final Optional matchingSessionProposal = findMatchingSessionProposal(account, id.with, ImmutableSet.copyOf(media)); + final Optional matchingSessionProposal = + findMatchingSessionProposal(account, id.with, ImmutableSet.copyOf(media)); if (matchingSessionProposal.isPresent()) { final String ourSessionId = matchingSessionProposal.get().sessionId; final String theirSessionId = id.sessionId; if (ComparisonChain.start() - .compare(ourSessionId, theirSessionId) - .compare(account.getJid().toEscapedString(), id.with.toEscapedString()) - .result() > 0) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": our session lost tie break. automatically accepting their session. winning Session=" + theirSessionId); - //TODO a retract for this reason should probably include some indication of tie break + .compare(ourSessionId, theirSessionId) + .compare( + account.getJid().toEscapedString(), + id.with.toEscapedString()) + .result() + > 0) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": our session lost tie break. automatically accepting their session. winning Session=" + + theirSessionId); + // TODO a retract for this reason should probably include some indication of + // tie break retractSessionProposal(matchingSessionProposal.get()); - final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, from); + final JingleRtpConnection rtpConnection = + new JingleRtpConnection(this, id, from); this.connections.put(id, rtpConnection); rtpConnection.setProposedMedia(ImmutableSet.copyOf(media)); rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": our session won tie break. waiting for other party to accept. winningSession=" + ourSessionId); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": our session won tie break. waiting for other party to accept. winningSession=" + + ourSessionId); } return; } - final boolean stranger = isWithStrangerAndStrangerNotificationsAreOff(account, id.with); + final boolean stranger = + isWithStrangerAndStrangerNotificationsAreOff(account, id.with); if (isBusy() || stranger) { - writeLogMissedIncoming(account, id.with.asBareJid(), id.sessionId, serverMsgId, timestamp); + writeLogMissedIncoming( + account, id.with.asBareJid(), id.sessionId, serverMsgId, timestamp); if (stranger) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring call proposal from stranger " + id.with); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": ignoring call proposal from stranger " + + id.with); return; } final int activeDevices = account.activeDevicesWithRtpCapability(); Log.d(Config.LOGTAG, "active devices with rtp capability: " + activeDevices); if (activeDevices == 0) { - final MessagePacket reject = mXmppConnectionService.getMessageGenerator().sessionReject(from, sessionId); + final MessagePacket reject = + mXmppConnectionService + .getMessageGenerator() + .sessionReject(from, sessionId); mXmppConnectionService.sendMessagePacket(account, reject); } else { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring proposal because busy on this device but there are other devices"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": ignoring proposal because busy on this device but there are other devices"); } } else { - final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, from); + final JingleRtpConnection rtpConnection = + new JingleRtpConnection(this, id, from); this.connections.put(id, rtpConnection); rtpConnection.setProposedMedia(ImmutableSet.copyOf(media)); rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp); } } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to react to proposed session with " + rtpDescriptions.size() + " rtp descriptions of " + descriptions.size() + " total descriptions"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": unable to react to proposed session with " + + rtpDescriptions.size() + + " rtp descriptions of " + + descriptions.size() + + " total descriptions"); } } else if (addressedDirectly && "proceed".equals(message.getName())) { synchronized (rtpSessionProposals) { - final RtpSessionProposal proposal = getRtpSessionProposal(account, from.asBareJid(), sessionId); + final RtpSessionProposal proposal = + getRtpSessionProposal(account, from.asBareJid(), sessionId); if (proposal != null) { rtpSessionProposals.remove(proposal); - final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, account.getJid()); + final JingleRtpConnection rtpConnection = + new JingleRtpConnection(this, id, account.getJid()); rtpConnection.setProposedMedia(proposal.media); this.connections.put(id, rtpConnection); rtpConnection.transitionOrThrow(AbstractJingleConnection.State.PROPOSED); rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": no rtp session proposal found for " + from + " to deliver proceed"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": no rtp session proposal found for " + + from + + " to deliver proceed"); if (remoteMsgId == null) { return; } @@ -355,63 +453,77 @@ public void deliverMessage(final Account account, final Jid to, final Jid from, } } } else if (addressedDirectly && "reject".equals(message.getName())) { - final RtpSessionProposal proposal = getRtpSessionProposal(account, from.asBareJid(), sessionId); + final RtpSessionProposal proposal = + getRtpSessionProposal(account, from.asBareJid(), sessionId); synchronized (rtpSessionProposals) { if (proposal != null && rtpSessionProposals.remove(proposal) != null) { - writeLogMissedOutgoing(account, proposal.with, proposal.sessionId, serverMsgId, timestamp); + writeLogMissedOutgoing( + account, proposal.with, proposal.sessionId, serverMsgId, timestamp); toneManager.transition(RtpEndUserState.DECLINED_OR_BUSY, proposal.media); - mXmppConnectionService.notifyJingleRtpConnectionUpdate(account, proposal.with, proposal.sessionId, RtpEndUserState.DECLINED_OR_BUSY); + mXmppConnectionService.notifyJingleRtpConnectionUpdate( + account, + proposal.with, + proposal.sessionId, + RtpEndUserState.DECLINED_OR_BUSY); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": no rtp session proposal found for " + from + " to deliver reject"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": no rtp session proposal found for " + + from + + " to deliver reject"); } } } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retrieved out of order jingle message" + message); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": retrieved out of order jingle message" + + message); } - } - private RtpSessionProposal getRtpSessionProposal(final Account account, Jid from, String sessionId) { + private RtpSessionProposal getRtpSessionProposal( + final Account account, Jid from, String sessionId) { for (RtpSessionProposal rtpSessionProposal : rtpSessionProposals.keySet()) { - if (rtpSessionProposal.sessionId.equals(sessionId) && rtpSessionProposal.with.equals(from) && rtpSessionProposal.account.getJid().equals(account.getJid())) { + if (rtpSessionProposal.sessionId.equals(sessionId) + && rtpSessionProposal.with.equals(from) + && rtpSessionProposal.account.getJid().equals(account.getJid())) { return rtpSessionProposal; } } return null; } - private void writeLogMissedOutgoing(final Account account, Jid with, final String sessionId, String serverMsgId, long timestamp) { - final Conversation conversation = mXmppConnectionService.findOrCreateConversation( - account, - with.asBareJid(), - false, - false - ); - final Message message = new Message( - conversation, - Message.STATUS_SEND, - Message.TYPE_RTP_SESSION, - sessionId - ); + private void writeLogMissedOutgoing( + final Account account, + Jid with, + final String sessionId, + String serverMsgId, + long timestamp) { + final Conversation conversation = + mXmppConnectionService.findOrCreateConversation( + account, with.asBareJid(), false, false); + final Message message = + new Message(conversation, Message.STATUS_SEND, Message.TYPE_RTP_SESSION, sessionId); message.setBody(new RtpSessionStatus(false, 0).toString()); message.setServerMsgId(serverMsgId); message.setTime(timestamp); writeMessage(message); } - private void writeLogMissedIncoming(final Account account, Jid with, final String sessionId, String serverMsgId, long timestamp) { - final Conversation conversation = mXmppConnectionService.findOrCreateConversation( - account, - with.asBareJid(), - false, - false - ); - final Message message = new Message( - conversation, - Message.STATUS_RECEIVED, - Message.TYPE_RTP_SESSION, - sessionId - ); + private void writeLogMissedIncoming( + final Account account, + Jid with, + final String sessionId, + String serverMsgId, + long timestamp) { + final Conversation conversation = + mXmppConnectionService.findOrCreateConversation( + account, with.asBareJid(), false, false); + final Message message = + new Message( + conversation, Message.STATUS_RECEIVED, Message.TYPE_RTP_SESSION, sessionId); message.setBody(new RtpSessionStatus(false, 0).toString()); message.setServerMsgId(serverMsgId); message.setTime(timestamp); @@ -430,34 +542,41 @@ private void writeMessage(final Message message) { } public void startJingleFileTransfer(final Message message) { - Preconditions.checkArgument(message.isFileOrImage(), "Message is not of type file or image"); + Preconditions.checkArgument( + message.isFileOrImage(), "Message is not of type file or image"); final Transferable old = message.getTransferable(); if (old != null) { old.cancel(); } final Account account = message.getConversation().getAccount(); final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(message); - final JingleFileTransferConnection connection = new JingleFileTransferConnection(this, id, account.getJid()); + final JingleFileTransferConnection connection = + new JingleFileTransferConnection(this, id, account.getJid()); mXmppConnectionService.markMessage(message, Message.STATUS_WAITING); this.connections.put(id, connection); connection.init(message); } public Optional getOngoingRtpConnection(final Contact contact) { - for (final Map.Entry entry : this.connections.entrySet()) { + for (final Map.Entry entry : + this.connections.entrySet()) { if (entry.getValue() instanceof JingleRtpConnection) { final AbstractJingleConnection.Id id = entry.getKey(); - if (id.account == contact.getAccount() && id.with.asBareJid().equals(contact.getJid().asBareJid())) { + if (id.account == contact.getAccount() + && id.with.asBareJid().equals(contact.getJid().asBareJid())) { return Optional.of(id); } } } synchronized (this.rtpSessionProposals) { - for (Map.Entry entry : this.rtpSessionProposals.entrySet()) { + for (Map.Entry entry : + this.rtpSessionProposals.entrySet()) { RtpSessionProposal proposal = entry.getKey(); - if (proposal.account == contact.getAccount() && contact.getJid().asBareJid().equals(proposal.with)) { + if (proposal.account == contact.getAccount() + && contact.getJid().asBareJid().equals(proposal.with)) { final DeviceDiscoveryState preexistingState = entry.getValue(); - if (preexistingState != null && preexistingState != DeviceDiscoveryState.FAILED) { + if (preexistingState != null + && preexistingState != DeviceDiscoveryState.FAILED) { return Optional.of(proposal); } } @@ -473,7 +592,8 @@ void finishConnection(final AbstractJingleConnection connection) { void finishConnectionOrThrow(final AbstractJingleConnection connection) { final AbstractJingleConnection.Id id = connection.getId(); if (this.connections.remove(id) == null) { - throw new IllegalStateException(String.format("Unable to finish connection with id=%s", id.toString())); + throw new IllegalStateException( + String.format("Unable to finish connection with id=%s", id.toString())); } } @@ -492,49 +612,70 @@ public boolean fireJingleRtpConnectionStateUpdates() { return firedUpdates; } - void getPrimaryCandidate(final Account account, final boolean initiator, final OnPrimaryCandidateFound listener) { + void getPrimaryCandidate( + final Account account, + final boolean initiator, + final OnPrimaryCandidateFound listener) { if (Config.DISABLE_PROXY_LOOKUP) { listener.onPrimaryCandidateFound(false, null); return; } if (!this.primaryCandidates.containsKey(account.getJid().asBareJid())) { - final Jid proxy = account.getXmppConnection().findDiscoItemByFeature(Namespace.BYTE_STREAMS); + final Jid proxy = + account.getXmppConnection().findDiscoItemByFeature(Namespace.BYTE_STREAMS); if (proxy != null) { IqPacket iq = new IqPacket(IqPacket.TYPE.GET); iq.setTo(proxy); iq.query(Namespace.BYTE_STREAMS); - account.getXmppConnection().sendIqPacket(iq, new OnIqPacketReceived() { - - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - final Element streamhost = packet.query().findChild("streamhost", Namespace.BYTE_STREAMS); - final String host = streamhost == null ? null : streamhost.getAttribute("host"); - final String port = streamhost == null ? null : streamhost.getAttribute("port"); - if (host != null && port != null) { - try { - JingleCandidate candidate = new JingleCandidate(nextRandomId(), true); - candidate.setHost(host); - candidate.setPort(Integer.parseInt(port)); - candidate.setType(JingleCandidate.TYPE_PROXY); - candidate.setJid(proxy); - candidate.setPriority(655360 + (initiator ? 30 : 0)); - primaryCandidates.put(account.getJid().asBareJid(), candidate); - listener.onPrimaryCandidateFound(true, candidate); - } catch (final NumberFormatException e) { - listener.onPrimaryCandidateFound(false, null); - } - } else { - listener.onPrimaryCandidateFound(false, null); - } - } - }); + account.getXmppConnection() + .sendIqPacket( + iq, + new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived( + Account account, IqPacket packet) { + final Element streamhost = + packet.query() + .findChild( + "streamhost", + Namespace.BYTE_STREAMS); + final String host = + streamhost == null + ? null + : streamhost.getAttribute("host"); + final String port = + streamhost == null + ? null + : streamhost.getAttribute("port"); + if (host != null && port != null) { + try { + JingleCandidate candidate = + new JingleCandidate(nextRandomId(), true); + candidate.setHost(host); + candidate.setPort(Integer.parseInt(port)); + candidate.setType(JingleCandidate.TYPE_PROXY); + candidate.setJid(proxy); + candidate.setPriority( + 655360 + (initiator ? 30 : 0)); + primaryCandidates.put( + account.getJid().asBareJid(), candidate); + listener.onPrimaryCandidateFound(true, candidate); + } catch (final NumberFormatException e) { + listener.onPrimaryCandidateFound(false, null); + } + } else { + listener.onPrimaryCandidateFound(false, null); + } + } + }); } else { listener.onPrimaryCandidateFound(false, null); } } else { - listener.onPrimaryCandidateFound(true, - this.primaryCandidates.get(account.getJid().asBareJid())); + listener.onPrimaryCandidateFound( + true, this.primaryCandidates.get(account.getJid().asBareJid())); } } @@ -556,64 +697,77 @@ public void retractSessionProposal(final Account account, final Jid with) { private void retractSessionProposal(RtpSessionProposal rtpSessionProposal) { final Account account = rtpSessionProposal.account; toneManager.transition(RtpEndUserState.ENDED, rtpSessionProposal.media); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retracting rtp session proposal with " + rtpSessionProposal.with); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": retracting rtp session proposal with " + + rtpSessionProposal.with); this.rtpSessionProposals.remove(rtpSessionProposal); - final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionRetract(rtpSessionProposal); - writeLogMissedOutgoing(account, rtpSessionProposal.with, rtpSessionProposal.sessionId, null, System.currentTimeMillis()); + final MessagePacket messagePacket = + mXmppConnectionService.getMessageGenerator().sessionRetract(rtpSessionProposal); + writeLogMissedOutgoing( + account, + rtpSessionProposal.with, + rtpSessionProposal.sessionId, + null, + System.currentTimeMillis()); mXmppConnectionService.sendMessagePacket(account, messagePacket); } - public String initializeRtpSession(final Account account, final Jid with, final Set media) { + public String initializeRtpSession( + final Account account, final Jid with, final Set media) { final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with); - final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, account.getJid()); + final JingleRtpConnection rtpConnection = + new JingleRtpConnection(this, id, account.getJid()); rtpConnection.setProposedMedia(media); this.connections.put(id, rtpConnection); rtpConnection.sendSessionInitiate(); return id.sessionId; } - public void proposeJingleRtpSession(final Account account, final Jid with, final Set media) { + public void proposeJingleRtpSession( + final Account account, final Jid with, final Set media) { synchronized (this.rtpSessionProposals) { - for (Map.Entry entry : this.rtpSessionProposals.entrySet()) { + for (Map.Entry entry : + this.rtpSessionProposals.entrySet()) { RtpSessionProposal proposal = entry.getKey(); if (proposal.account == account && with.asBareJid().equals(proposal.with)) { final DeviceDiscoveryState preexistingState = entry.getValue(); - if (preexistingState != null && preexistingState != DeviceDiscoveryState.FAILED) { + if (preexistingState != null + && preexistingState != DeviceDiscoveryState.FAILED) { final RtpEndUserState endUserState = preexistingState.toEndUserState(); toneManager.transition(endUserState, media); mXmppConnectionService.notifyJingleRtpConnectionUpdate( - account, - with, - proposal.sessionId, - endUserState - ); + account, with, proposal.sessionId, endUserState); return; } } } if (isBusy()) { if (hasMatchingRtpSession(account, with, media)) { - Log.d(Config.LOGTAG, "ignoring request to propose jingle session because the other party already created one for us"); + Log.d( + Config.LOGTAG, + "ignoring request to propose jingle session because the other party already created one for us"); return; } - throw new IllegalStateException("There is already a running RTP session. This should have been caught by the UI"); + throw new IllegalStateException( + "There is already a running RTP session. This should have been caught by the UI"); } - final RtpSessionProposal proposal = RtpSessionProposal.of(account, with.asBareJid(), media); + final RtpSessionProposal proposal = + RtpSessionProposal.of(account, with.asBareJid(), media); this.rtpSessionProposals.put(proposal, DeviceDiscoveryState.SEARCHING); mXmppConnectionService.notifyJingleRtpConnectionUpdate( - account, - proposal.with, - proposal.sessionId, - RtpEndUserState.FINDING_DEVICE - ); - final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionProposal(proposal); + account, proposal.with, proposal.sessionId, RtpEndUserState.FINDING_DEVICE); + final MessagePacket messagePacket = + mXmppConnectionService.getMessageGenerator().sessionProposal(proposal); mXmppConnectionService.sendMessagePacket(account, messagePacket); } } public boolean hasMatchingProposal(final Account account, final Jid with) { synchronized (this.rtpSessionProposals) { - for (Map.Entry entry : this.rtpSessionProposals.entrySet()) { + for (Map.Entry entry : + this.rtpSessionProposals.entrySet()) { final RtpSessionProposal proposal = entry.getKey(); if (proposal.account == account && with.asBareJid().equals(proposal.with)) { return true; @@ -642,10 +796,12 @@ public void deliverIbbPacket(Account account, IqPacket packet) { if (sid != null) { for (final AbstractJingleConnection connection : this.connections.values()) { if (connection instanceof JingleFileTransferConnection) { - final JingleFileTransferConnection fileTransfer = (JingleFileTransferConnection) connection; + final JingleFileTransferConnection fileTransfer = + (JingleFileTransferConnection) connection; final JingleTransport transport = fileTransfer.getTransport(); if (transport instanceof JingleInBandTransport) { - final JingleInBandTransport inBandTransport = (JingleInBandTransport) transport; + final JingleInBandTransport inBandTransport = + (JingleInBandTransport) transport; if (inBandTransport.matches(account, sid)) { inBandTransport.deliverPayload(packet, payload); } @@ -655,7 +811,8 @@ public void deliverIbbPacket(Account account, IqPacket packet) { } } Log.d(Config.LOGTAG, "unable to deliver ibb packet: " + packet.toString()); - account.getXmppConnection().sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null); + account.getXmppConnection() + .sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null); } public void notifyRebound(final Account account) { @@ -668,8 +825,10 @@ public void notifyRebound(final Account account) { } } - public WeakReference findJingleRtpConnection(Account account, Jid with, String sessionId) { - final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with, sessionId); + public WeakReference findJingleRtpConnection( + Account account, Jid with, String sessionId) { + final AbstractJingleConnection.Id id = + AbstractJingleConnection.Id.of(account, with, sessionId); final AbstractJingleConnection connection = connections.get(id); if (connection instanceof JingleRtpConnection) { return new WeakReference<>((JingleRtpConnection) connection); @@ -679,34 +838,53 @@ public WeakReference findJingleRtpConnection(Account accoun private void resendSessionProposals(final Account account) { synchronized (this.rtpSessionProposals) { - for (final Map.Entry entry : this.rtpSessionProposals.entrySet()) { + for (final Map.Entry entry : + this.rtpSessionProposals.entrySet()) { final RtpSessionProposal proposal = entry.getKey(); - if (entry.getValue() == DeviceDiscoveryState.SEARCHING && proposal.account == account) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resending session proposal to " + proposal.with); - final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionProposal(proposal); + if (entry.getValue() == DeviceDiscoveryState.SEARCHING + && proposal.account == account) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": resending session proposal to " + + proposal.with); + final MessagePacket messagePacket = + mXmppConnectionService.getMessageGenerator().sessionProposal(proposal); mXmppConnectionService.sendMessagePacket(account, messagePacket); } } } } - public void updateProposedSessionDiscovered(Account account, Jid from, String sessionId, final DeviceDiscoveryState target) { + public void updateProposedSessionDiscovered( + Account account, Jid from, String sessionId, final DeviceDiscoveryState target) { synchronized (this.rtpSessionProposals) { - final RtpSessionProposal sessionProposal = getRtpSessionProposal(account, from.asBareJid(), sessionId); - final DeviceDiscoveryState currentState = sessionProposal == null ? null : rtpSessionProposals.get(sessionProposal); + final RtpSessionProposal sessionProposal = + getRtpSessionProposal(account, from.asBareJid(), sessionId); + final DeviceDiscoveryState currentState = + sessionProposal == null ? null : rtpSessionProposals.get(sessionProposal); if (currentState == null) { Log.d(Config.LOGTAG, "unable to find session proposal for session id " + sessionId); return; } if (currentState == DeviceDiscoveryState.DISCOVERED) { - Log.d(Config.LOGTAG, "session proposal already at discovered. not going to fall back"); + Log.d( + Config.LOGTAG, + "session proposal already at discovered. not going to fall back"); return; } this.rtpSessionProposals.put(sessionProposal, target); final RtpEndUserState endUserState = target.toEndUserState(); toneManager.transition(endUserState, sessionProposal.media); - mXmppConnectionService.notifyJingleRtpConnectionUpdate(account, sessionProposal.with, sessionProposal.sessionId, endUserState); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": flagging session " + sessionId + " as " + target); + mXmppConnectionService.notifyJingleRtpConnectionUpdate( + account, sessionProposal.with, sessionProposal.sessionId, endUserState); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": flagging session " + + sessionId + + " as " + + target); } } @@ -731,7 +909,8 @@ public void endRtpSession(final String sessionId) { } public void failProceed(Account account, final Jid with, String sessionId) { - final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with, sessionId); + final AbstractJingleConnection.Id id = + AbstractJingleConnection.Id.of(account, with, sessionId); final AbstractJingleConnection existingJingleConnection = connections.get(id); if (existingJingleConnection instanceof JingleRtpConnection) { ((JingleRtpConnection) existingJingleConnection).deliverFailedProceed(); @@ -742,13 +921,17 @@ void ensureConnectionIsRegistered(final AbstractJingleConnection connection) { if (connections.containsValue(connection)) { return; } - final IllegalStateException e = new IllegalStateException("JingleConnection has not been registered with connection manager"); + final IllegalStateException e = + new IllegalStateException( + "JingleConnection has not been registered with connection manager"); Log.e(Config.LOGTAG, "ensureConnectionIsRegistered() failed. Going to throw", e); throw e; } - void setTerminalSessionState(AbstractJingleConnection.Id id, final RtpEndUserState state, final Set media) { - this.terminatedSessions.put(PersistableSessionId.of(id), new TerminatedRtpSession(state, media)); + void setTerminalSessionState( + AbstractJingleConnection.Id id, final RtpEndUserState state, final Set media) { + this.terminatedSessions.put( + PersistableSessionId.of(id), new TerminatedRtpSession(state, media)); } public TerminatedRtpSession getTerminalSessionState(final Jid with, final String sessionId) { @@ -773,8 +956,7 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; PersistableSessionId that = (PersistableSessionId) o; - return Objects.equal(with, that.with) && - Objects.equal(sessionId, that.sessionId); + return Objects.equal(with, that.with) && Objects.equal(sessionId, that.sessionId); } @Override @@ -794,7 +976,10 @@ public static class TerminatedRtpSession { } public enum DeviceDiscoveryState { - SEARCHING, SEARCHING_ACKNOWLEDGED, DISCOVERED, FAILED; + SEARCHING, + SEARCHING_ACKNOWLEDGED, + DISCOVERED, + FAILED; public RtpEndUserState toEndUserState() { switch (this) { @@ -835,9 +1020,9 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; RtpSessionProposal proposal = (RtpSessionProposal) o; - return Objects.equal(account.getJid(), proposal.account.getJid()) && - Objects.equal(with, proposal.with) && - Objects.equal(sessionId, proposal.sessionId); + return Objects.equal(account.getJid(), proposal.account.getJid()) + && Objects.equal(with, proposal.with) + && Objects.equal(sessionId, proposal.sessionId); } @Override diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 9ed4d188f..d834d81a1 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -33,6 +33,8 @@ import java.util.Map; import java.util.Queue; import java.util.Set; +import java.util.Timer; +import java.util.TimerTask; import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -61,91 +63,103 @@ import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; -public class JingleRtpConnection extends AbstractJingleConnection implements WebRTCWrapper.EventCallback { +public class JingleRtpConnection extends AbstractJingleConnection + implements WebRTCWrapper.EventCallback { - public static final List STATES_SHOWING_ONGOING_CALL = Arrays.asList( - State.PROCEED, - State.SESSION_INITIALIZED_PRE_APPROVED, - State.SESSION_ACCEPTED - ); + public static final List STATES_SHOWING_ONGOING_CALL = + Arrays.asList( + State.PROCEED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED); private static final long BUSY_TIME_OUT = 30; - private static final List TERMINATED = Arrays.asList( - State.ACCEPTED, - State.REJECTED, - State.REJECTED_RACED, - State.RETRACTED, - State.RETRACTED_RACED, - State.TERMINATED_SUCCESS, - State.TERMINATED_DECLINED_OR_BUSY, - State.TERMINATED_CONNECTIVITY_ERROR, - State.TERMINATED_CANCEL_OR_TIMEOUT, - State.TERMINATED_APPLICATION_FAILURE, - State.TERMINATED_SECURITY_ERROR - ); + private static final List TERMINATED = + Arrays.asList( + State.ACCEPTED, + State.REJECTED, + State.REJECTED_RACED, + State.RETRACTED, + State.RETRACTED_RACED, + State.TERMINATED_SUCCESS, + State.TERMINATED_DECLINED_OR_BUSY, + State.TERMINATED_CONNECTIVITY_ERROR, + State.TERMINATED_CANCEL_OR_TIMEOUT, + State.TERMINATED_APPLICATION_FAILURE, + State.TERMINATED_SECURITY_ERROR); private static final Map> VALID_TRANSITIONS; static { - final ImmutableMap.Builder> transitionBuilder = new ImmutableMap.Builder<>(); - transitionBuilder.put(State.NULL, ImmutableList.of( + final ImmutableMap.Builder> transitionBuilder = + new ImmutableMap.Builder<>(); + transitionBuilder.put( + State.NULL, + ImmutableList.of( + State.PROPOSED, + State.SESSION_INITIALIZED, + State.TERMINATED_APPLICATION_FAILURE, + State.TERMINATED_SECURITY_ERROR)); + transitionBuilder.put( State.PROPOSED, - State.SESSION_INITIALIZED, - State.TERMINATED_APPLICATION_FAILURE, - State.TERMINATED_SECURITY_ERROR - )); - transitionBuilder.put(State.PROPOSED, ImmutableList.of( - State.ACCEPTED, + ImmutableList.of( + State.ACCEPTED, + State.PROCEED, + State.REJECTED, + State.RETRACTED, + State.TERMINATED_APPLICATION_FAILURE, + State.TERMINATED_SECURITY_ERROR, + State.TERMINATED_CONNECTIVITY_ERROR // only used when the xmpp connection + // rebinds + )); + transitionBuilder.put( State.PROCEED, - State.REJECTED, - State.RETRACTED, - State.TERMINATED_APPLICATION_FAILURE, - State.TERMINATED_SECURITY_ERROR, - State.TERMINATED_CONNECTIVITY_ERROR //only used when the xmpp connection rebinds - )); - transitionBuilder.put(State.PROCEED, ImmutableList.of( - State.REJECTED_RACED, - State.RETRACTED_RACED, + ImmutableList.of( + State.REJECTED_RACED, + State.RETRACTED_RACED, + State.SESSION_INITIALIZED_PRE_APPROVED, + State.TERMINATED_SUCCESS, + State.TERMINATED_APPLICATION_FAILURE, + State.TERMINATED_SECURITY_ERROR, + State.TERMINATED_CONNECTIVITY_ERROR // at this state used for error + // bounces of the proceed message + )); + transitionBuilder.put( + State.SESSION_INITIALIZED, + ImmutableList.of( + State.SESSION_ACCEPTED, + State.TERMINATED_SUCCESS, + State.TERMINATED_DECLINED_OR_BUSY, + State.TERMINATED_CONNECTIVITY_ERROR, // at this state used for IQ errors + // and IQ timeouts + State.TERMINATED_CANCEL_OR_TIMEOUT, + State.TERMINATED_APPLICATION_FAILURE, + State.TERMINATED_SECURITY_ERROR)); + transitionBuilder.put( State.SESSION_INITIALIZED_PRE_APPROVED, - State.TERMINATED_SUCCESS, - State.TERMINATED_APPLICATION_FAILURE, - State.TERMINATED_SECURITY_ERROR, - State.TERMINATED_CONNECTIVITY_ERROR //at this state used for error bounces of the proceed message - )); - transitionBuilder.put(State.SESSION_INITIALIZED, ImmutableList.of( + ImmutableList.of( + State.SESSION_ACCEPTED, + State.TERMINATED_SUCCESS, + State.TERMINATED_DECLINED_OR_BUSY, + State.TERMINATED_CONNECTIVITY_ERROR, // at this state used for IQ errors + // and IQ timeouts + State.TERMINATED_CANCEL_OR_TIMEOUT, + State.TERMINATED_APPLICATION_FAILURE, + State.TERMINATED_SECURITY_ERROR)); + transitionBuilder.put( State.SESSION_ACCEPTED, - State.TERMINATED_SUCCESS, - State.TERMINATED_DECLINED_OR_BUSY, - State.TERMINATED_CONNECTIVITY_ERROR, //at this state used for IQ errors and IQ timeouts - State.TERMINATED_CANCEL_OR_TIMEOUT, - State.TERMINATED_APPLICATION_FAILURE, - State.TERMINATED_SECURITY_ERROR - )); - transitionBuilder.put(State.SESSION_INITIALIZED_PRE_APPROVED, ImmutableList.of( - State.SESSION_ACCEPTED, - State.TERMINATED_SUCCESS, - State.TERMINATED_DECLINED_OR_BUSY, - State.TERMINATED_CONNECTIVITY_ERROR, //at this state used for IQ errors and IQ timeouts - State.TERMINATED_CANCEL_OR_TIMEOUT, - State.TERMINATED_APPLICATION_FAILURE, - State.TERMINATED_SECURITY_ERROR - )); - transitionBuilder.put(State.SESSION_ACCEPTED, ImmutableList.of( - State.TERMINATED_SUCCESS, - State.TERMINATED_DECLINED_OR_BUSY, - State.TERMINATED_CONNECTIVITY_ERROR, - State.TERMINATED_CANCEL_OR_TIMEOUT, - State.TERMINATED_APPLICATION_FAILURE, - State.TERMINATED_SECURITY_ERROR - )); + ImmutableList.of( + State.TERMINATED_SUCCESS, + State.TERMINATED_DECLINED_OR_BUSY, + State.TERMINATED_CONNECTIVITY_ERROR, + State.TERMINATED_CANCEL_OR_TIMEOUT, + State.TERMINATED_APPLICATION_FAILURE, + State.TERMINATED_SECURITY_ERROR)); VALID_TRANSITIONS = transitionBuilder.build(); } private final WebRTCWrapper webRTCWrapper = new WebRTCWrapper(this); - private final Queue> pendingIceCandidates = new LinkedList<>(); + private final Queue> + pendingIceCandidates = new LinkedList<>(); private final OmemoVerification omemoVerification = new OmemoVerification(); private final Message message; private State state = State.NULL; - private StateTransitionException stateTransitionException; private Set proposedMedia; private RtpContentMap initiatorRtpContentMap; private RtpContentMap responderRtpContentMap; @@ -156,18 +170,16 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) { super(jingleConnectionManager, id, initiator); - final Conversation conversation = jingleConnectionManager.getXmppConnectionService().findOrCreateConversation( - id.account, - id.with.asBareJid(), - false, - false - ); - this.message = new Message( - conversation, - isInitiator() ? Message.STATUS_SEND : Message.STATUS_RECEIVED, - Message.TYPE_RTP_SESSION, - id.sessionId - ); + final Conversation conversation = + jingleConnectionManager + .getXmppConnectionService() + .findOrCreateConversation(id.account, id.with.asBareJid(), false, false); + this.message = + new Message( + conversation, + isInitiator() ? Message.STATUS_SEND : Message.STATUS_RECEIVED, + Message.TYPE_RTP_SESSION, + id.sessionId); } private static State reasonToState(Reason reason) { @@ -208,7 +220,11 @@ synchronized void deliverPacket(final JinglePacket jinglePacket) { break; default: respondOk(jinglePacket); - Log.d(Config.LOGTAG, String.format("%s: received unhandled jingle action %s", id.account.getJid().asBareJid(), jinglePacket.getAction())); + Log.d( + Config.LOGTAG, + String.format( + "%s: received unhandled jingle action %s", + id.account.getJid().asBareJid(), jinglePacket.getAction())); break; } } @@ -222,8 +238,12 @@ synchronized void notifyRebound() { if (!isInitiator() && isInState(State.PROPOSED, State.SESSION_INITIALIZED)) { xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); } - if (isInState(State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) { - //we might have already changed resources (full jid) at this point; so this might not even reach the other party + if (isInState( + State.SESSION_INITIALIZED, + State.SESSION_INITIALIZED_PRE_APPROVED, + State.SESSION_ACCEPTED)) { + // we might have already changed resources (full jid) at this point; so this might not + // even reach the other party sendSessionTerminate(Reason.CONNECTIVITY_ERROR); } else { transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR); @@ -235,9 +255,21 @@ private void receiveSessionTerminate(final JinglePacket jinglePacket) { respondOk(jinglePacket); final JinglePacket.ReasonWrapper wrapper = jinglePacket.getReason(); final State previous = this.state; - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received session terminate reason=" + wrapper.reason + "(" + Strings.nullToEmpty(wrapper.text) + ") while in state " + previous); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received session terminate reason=" + + wrapper.reason + + "(" + + Strings.nullToEmpty(wrapper.text) + + ") while in state " + + previous); if (TERMINATED.contains(previous)) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring session terminate because already in " + previous); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": ignoring session terminate because already in " + + previous); return; } webRTCWrapper.close(); @@ -251,13 +283,23 @@ private void receiveSessionTerminate(final JinglePacket jinglePacket) { } private void receiveTransportInfo(final JinglePacket jinglePacket) { - //Due to the asynchronicity of processing session-init we might move from NULL|PROCEED to INITIALIZED only after transport-info has been received - if (isInState(State.NULL, State.PROCEED, State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) { + // Due to the asynchronicity of processing session-init we might move from NULL|PROCEED to + // INITIALIZED only after transport-info has been received + if (isInState( + State.NULL, + State.PROCEED, + State.SESSION_INITIALIZED, + State.SESSION_INITIALIZED_PRE_APPROVED, + State.SESSION_ACCEPTED)) { final RtpContentMap contentMap; try { contentMap = RtpContentMap.of(jinglePacket); } catch (final IllegalArgumentException | NullPointerException e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents; ignoring", e); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": improperly formatted contents; ignoring", + e); respondOk(jinglePacket); return; } @@ -265,18 +307,27 @@ private void receiveTransportInfo(final JinglePacket jinglePacket) { } else { if (isTerminated()) { respondOk(jinglePacket); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring out-of-order transport info; we where already terminated"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": ignoring out-of-order transport info; we where already terminated"); } else { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received transport info while in state=" + this.state); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received transport info while in state=" + + this.state); terminateWithOutOfOrder(jinglePacket); } } } - private void receiveTransportInfo(final JinglePacket jinglePacket, final RtpContentMap contentMap) { - final Set> candidates = contentMap.contents.entrySet(); + private void receiveTransportInfo( + final JinglePacket jinglePacket, final RtpContentMap contentMap) { + final Set> candidates = + contentMap.contents.entrySet(); if (this.state == State.SESSION_ACCEPTED) { - //zero candidates + modified credentials are an ICE restart offer + // zero candidates + modified credentials are an ICE restart offer if (checkForIceRestart(jinglePacket, contentMap)) { return; } @@ -284,7 +335,10 @@ private void receiveTransportInfo(final JinglePacket jinglePacket, final RtpCont try { processCandidates(candidates); } catch (final WebRTCWrapper.PeerConnectionNotInitialized e) { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnection was not initialized when processing transport info. this usually indicates a race condition that can be ignored"); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": PeerConnection was not initialized when processing transport info. this usually indicates a race condition that can be ignored"); } } else { respondOk(jinglePacket); @@ -292,7 +346,8 @@ private void receiveTransportInfo(final JinglePacket jinglePacket, final RtpCont } } - private boolean checkForIceRestart(final JinglePacket jinglePacket, final RtpContentMap rtpContentMap) { + private boolean checkForIceRestart( + final JinglePacket jinglePacket, final RtpContentMap rtpContentMap) { final RtpContentMap existing = getRemoteContentMap(); final IceUdpTransportInfo.Credentials existingCredentials; final IceUdpTransportInfo.Credentials newCredentials; @@ -306,7 +361,8 @@ private boolean checkForIceRestart(final JinglePacket jinglePacket, final RtpCon if (existingCredentials.equals(newCredentials)) { return false; } - //TODO an alternative approach is to check if we already got an iq result to our ICE-restart + // TODO an alternative approach is to check if we already got an iq result to our + // ICE-restart // and if that's the case we are seeing an answer. // This might be more spec compliant but also more error prone potentially final boolean isOffer = rtpContentMap.emptyCandidates(); @@ -314,10 +370,17 @@ private boolean checkForIceRestart(final JinglePacket jinglePacket, final RtpCon try { if (isOffer) { Log.d(Config.LOGTAG, "received offer to restart ICE " + newCredentials); - restartContentMap = existing.modifiedCredentials(newCredentials, IceUdpTransportInfo.Setup.ACTPASS); + restartContentMap = + existing.modifiedCredentials( + newCredentials, IceUdpTransportInfo.Setup.ACTPASS); } else { final IceUdpTransportInfo.Setup setup = getPeerDtlsSetup(); - Log.d(Config.LOGTAG, "received confirmation of ICE restart" + newCredentials + " peer_setup=" + setup); + Log.d( + Config.LOGTAG, + "received confirmation of ICE restart" + + newCredentials + + " peer_setup=" + + setup); // DTLS setup attribute needs to be rewritten to reflect current peer state // https://groups.google.com/g/discuss-webrtc/c/DfpIMwvUfeM restartContentMap = existing.modifiedCredentials(newCredentials, setup); @@ -333,7 +396,7 @@ private boolean checkForIceRestart(final JinglePacket jinglePacket, final RtpCon respondOk(jinglePacket); final Throwable rootCause = Throwables.getRootCause(exception); if (rootCause instanceof WebRTCWrapper.PeerConnectionNotInitialized) { - //If this happens a termination is already in progress + // If this happens a termination is already in progress Log.d(Config.LOGTAG, "ignoring PeerConnectionNotInitialized on ICE restart"); return true; } @@ -359,13 +422,22 @@ private void storePeerDtlsSetup(final IceUdpTransportInfo.Setup setup) { this.peerDtlsSetup = setup; } - private boolean applyIceRestart(final JinglePacket jinglePacket, final RtpContentMap restartContentMap, final boolean isOffer) throws ExecutionException, InterruptedException { + private boolean applyIceRestart( + final JinglePacket jinglePacket, + final RtpContentMap restartContentMap, + final boolean isOffer) + throws ExecutionException, InterruptedException { final SessionDescription sessionDescription = SessionDescription.of(restartContentMap); - final org.webrtc.SessionDescription.Type type = isOffer ? org.webrtc.SessionDescription.Type.OFFER : org.webrtc.SessionDescription.Type.ANSWER; - org.webrtc.SessionDescription sdp = new org.webrtc.SessionDescription(type, sessionDescription.toString()); + final org.webrtc.SessionDescription.Type type = + isOffer + ? org.webrtc.SessionDescription.Type.OFFER + : org.webrtc.SessionDescription.Type.ANSWER; + org.webrtc.SessionDescription sdp = + new org.webrtc.SessionDescription(type, sessionDescription.toString()); if (isOffer && webRTCWrapper.getSignalingState() != PeerConnection.SignalingState.STABLE) { if (isInitiator()) { - //We ignore the offer and respond with tie-break. This will clause the responder not to apply the content map + // We ignore the offer and respond with tie-break. This will clause the responder + // not to apply the content map return false; } } @@ -375,7 +447,7 @@ private boolean applyIceRestart(final JinglePacket jinglePacket, final RtpConten webRTCWrapper.setIsReadyToReceiveIceCandidates(false); final SessionDescription localSessionDescription = setLocalSessionDescription(); setLocalContentMap(RtpContentMap.of(localSessionDescription)); - //We need to respond OK before sending any candidates + // We need to respond OK before sending any candidates respondOk(jinglePacket); webRTCWrapper.setIsReadyToReceiveIceCandidates(true); } else { @@ -384,32 +456,40 @@ private boolean applyIceRestart(final JinglePacket jinglePacket, final RtpConten return true; } - private void processCandidates(final Set> contents) { + private void processCandidates( + final Set> contents) { for (final Map.Entry content : contents) { processCandidate(content); } } - private void processCandidate(final Map.Entry content) { + private void processCandidate( + final Map.Entry content) { final RtpContentMap rtpContentMap = getRemoteContentMap(); final List indices = toIdentificationTags(rtpContentMap); - final String sdpMid = content.getKey(); //aka content name + final String sdpMid = content.getKey(); // aka content name final IceUdpTransportInfo transport = content.getValue().transport; final IceUdpTransportInfo.Credentials credentials = transport.getCredentials(); - //TODO check that credentials remained the same + // TODO check that credentials remained the same for (final IceUdpTransportInfo.Candidate candidate : transport.getCandidates()) { final String sdp; try { sdp = candidate.toSdpAttribute(credentials.ufrag); } catch (final IllegalArgumentException e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring invalid ICE candidate " + e.getMessage()); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": ignoring invalid ICE candidate " + + e.getMessage()); continue; } final int mLineIndex = indices.indexOf(sdpMid); if (mLineIndex < 0) { - Log.w(Config.LOGTAG, "mLineIndex not found for " + sdpMid + ". available indices " + indices); + Log.w( + Config.LOGTAG, + "mLineIndex not found for " + sdpMid + ". available indices " + indices); } final IceCandidate iceCandidate = new IceCandidate(sdpMid, mLineIndex, sdp); Log.d(Config.LOGTAG, "received candidate: " + iceCandidate); @@ -423,14 +503,21 @@ private RtpContentMap getRemoteContentMap() { private List toIdentificationTags(final RtpContentMap rtpContentMap) { final Group originalGroup = rtpContentMap.group; - final List identificationTags = originalGroup == null ? rtpContentMap.getNames() : originalGroup.getIdentificationTags(); + final List identificationTags = + originalGroup == null + ? rtpContentMap.getNames() + : originalGroup.getIdentificationTags(); if (identificationTags.size() == 0) { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": no identification tags found in initial offer. we won't be able to calculate mLineIndices"); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": no identification tags found in initial offer. we won't be able to calculate mLineIndices"); } return identificationTags; } - private ListenableFuture receiveRtpContentMap(final JinglePacket jinglePacket, final boolean expectVerification) { + private ListenableFuture receiveRtpContentMap( + final JinglePacket jinglePacket, final boolean expectVerification) { final RtpContentMap receivedContentMap; try { receivedContentMap = RtpContentMap.of(jinglePacket); @@ -438,17 +525,26 @@ private ListenableFuture receiveRtpContentMap(final JinglePacket return Futures.immediateFailedFuture(e); } if (receivedContentMap instanceof OmemoVerifiedRtpContentMap) { - final ListenableFuture> future = id.account.getAxolotlService().decrypt((OmemoVerifiedRtpContentMap) receivedContentMap, id.with); - return Futures.transform(future, omemoVerifiedPayload -> { - //TODO test if an exception here triggers a correct abort - omemoVerification.setOrEnsureEqual(omemoVerifiedPayload); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received verifiable DTLS fingerprint via " + omemoVerification); - return omemoVerifiedPayload.getPayload(); - }, MoreExecutors.directExecutor()); + final ListenableFuture> future = + id.account + .getAxolotlService() + .decrypt((OmemoVerifiedRtpContentMap) receivedContentMap, id.with); + return Futures.transform( + future, + omemoVerifiedPayload -> { + // TODO test if an exception here triggers a correct abort + omemoVerification.setOrEnsureEqual(omemoVerifiedPayload); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received verifiable DTLS fingerprint via " + + omemoVerification); + return omemoVerifiedPayload.getPayload(); + }, + MoreExecutors.directExecutor()); } else if (Config.REQUIRE_RTP_VERIFICATION || expectVerification) { return Futures.immediateFailedFuture( - new SecurityException("DTLS fingerprint was unexpectedly not verifiable") - ); + new SecurityException("DTLS fingerprint was unexpectedly not verifiable")); } else { return Futures.immediateFuture(receivedContentMap); } @@ -456,13 +552,17 @@ private ListenableFuture receiveRtpContentMap(final JinglePacket private void receiveSessionInitiate(final JinglePacket jinglePacket) { if (isInitiator()) { - Log.d(Config.LOGTAG, String.format("%s: received session-initiate even though we were initiating", id.account.getJid().asBareJid())); + Log.d( + Config.LOGTAG, + String.format( + "%s: received session-initiate even though we were initiating", + id.account.getJid().asBareJid())); if (isTerminated()) { - Log.d(Config.LOGTAG, String.format( - "%s: got a reason to terminate with out-of-order. but already in state %s", - id.account.getJid().asBareJid(), - getState() - )); + Log.d( + Config.LOGTAG, + String.format( + "%s: got a reason to terminate with out-of-order. but already in state %s", + id.account.getJid().asBareJid(), getState())); respondWithOutOfOrder(jinglePacket); } else { terminateWithOutOfOrder(jinglePacket); @@ -470,43 +570,51 @@ private void receiveSessionInitiate(final JinglePacket jinglePacket) { return; } final ListenableFuture future = receiveRtpContentMap(jinglePacket, false); - Futures.addCallback(future, new FutureCallback() { - @Override - public void onSuccess(@Nullable RtpContentMap rtpContentMap) { - receiveSessionInitiate(jinglePacket, rtpContentMap); - } + Futures.addCallback( + future, + new FutureCallback() { + @Override + public void onSuccess(@Nullable RtpContentMap rtpContentMap) { + receiveSessionInitiate(jinglePacket, rtpContentMap); + } - @Override - public void onFailure(@NonNull final Throwable throwable) { - respondOk(jinglePacket); - sendSessionTerminate(Reason.ofThrowable(throwable), throwable.getMessage()); - } - }, MoreExecutors.directExecutor()); + @Override + public void onFailure(@NonNull final Throwable throwable) { + respondOk(jinglePacket); + sendSessionTerminate(Reason.ofThrowable(throwable), throwable.getMessage()); + } + }, + MoreExecutors.directExecutor()); } - private void receiveSessionInitiate(final JinglePacket jinglePacket, final RtpContentMap contentMap) { + private void receiveSessionInitiate( + final JinglePacket jinglePacket, final RtpContentMap contentMap) { try { contentMap.requireContentDescriptions(); contentMap.requireDTLSFingerprint(true); } catch (final RuntimeException e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", Throwables.getRootCause(e)); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": improperly formatted contents", + Throwables.getRootCause(e)); respondOk(jinglePacket); sendSessionTerminate(Reason.of(e), e.getMessage()); return; } - Log.d(Config.LOGTAG, "processing session-init with " + contentMap.contents.size() + " contents"); + Log.d( + Config.LOGTAG, + "processing session-init with " + contentMap.contents.size() + " contents"); final State target; if (this.state == State.PROCEED) { Preconditions.checkState( proposedMedia != null && proposedMedia.size() > 0, - "proposed media must be set when processing pre-approved session-initiate" - ); + "proposed media must be set when processing pre-approved session-initiate"); if (!this.proposedMedia.equals(contentMap.getMedia())) { - sendSessionTerminate(Reason.SECURITY_ERROR, String.format( - "Your session proposal (Jingle Message Initiation) included media %s but your session-initiate was %s", - this.proposedMedia, - contentMap.getMedia() - )); + sendSessionTerminate( + Reason.SECURITY_ERROR, + String.format( + "Your session proposal (Jingle Message Initiation) included media %s but your session-initiate was %s", + this.proposedMedia, contentMap.getMedia())); return; } target = State.SESSION_INITIALIZED_PRE_APPROVED; @@ -517,67 +625,100 @@ private void receiveSessionInitiate(final JinglePacket jinglePacket, final RtpCo respondOk(jinglePacket); pendingIceCandidates.addAll(contentMap.contents.entrySet()); if (target == State.SESSION_INITIALIZED_PRE_APPROVED) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": automatically accepting session-initiate"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": automatically accepting session-initiate"); sendSessionAccept(); } else { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received not pre-approved session-initiate. start ringing"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received not pre-approved session-initiate. start ringing"); startRinging(); } } else { - Log.d(Config.LOGTAG, String.format("%s: received session-initiate while in state %s", id.account.getJid().asBareJid(), state)); + Log.d( + Config.LOGTAG, + String.format( + "%s: received session-initiate while in state %s", + id.account.getJid().asBareJid(), state)); terminateWithOutOfOrder(jinglePacket); } } private void receiveSessionAccept(final JinglePacket jinglePacket) { if (!isInitiator()) { - Log.d(Config.LOGTAG, String.format("%s: received session-accept even though we were responding", id.account.getJid().asBareJid())); + Log.d( + Config.LOGTAG, + String.format( + "%s: received session-accept even though we were responding", + id.account.getJid().asBareJid())); terminateWithOutOfOrder(jinglePacket); return; } - final ListenableFuture future = receiveRtpContentMap(jinglePacket, this.omemoVerification.hasFingerprint()); - Futures.addCallback(future, new FutureCallback() { - @Override - public void onSuccess(@Nullable RtpContentMap rtpContentMap) { - receiveSessionAccept(jinglePacket, rtpContentMap); - } + final ListenableFuture future = + receiveRtpContentMap(jinglePacket, this.omemoVerification.hasFingerprint()); + Futures.addCallback( + future, + new FutureCallback() { + @Override + public void onSuccess(@Nullable RtpContentMap rtpContentMap) { + receiveSessionAccept(jinglePacket, rtpContentMap); + } - @Override - public void onFailure(@NonNull final Throwable throwable) { - respondOk(jinglePacket); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents in session-accept", throwable); - webRTCWrapper.close(); - sendSessionTerminate(Reason.ofThrowable(throwable), throwable.getMessage()); - } - }, MoreExecutors.directExecutor()); + @Override + public void onFailure(@NonNull final Throwable throwable) { + respondOk(jinglePacket); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": improperly formatted contents in session-accept", + throwable); + webRTCWrapper.close(); + sendSessionTerminate(Reason.ofThrowable(throwable), throwable.getMessage()); + } + }, + MoreExecutors.directExecutor()); } - private void receiveSessionAccept(final JinglePacket jinglePacket, final RtpContentMap contentMap) { + private void receiveSessionAccept( + final JinglePacket jinglePacket, final RtpContentMap contentMap) { try { contentMap.requireContentDescriptions(); contentMap.requireDTLSFingerprint(); } catch (final RuntimeException e) { respondOk(jinglePacket); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents in session-accept", e); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": improperly formatted contents in session-accept", + e); webRTCWrapper.close(); sendSessionTerminate(Reason.of(e), e.getMessage()); return; } final Set initiatorMedia = this.initiatorRtpContentMap.getMedia(); if (!initiatorMedia.equals(contentMap.getMedia())) { - sendSessionTerminate(Reason.SECURITY_ERROR, String.format( - "Your session-included included media %s but our session-initiate was %s", - this.proposedMedia, - contentMap.getMedia() - )); + sendSessionTerminate( + Reason.SECURITY_ERROR, + String.format( + "Your session-included included media %s but our session-initiate was %s", + this.proposedMedia, contentMap.getMedia())); return; } - Log.d(Config.LOGTAG, "processing session-accept with " + contentMap.contents.size() + " contents"); + Log.d( + Config.LOGTAG, + "processing session-accept with " + contentMap.contents.size() + " contents"); if (transition(State.SESSION_ACCEPTED)) { respondOk(jinglePacket); receiveSessionAccept(contentMap); } else { - Log.d(Config.LOGTAG, String.format("%s: received session-accept while in state %s", id.account.getJid().asBareJid(), state)); + Log.d( + Config.LOGTAG, + String.format( + "%s: received session-accept while in state %s", + id.account.getJid().asBareJid(), state)); respondOk(jinglePacket); } } @@ -589,21 +730,29 @@ private void receiveSessionAccept(final RtpContentMap contentMap) { try { sessionDescription = SessionDescription.of(contentMap); } catch (final IllegalArgumentException | NullPointerException e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable convert offer from session-accept to SDP", e); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": unable convert offer from session-accept to SDP", + e); webRTCWrapper.close(); sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage()); return; } - final org.webrtc.SessionDescription answer = new org.webrtc.SessionDescription( - org.webrtc.SessionDescription.Type.ANSWER, - sessionDescription.toString() - ); + final org.webrtc.SessionDescription answer = + new org.webrtc.SessionDescription( + org.webrtc.SessionDescription.Type.ANSWER, sessionDescription.toString()); try { this.webRTCWrapper.setRemoteDescription(answer).get(); } catch (final Exception e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to set remote description after receiving session-accept", Throwables.getRootCause(e)); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": unable to set remote description after receiving session-accept", + Throwables.getRootCause(e)); webRTCWrapper.close(); - sendSessionTerminate(Reason.FAILED_APPLICATION, Throwables.getRootCause(e).getMessage()); + sendSessionTerminate( + Reason.FAILED_APPLICATION, Throwables.getRootCause(e).getMessage()); return; } processCandidates(contentMap.contents.entrySet()); @@ -618,7 +767,11 @@ private void sendSessionAccept() { try { offer = SessionDescription.of(rtpContentMap); } catch (final IllegalArgumentException | NullPointerException e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable convert offer from session-initiate to SDP", e); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": unable convert offer from session-initiate to SDP", + e); webRTCWrapper.close(); sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage()); return; @@ -630,9 +783,15 @@ private void sendSessionAccept(final Set media, final SessionDescription discoverIceServers(iceServers -> sendSessionAccept(media, offer, iceServers)); } - private synchronized void sendSessionAccept(final Set media, final SessionDescription offer, final List iceServers) { + private synchronized void sendSessionAccept( + final Set media, + final SessionDescription offer, + final List iceServers) { if (isTerminated()) { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": ICE servers got discovered when session was already terminated. nothing to do."); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": ICE servers got discovered when session was already terminated. nothing to do."); return; } try { @@ -643,14 +802,14 @@ private synchronized void sendSessionAccept(final Set media, final Sessio sendSessionTerminate(Reason.FAILED_APPLICATION); return; } - final org.webrtc.SessionDescription sdp = new org.webrtc.SessionDescription( - org.webrtc.SessionDescription.Type.OFFER, - offer.toString() - ); + final org.webrtc.SessionDescription sdp = + new org.webrtc.SessionDescription( + org.webrtc.SessionDescription.Type.OFFER, offer.toString()); try { this.webRTCWrapper.setRemoteDescription(sdp).get(); addIceCandidatesFromBlackLog(); - org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.setLocalDescription().get(); + org.webrtc.SessionDescription webRTCSessionDescription = + this.webRTCWrapper.setLocalDescription().get(); prepareSessionAccept(webRTCSessionDescription); } catch (final Exception e) { failureToAcceptSession(e); @@ -671,18 +830,24 @@ private void addIceCandidatesFromBlackLog() { Map.Entry foo; while ((foo = this.pendingIceCandidates.poll()) != null) { processCandidate(foo); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": added candidate from back log"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": added candidate from back log"); } } - private void prepareSessionAccept(final org.webrtc.SessionDescription webRTCSessionDescription) { - final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); + private void prepareSessionAccept( + final org.webrtc.SessionDescription webRTCSessionDescription) { + final SessionDescription sessionDescription = + SessionDescription.parse(webRTCSessionDescription.description); final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription); this.responderRtpContentMap = respondingRtpContentMap; storePeerDtlsSetup(respondingRtpContentMap.getDtlsSetup().flip()); webRTCWrapper.setIsReadyToReceiveIceCandidates(true); - final ListenableFuture outgoingContentMapFuture = prepareOutgoingContentMap(respondingRtpContentMap); - Futures.addCallback(outgoingContentMapFuture, + final ListenableFuture outgoingContentMapFuture = + prepareOutgoingContentMap(respondingRtpContentMap); + Futures.addCallback( + outgoingContentMapFuture, new FutureCallback() { @Override public void onSuccess(final RtpContentMap outgoingContentMap) { @@ -694,35 +859,56 @@ public void onFailure(@NonNull Throwable throwable) { failureToAcceptSession(throwable); } }, - MoreExecutors.directExecutor() - ); + MoreExecutors.directExecutor()); } private void sendSessionAccept(final RtpContentMap rtpContentMap) { if (isTerminated()) { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": preparing session accept was too slow. already terminated. nothing to do."); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": preparing session accept was too slow. already terminated. nothing to do."); return; } transitionOrThrow(State.SESSION_ACCEPTED); - final JinglePacket sessionAccept = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId); + final JinglePacket sessionAccept = + rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId); send(sessionAccept); } - private ListenableFuture prepareOutgoingContentMap(final RtpContentMap rtpContentMap) { + private ListenableFuture prepareOutgoingContentMap( + final RtpContentMap rtpContentMap) { if (this.omemoVerification.hasDeviceId()) { - ListenableFuture> verifiedPayloadFuture = id.account.getAxolotlService() - .encrypt(rtpContentMap, id.with, omemoVerification.getDeviceId()); - return Futures.transform(verifiedPayloadFuture, verifiedPayload -> { - omemoVerification.setOrEnsureEqual(verifiedPayload); - return verifiedPayload.getPayload(); - }, MoreExecutors.directExecutor()); + ListenableFuture> + verifiedPayloadFuture = + id.account + .getAxolotlService() + .encrypt( + rtpContentMap, + id.with, + omemoVerification.getDeviceId()); + return Futures.transform( + verifiedPayloadFuture, + verifiedPayload -> { + omemoVerification.setOrEnsureEqual(verifiedPayload); + return verifiedPayload.getPayload(); + }, + MoreExecutors.directExecutor()); } else { return Futures.immediateFuture(rtpContentMap); } } - synchronized void deliveryMessage(final Jid from, final Element message, final String serverMessageId, final long timestamp) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": delivered message to JingleRtpConnection " + message); + synchronized void deliveryMessage( + final Jid from, + final Element message, + final String serverMessageId, + final long timestamp) { + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": delivered message to JingleRtpConnection " + + message); switch (message.getName()) { case "propose": receivePropose(from, Propose.upgrade(message), serverMessageId, timestamp); @@ -745,47 +931,73 @@ synchronized void deliveryMessage(final Jid from, final Element message, final S } void deliverFailedProceed() { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": receive message error for proceed message"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": receive message error for proceed message"); if (transition(State.TERMINATED_CONNECTIVITY_ERROR)) { webRTCWrapper.close(); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": transitioned into connectivity error"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": transitioned into connectivity error"); this.finish(); } } private void receiveAccept(final Jid from, final String serverMsgId, final long timestamp) { - final boolean originatedFromMyself = from.asBareJid().equals(id.account.getJid().asBareJid()); + final boolean originatedFromMyself = + from.asBareJid().equals(id.account.getJid().asBareJid()); if (originatedFromMyself) { if (transition(State.ACCEPTED)) { if (serverMsgId != null) { this.message.setServerMsgId(serverMsgId); } this.message.setTime(timestamp); - this.message.setCarbon(true); //indicate that call was accepted on other device + this.message.setCarbon(true); // indicate that call was accepted on other device this.writeLogMessageSuccess(0); - this.xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); + this.xmppConnectionService + .getNotificationService() + .cancelIncomingCallNotification(); this.finish(); } else { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to transition to accept because already in state=" + this.state); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": unable to transition to accept because already in state=" + + this.state); } } else { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring 'accept' from " + from); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": ignoring 'accept' from " + from); } } private void receiveReject(final Jid from, final String serverMsgId, final long timestamp) { - final boolean originatedFromMyself = from.asBareJid().equals(id.account.getJid().asBareJid()); - //reject from another one of my clients + final boolean originatedFromMyself = + from.asBareJid().equals(id.account.getJid().asBareJid()); + // reject from another one of my clients if (originatedFromMyself) { receiveRejectFromMyself(serverMsgId, timestamp); } else if (isInitiator()) { if (from.equals(id.with)) { receiveRejectFromResponder(); } else { - Log.d(Config.LOGTAG, id.account.getJid() + ": ignoring reject from " + from + " for session with " + id.with); + Log.d( + Config.LOGTAG, + id.account.getJid() + + ": ignoring reject from " + + from + + " for session with " + + id.with); } } else { - Log.d(Config.LOGTAG, id.account.getJid() + ": ignoring reject from " + from + " for session with " + id.with); + Log.d( + Config.LOGTAG, + id.account.getJid() + + ": ignoring reject from " + + from + + " for session with " + + id.with); } } @@ -797,54 +1009,94 @@ private void receiveRejectFromMyself(String serverMsgId, long timestamp) { this.message.setServerMsgId(serverMsgId); } this.message.setTime(timestamp); - this.message.setCarbon(true); //indicate that call was rejected on other device + this.message.setCarbon(true); // indicate that call was rejected on other device writeLogMessageMissed(); } else { - Log.d(Config.LOGTAG, "not able to transition into REJECTED because already in " + this.state); + Log.d( + Config.LOGTAG, + "not able to transition into REJECTED because already in " + this.state); } } private void receiveRejectFromResponder() { if (isInState(State.PROCEED)) { - Log.d(Config.LOGTAG, id.account.getJid() + ": received reject while still in proceed. callee reconsidered"); + Log.d( + Config.LOGTAG, + id.account.getJid() + + ": received reject while still in proceed. callee reconsidered"); closeTransitionLogFinish(State.REJECTED_RACED); return; } if (isInState(State.SESSION_INITIALIZED_PRE_APPROVED)) { - Log.d(Config.LOGTAG, id.account.getJid() + ": received reject while in SESSION_INITIATED_PRE_APPROVED. callee reconsidered before receiving session-init"); + Log.d( + Config.LOGTAG, + id.account.getJid() + + ": received reject while in SESSION_INITIATED_PRE_APPROVED. callee reconsidered before receiving session-init"); closeTransitionLogFinish(State.TERMINATED_DECLINED_OR_BUSY); return; } - Log.d(Config.LOGTAG, id.account.getJid() + ": ignoring reject from responder because already in state " + this.state); + Log.d( + Config.LOGTAG, + id.account.getJid() + + ": ignoring reject from responder because already in state " + + this.state); } - private void receivePropose(final Jid from, final Propose propose, final String serverMsgId, final long timestamp) { - final boolean originatedFromMyself = from.asBareJid().equals(id.account.getJid().asBareJid()); + private void receivePropose( + final Jid from, final Propose propose, final String serverMsgId, final long timestamp) { + final boolean originatedFromMyself = + from.asBareJid().equals(id.account.getJid().asBareJid()); if (originatedFromMyself) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": saw proposal from myself. ignoring"); - } else if (transition(State.PROPOSED, () -> { - final Collection descriptions = Collections2.transform( - Collections2.filter(propose.getDescriptions(), d -> d instanceof RtpDescription), - input -> (RtpDescription) input - ); - final Collection media = Collections2.transform(descriptions, RtpDescription::getMedia); - Preconditions.checkState(!media.contains(Media.UNKNOWN), "RTP descriptions contain unknown media"); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received session proposal from " + from + " for " + media); - this.proposedMedia = Sets.newHashSet(media); - })) { + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": saw proposal from myself. ignoring"); + } else if (transition( + State.PROPOSED, + () -> { + final Collection descriptions = + Collections2.transform( + Collections2.filter( + propose.getDescriptions(), + d -> d instanceof RtpDescription), + input -> (RtpDescription) input); + final Collection media = + Collections2.transform(descriptions, RtpDescription::getMedia); + Preconditions.checkState( + !media.contains(Media.UNKNOWN), + "RTP descriptions contain unknown media"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received session proposal from " + + from + + " for " + + media); + this.proposedMedia = Sets.newHashSet(media); + })) { if (serverMsgId != null) { this.message.setServerMsgId(serverMsgId); } this.message.setTime(timestamp); startRinging(); } else { - Log.d(Config.LOGTAG, id.account.getJid() + ": ignoring session proposal because already in " + state); + Log.d( + Config.LOGTAG, + id.account.getJid() + + ": ignoring session proposal because already in " + + state); } } private void startRinging() { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received call from " + id.with + ". start ringing"); - ringingTimeoutFuture = jingleConnectionManager.schedule(this::ringingTimeout, BUSY_TIME_OUT, TimeUnit.SECONDS); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received call from " + + id.with + + ". start ringing"); + ringingTimeoutFuture = + jingleConnectionManager.schedule( + this::ringingTimeout, BUSY_TIME_OUT, TimeUnit.SECONDS); xmppConnectionService.getNotificationService().startRinging(id, getMedia()); } @@ -869,8 +1121,11 @@ private void cancelRingingTimeout() { } } - private void receiveProceed(final Jid from, final Proceed proceed, final String serverMsgId, final long timestamp) { - final Set media = Preconditions.checkNotNull(this.proposedMedia, "Proposed media has to be set before handling proceed"); + private void receiveProceed( + final Jid from, final Proceed proceed, final String serverMsgId, final long timestamp) { + final Set media = + Preconditions.checkNotNull( + this.proposedMedia, "Proposed media has to be set before handling proceed"); Preconditions.checkState(media.size() > 0, "Proposed media should not be empty"); if (from.equals(id.with)) { if (isInitiator()) { @@ -884,34 +1139,64 @@ private void receiveProceed(final Jid from, final Proceed proceed, final String this.omemoVerification.setDeviceId(remoteDeviceId); } else { if (remoteDeviceId != null) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": remote party signaled support for OMEMO verification but we have OMEMO disabled"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": remote party signaled support for OMEMO verification but we have OMEMO disabled"); } this.omemoVerification.setDeviceId(null); } this.sendSessionInitiate(media, State.SESSION_INITIALIZED_PRE_APPROVED); } else { - Log.d(Config.LOGTAG, String.format("%s: ignoring proceed because already in %s", id.account.getJid().asBareJid(), this.state)); + Log.d( + Config.LOGTAG, + String.format( + "%s: ignoring proceed because already in %s", + id.account.getJid().asBareJid(), this.state)); } } else { - Log.d(Config.LOGTAG, String.format("%s: ignoring proceed because we were not initializing", id.account.getJid().asBareJid())); + Log.d( + Config.LOGTAG, + String.format( + "%s: ignoring proceed because we were not initializing", + id.account.getJid().asBareJid())); } } else if (from.asBareJid().equals(id.account.getJid().asBareJid())) { if (transition(State.ACCEPTED)) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": moved session with " + id.with + " into state accepted after received carbon copied procced"); - this.xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": moved session with " + + id.with + + " into state accepted after received carbon copied procced"); + this.xmppConnectionService + .getNotificationService() + .cancelIncomingCallNotification(); this.finish(); } } else { - Log.d(Config.LOGTAG, String.format("%s: ignoring proceed from %s. was expected from %s", id.account.getJid().asBareJid(), from, id.with)); + Log.d( + Config.LOGTAG, + String.format( + "%s: ignoring proceed from %s. was expected from %s", + id.account.getJid().asBareJid(), from, id.with)); } } private void receiveRetract(final Jid from, final String serverMsgId, final long timestamp) { if (from.equals(id.with)) { - final State target = this.state == State.PROCEED ? State.RETRACTED_RACED : State.RETRACTED; + final State target = + this.state == State.PROCEED ? State.RETRACTED_RACED : State.RETRACTED; if (transition(target)) { xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": session with " + id.with + " has been retracted (serverMsgId=" + serverMsgId + ")"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": session with " + + id.with + + " has been retracted (serverMsgId=" + + serverMsgId + + ")"); if (serverMsgId != null) { this.message.setServerMsgId(serverMsgId); } @@ -925,8 +1210,15 @@ private void receiveRetract(final Jid from, final String serverMsgId, final long Log.d(Config.LOGTAG, "ignoring retract because already in " + this.state); } } else { - //TODO parse retract from self - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received retract from " + from + ". expected retract from" + id.with + ". ignoring"); + // TODO parse retract from self + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received retract from " + + from + + ". expected retract from" + + id.with + + ". ignoring"); } } @@ -939,9 +1231,15 @@ private void sendSessionInitiate(final Set media, final State targetState discoverIceServers(iceServers -> sendSessionInitiate(media, targetState, iceServers)); } - private synchronized void sendSessionInitiate(final Set media, final State targetState, final List iceServers) { + private synchronized void sendSessionInitiate( + final Set media, + final State targetState, + final List iceServers) { if (isTerminated()) { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": ICE servers got discovered when session was already terminated. nothing to do."); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": ICE servers got discovered when session was already terminated. nothing to do."); return; } try { @@ -953,10 +1251,12 @@ private synchronized void sendSessionInitiate(final Set media, final Stat return; } try { - org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.setLocalDescription().get(); + org.webrtc.SessionDescription webRTCSessionDescription = + this.webRTCWrapper.setLocalDescription().get(); prepareSessionInitiate(webRTCSessionDescription, targetState); } catch (final Exception e) { - //TODO sending the error text is worthwhile as well. Especially for FailureToSet exceptions + // TODO sending the error text is worthwhile as well. Especially for FailureToSet + // exceptions failureToInitiateSession(e, targetState); } } @@ -965,7 +1265,10 @@ private void failureToInitiateSession(final Throwable throwable, final State tar if (isTerminated()) { return; } - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to sendSessionInitiate", Throwables.getRootCause(throwable)); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": unable to sendSessionInitiate", + Throwables.getRootCause(throwable)); webRTCWrapper.close(); final Reason reason = Reason.ofThrowable(throwable); if (isInState(targetState)) { @@ -976,49 +1279,71 @@ private void failureToInitiateSession(final Throwable throwable, final State tar } private void sendRetract(final Reason reason) { - //TODO embed reason into retract + // TODO embed reason into retract sendJingleMessage("retract", id.with.asBareJid()); transitionOrThrow(reasonToState(reason)); this.finish(); } - private void prepareSessionInitiate(final org.webrtc.SessionDescription webRTCSessionDescription, final State targetState) { - final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); + private void prepareSessionInitiate( + final org.webrtc.SessionDescription webRTCSessionDescription, final State targetState) { + final SessionDescription sessionDescription = + SessionDescription.parse(webRTCSessionDescription.description); final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); this.initiatorRtpContentMap = rtpContentMap; this.webRTCWrapper.setIsReadyToReceiveIceCandidates(true); - final ListenableFuture outgoingContentMapFuture = encryptSessionInitiate(rtpContentMap); - Futures.addCallback(outgoingContentMapFuture, new FutureCallback() { - @Override - public void onSuccess(final RtpContentMap outgoingContentMap) { - sendSessionInitiate(outgoingContentMap, targetState); - } + final ListenableFuture outgoingContentMapFuture = + encryptSessionInitiate(rtpContentMap); + Futures.addCallback( + outgoingContentMapFuture, + new FutureCallback() { + @Override + public void onSuccess(final RtpContentMap outgoingContentMap) { + sendSessionInitiate(outgoingContentMap, targetState); + } - @Override - public void onFailure(@NonNull final Throwable throwable) { - failureToInitiateSession(throwable, targetState); - } - }, MoreExecutors.directExecutor()); + @Override + public void onFailure(@NonNull final Throwable throwable) { + failureToInitiateSession(throwable, targetState); + } + }, + MoreExecutors.directExecutor()); } private void sendSessionInitiate(final RtpContentMap rtpContentMap, final State targetState) { if (isTerminated()) { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": preparing session was too slow. already terminated. nothing to do."); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": preparing session was too slow. already terminated. nothing to do."); return; } this.transitionOrThrow(targetState); - final JinglePacket sessionInitiate = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId); + final JinglePacket sessionInitiate = + rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId); send(sessionInitiate); } - private ListenableFuture encryptSessionInitiate(final RtpContentMap rtpContentMap) { + private ListenableFuture encryptSessionInitiate( + final RtpContentMap rtpContentMap) { if (this.omemoVerification.hasDeviceId()) { - final ListenableFuture> verifiedPayloadFuture = id.account.getAxolotlService() - .encrypt(rtpContentMap, id.with, omemoVerification.getDeviceId()); - final ListenableFuture future = Futures.transform(verifiedPayloadFuture, verifiedPayload -> { - omemoVerification.setSessionFingerprint(verifiedPayload.getFingerprint()); - return verifiedPayload.getPayload(); - }, MoreExecutors.directExecutor()); + final ListenableFuture> + verifiedPayloadFuture = + id.account + .getAxolotlService() + .encrypt( + rtpContentMap, + id.with, + omemoVerification.getDeviceId()); + final ListenableFuture future = + Futures.transform( + verifiedPayloadFuture, + verifiedPayload -> { + omemoVerification.setSessionFingerprint( + verifiedPayload.getFingerprint()); + return verifiedPayload.getPayload(); + }, + MoreExecutors.directExecutor()); if (Config.REQUIRE_RTP_VERIFICATION) { return future; } @@ -1026,11 +1351,14 @@ private ListenableFuture encryptSessionInitiate(final RtpContentM future, CryptoFailedException.class, e -> { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to use OMEMO DTLS verification on outgoing session initiate. falling back", e); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": unable to use OMEMO DTLS verification on outgoing session initiate. falling back", + e); return rtpContentMap; }, - MoreExecutors.directExecutor() - ); + MoreExecutors.directExecutor()); } else { return Futures.immediateFuture(rtpContentMap); } @@ -1047,23 +1375,31 @@ private void sendSessionTerminate(final Reason reason, final String text) { if (previous != State.NULL) { writeLogMessage(target); } - final JinglePacket jinglePacket = new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); + final JinglePacket jinglePacket = + new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); jinglePacket.setReason(reason, text); Log.d(Config.LOGTAG, jinglePacket.toString()); send(jinglePacket); finish(); } - private void sendTransportInfo(final String contentName, IceUdpTransportInfo.Candidate candidate) { + private void sendTransportInfo( + final String contentName, IceUdpTransportInfo.Candidate candidate) { final RtpContentMap transportInfo; try { - final RtpContentMap rtpContentMap = isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap; + final RtpContentMap rtpContentMap = + isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap; transportInfo = rtpContentMap.transportInfo(contentName, candidate); } catch (final Exception e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to prepare transport-info from candidate for content=" + contentName); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": unable to prepare transport-info from candidate for content=" + + contentName); return; } - final JinglePacket jinglePacket = transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); + final JinglePacket jinglePacket = + transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); send(jinglePacket); } @@ -1085,19 +1421,28 @@ private synchronized void handleIqResponse(final Account account, final IqPacket private void handleIqErrorResponse(final IqPacket response) { Preconditions.checkArgument(response.getType() == IqPacket.TYPE.ERROR); final String errorCondition = response.getErrorCondition(); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ-error from " + response.getFrom() + " in RTP session. " + errorCondition); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received IQ-error from " + + response.getFrom() + + " in RTP session. " + + errorCondition); if (isTerminated()) { - Log.i(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring error because session was already terminated"); + Log.i( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": ignoring error because session was already terminated"); return; } this.webRTCWrapper.close(); final State target; if (Arrays.asList( - "service-unavailable", - "recipient-unavailable", - "remote-server-not-found", - "remote-server-timeout" - ).contains(errorCondition)) { + "service-unavailable", + "recipient-unavailable", + "remote-server-not-found", + "remote-server-timeout") + .contains(errorCondition)) { target = State.TERMINATED_CONNECTIVITY_ERROR; } else { target = State.TERMINATED_APPLICATION_FAILURE; @@ -1108,9 +1453,17 @@ private void handleIqErrorResponse(final IqPacket response) { private void handleIqTimeoutResponse(final IqPacket response) { Preconditions.checkArgument(response.getType() == IqPacket.TYPE.TIMEOUT); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ timeout in RTP session with " + id.with + ". terminating with connectivity error"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received IQ timeout in RTP session with " + + id.with + + ". terminating with connectivity error"); if (isTerminated()) { - Log.i(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring error because session was already terminated"); + Log.i( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": ignoring error because session was already terminated"); return; } this.webRTCWrapper.close(); @@ -1119,7 +1472,9 @@ private void handleIqTimeoutResponse(final IqPacket response) { } private void terminateWithOutOfOrder(final JinglePacket jinglePacket) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": terminating session with out-of-order"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": terminating session with out-of-order"); this.webRTCWrapper.close(); transitionOrThrow(State.TERMINATED_APPLICATION_FAILURE); respondWithOutOfOrder(jinglePacket); @@ -1134,19 +1489,18 @@ private void respondWithOutOfOrder(final JinglePacket jinglePacket) { respondWithJingleError(jinglePacket, "out-of-order", "unexpected-request", "wait"); } - void respondWithJingleError(final IqPacket original, String jingleCondition, String condition, String conditionType) { - jingleConnectionManager.respondWithJingleError(id.account, original, jingleCondition, condition, conditionType); + void respondWithJingleError( + final IqPacket original, + String jingleCondition, + String condition, + String conditionType) { + jingleConnectionManager.respondWithJingleError( + id.account, original, jingleCondition, condition, conditionType); } private void respondOk(final JinglePacket jinglePacket) { - xmppConnectionService.sendIqPacket(id.account, jinglePacket.generateResponse(IqPacket.TYPE.RESULT), null); - } - - public void throwStateTransitionException() { - final StateTransitionException exception = this.stateTransitionException; - if (exception != null) { - throw new IllegalStateException(String.format("Transition to %s did not call finish", exception.state), exception); - } + xmppConnectionService.sendIqPacket( + id.account, jinglePacket.generateResponse(IqPacket.TYPE.RESULT), null); } public RtpEndUserState getEndUserState() { @@ -1193,23 +1547,25 @@ public RtpEndUserState getEndUserState() { return RtpEndUserState.RETRACTED; } case TERMINATED_CONNECTIVITY_ERROR: - return zeroDuration() ? RtpEndUserState.CONNECTIVITY_ERROR : RtpEndUserState.CONNECTIVITY_LOST_ERROR; + return zeroDuration() + ? RtpEndUserState.CONNECTIVITY_ERROR + : RtpEndUserState.CONNECTIVITY_LOST_ERROR; case TERMINATED_APPLICATION_FAILURE: return RtpEndUserState.APPLICATION_ERROR; case TERMINATED_SECURITY_ERROR: return RtpEndUserState.SECURITY_ERROR; } - throw new IllegalStateException(String.format("%s has no equivalent EndUserState", this.state)); + throw new IllegalStateException( + String.format("%s has no equivalent EndUserState", this.state)); } - private RtpEndUserState getPeerConnectionStateAsEndUserState() { final PeerConnection.PeerConnectionState state; try { state = webRTCWrapper.getState(); } catch (final WebRTCWrapper.PeerConnectionNotInitialized e) { - //We usually close the WebRTCWrapper *before* transitioning so we might still - //be in SESSION_ACCEPTED even though the peerConnection has been torn down + // We usually close the WebRTCWrapper *before* transitioning so we might still + // be in SESSION_ACCEPTED even though the peerConnection has been torn down return RtpEndUserState.ENDING_CALL; } switch (state) { @@ -1221,7 +1577,9 @@ private RtpEndUserState getPeerConnectionStateAsEndUserState() { case CLOSED: return RtpEndUserState.ENDING_CALL; default: - return zeroDuration() ? RtpEndUserState.CONNECTIVITY_ERROR : RtpEndUserState.RECONNECTING; + return zeroDuration() + ? RtpEndUserState.CONNECTIVITY_ERROR + : RtpEndUserState.RECONNECTING; } } @@ -1230,35 +1588,32 @@ public Set getMedia() { if (current == State.NULL) { if (isInitiator()) { return Preconditions.checkNotNull( - this.proposedMedia, - "RTP connection has not been initialized properly" - ); + this.proposedMedia, "RTP connection has not been initialized properly"); } throw new IllegalStateException("RTP connection has not been initialized yet"); } if (Arrays.asList(State.PROPOSED, State.PROCEED).contains(current)) { return Preconditions.checkNotNull( - this.proposedMedia, - "RTP connection has not been initialized properly" - ); + this.proposedMedia, "RTP connection has not been initialized properly"); } final RtpContentMap initiatorContentMap = initiatorRtpContentMap; if (initiatorContentMap != null) { return initiatorContentMap.getMedia(); } else if (isTerminated()) { - return Collections.emptySet(); //we might fail before we ever got a chance to set media + return Collections.emptySet(); // we might fail before we ever got a chance to set media } else { - return Preconditions.checkNotNull(this.proposedMedia, "RTP connection has not been initialized properly"); + return Preconditions.checkNotNull( + this.proposedMedia, "RTP connection has not been initialized properly"); } } - public boolean isVerified() { final String fingerprint = this.omemoVerification.getFingerprint(); if (fingerprint == null) { return false; } - final FingerprintStatus status = id.account.getAxolotlService().getFingerprintTrust(fingerprint); + final FingerprintStatus status = + id.account.getAxolotlService().getFingerprintTrust(fingerprint); return status != null && status.isVerified(); } @@ -1273,18 +1628,23 @@ public synchronized void acceptCall() { acceptCallFromSessionInitialized(); break; case ACCEPTED: - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": the call has already been accepted with another client. UI was just lagging behind"); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": the call has already been accepted with another client. UI was just lagging behind"); break; case PROCEED: case SESSION_ACCEPTED: - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": the call has already been accepted. user probably double tapped the UI"); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": the call has already been accepted. user probably double tapped the UI"); break; default: throw new IllegalStateException("Can not accept call from " + this.state); } } - public void notifyPhoneCall() { Log.d(Config.LOGTAG, "a phone call has just been started. killing jingle rtp connections"); if (Arrays.asList(State.PROPOSED, State.SESSION_INITIALIZED).contains(this.state)) { @@ -1296,7 +1656,10 @@ public void notifyPhoneCall() { public synchronized void rejectCall() { if (isTerminated()) { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": received rejectCall() when session has already been terminated. nothing to do"); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received rejectCall() when session has already been terminated. nothing to do"); return; } switch (this.state) { @@ -1313,7 +1676,10 @@ public synchronized void rejectCall() { public synchronized void endCall() { if (isTerminated()) { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": received endCall() when session has already been terminated. nothing to do"); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received endCall() when session has already been terminated. nothing to do"); return; } if (isInState(State.PROPOSED) && !isInitiator()) { @@ -1328,7 +1694,8 @@ public synchronized void endCall() { } return; } - if (isInitiator() && isInState(State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED)) { + if (isInitiator() + && isInState(State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED)) { this.webRTCWrapper.close(); sendSessionTerminate(Reason.CANCEL); return; @@ -1342,11 +1709,17 @@ public synchronized void endCall() { sendSessionTerminate(Reason.SUCCESS); return; } - if (isInState(State.TERMINATED_APPLICATION_FAILURE, State.TERMINATED_CONNECTIVITY_ERROR, State.TERMINATED_DECLINED_OR_BUSY)) { - Log.d(Config.LOGTAG, "ignoring request to end call because already in state " + this.state); + if (isInState( + State.TERMINATED_APPLICATION_FAILURE, + State.TERMINATED_CONNECTIVITY_ERROR, + State.TERMINATED_DECLINED_OR_BUSY)) { + Log.d( + Config.LOGTAG, + "ignoring request to end call because already in state " + this.state); return; } - throw new IllegalStateException("called 'endCall' while in state " + this.state + ". isInitiator=" + isInitiator()); + throw new IllegalStateException( + "called 'endCall' while in state " + this.state + ". isInitiator=" + isInitiator()); } private void retractFromProceed() { @@ -1362,7 +1735,9 @@ private void closeTransitionLogFinish(final State state) { finish(); } - private void setupWebRTC(final Set media, final List iceServers) throws WebRTCWrapper.InitializationException { + private void setupWebRTC( + final Set media, final List iceServers) + throws WebRTCWrapper.InitializationException { this.jingleConnectionManager.ensureConnectionIsRegistered(this); final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference; if (media.contains(Media.VIDEO)) { @@ -1406,14 +1781,18 @@ private void sendJingleMessage(final String action) { private void sendJingleMessage(final String action, final Jid to) { final MessagePacket messagePacket = new MessagePacket(); - messagePacket.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those + messagePacket.setType(MessagePacket.TYPE_CHAT); // we want to carbon copy those messagePacket.setTo(to); - final Element intent = messagePacket.addChild(action, Namespace.JINGLE_MESSAGE).setAttribute("id", id.sessionId); + final Element intent = + messagePacket + .addChild(action, Namespace.JINGLE_MESSAGE) + .setAttribute("id", id.sessionId); if ("proceed".equals(action)) { messagePacket.setId(JINGLE_MESSAGE_PROCEED_ID_PREFIX + id.sessionId); if (isOmemoEnabled()) { final int deviceId = id.account.getAxolotlService().getOwnDeviceId(); - final Element device = intent.addChild("device", Namespace.OMEMO_DTLS_SRTP_VERIFICATION); + final Element device = + intent.addChild("device", Namespace.OMEMO_DTLS_SRTP_VERIFICATION); device.setAttribute("id", deviceId); } } @@ -1424,7 +1803,8 @@ private void sendJingleMessage(final String action, final Jid to) { private boolean isOmemoEnabled() { final Conversational conversational = message.getConversation(); if (conversational instanceof Conversation) { - return ((Conversation) conversational).getNextEncryption() == Message.ENCRYPTION_AXOLOTL; + return ((Conversation) conversational).getNextEncryption() + == Message.ENCRYPTION_AXOLOTL; } return false; } @@ -1446,7 +1826,6 @@ private synchronized boolean transition(final State target, final Runnable runna final Collection validTransitions = VALID_TRANSITIONS.get(this.state); if (validTransitions != null && validTransitions.contains(target)) { this.state = target; - this.stateTransitionException = new StateTransitionException(target); if (runnable != null) { runnable.run(); } @@ -1461,26 +1840,31 @@ private synchronized boolean transition(final State target, final Runnable runna void transitionOrThrow(final State target) { if (!transition(target)) { - throw new IllegalStateException(String.format("Unable to transition from %s to %s", this.state, target)); + throw new IllegalStateException( + String.format("Unable to transition from %s to %s", this.state, target)); } } @Override public void onIceCandidate(final IceCandidate iceCandidate) { - final RtpContentMap rtpContentMap = isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap; + final RtpContentMap rtpContentMap = + isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap; final String ufrag = rtpContentMap.getCredentials().ufrag; - final IceUdpTransportInfo.Candidate candidate = IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp, ufrag); + final IceUdpTransportInfo.Candidate candidate = + IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp, ufrag); if (candidate == null) { - Log.d(Config.LOGTAG, "ignoring (not sending) candidate: " + iceCandidate.toString()); + Log.d(Config.LOGTAG, "ignoring (not sending) candidate: " + iceCandidate); return; } - Log.d(Config.LOGTAG, "sending candidate: " + iceCandidate.toString()); + Log.d(Config.LOGTAG, "sending candidate: " + iceCandidate); sendTransportInfo(iceCandidate.sdpMid, candidate); } @Override public void onConnectionChange(final PeerConnection.PeerConnectionState newState) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnectionState changed to " + newState); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": PeerConnectionState changed to " + newState); this.stateHistory.add(newState); if (newState == PeerConnection.PeerConnectionState.CONNECTED) { this.sessionDuration.start(); @@ -1490,12 +1874,17 @@ public void onConnectionChange(final PeerConnection.PeerConnectionState newState updateOngoingCallNotification(); } - final boolean neverConnected = !this.stateHistory.contains(PeerConnection.PeerConnectionState.CONNECTED); + final boolean neverConnected = + !this.stateHistory.contains(PeerConnection.PeerConnectionState.CONNECTED); if (newState == PeerConnection.PeerConnectionState.FAILED) { if (neverConnected) { if (isTerminated()) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": not sending session-terminate after connectivity error because session is already in state " + this.state); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": not sending session-terminate after connectivity error because session is already in state " + + this.state); return; } webRTCWrapper.execute(this::closeWebRTCSessionAfterFailedConnection); @@ -1513,7 +1902,7 @@ public void onRenegotiationNeeded() { } private void initiateIceRestart() { - //TODO discover new TURN/STUN credentials + // TODO discover new TURN/STUN credentials this.stateHistory.clear(); this.webRTCWrapper.setIsReadyToReceiveIceCandidates(false); final SessionDescription sessionDescription; @@ -1527,28 +1916,32 @@ private void initiateIceRestart() { } final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); final RtpContentMap transportInfo = rtpContentMap.transportInfo(); - final JinglePacket jinglePacket = transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); + final JinglePacket jinglePacket = + transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); Log.d(Config.LOGTAG, "initiating ice restart: " + jinglePacket); jinglePacket.setTo(id.with); - xmppConnectionService.sendIqPacket(id.account, jinglePacket, (account, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, "received success to our ice restart"); - setLocalContentMap(rtpContentMap); - webRTCWrapper.setIsReadyToReceiveIceCandidates(true); - return; - } - if (response.getType() == IqPacket.TYPE.ERROR) { - final Element error = response.findChild("error"); - if (error != null && error.hasChild("tie-break", Namespace.JINGLE_ERRORS)) { - Log.d(Config.LOGTAG, "received tie-break as result of ice restart"); - return; - } - handleIqErrorResponse(response); - } - if (response.getType() == IqPacket.TYPE.TIMEOUT) { - handleIqTimeoutResponse(response); - } - }); + xmppConnectionService.sendIqPacket( + id.account, + jinglePacket, + (account, response) -> { + if (response.getType() == IqPacket.TYPE.RESULT) { + Log.d(Config.LOGTAG, "received success to our ice restart"); + setLocalContentMap(rtpContentMap); + webRTCWrapper.setIsReadyToReceiveIceCandidates(true); + return; + } + if (response.getType() == IqPacket.TYPE.ERROR) { + final Element error = response.findChild("error"); + if (error != null && error.hasChild("tie-break", Namespace.JINGLE_ERRORS)) { + Log.d(Config.LOGTAG, "received tie-break as result of ice restart"); + return; + } + handleIqErrorResponse(response); + } + if (response.getType() == IqPacket.TYPE.TIMEOUT) { + handleIqTimeoutResponse(response); + } + }); } private void setLocalContentMap(final RtpContentMap rtpContentMap) { @@ -1567,8 +1960,10 @@ private void setRemoteContentMap(final RtpContentMap rtpContentMap) { } } - private SessionDescription setLocalSessionDescription() throws ExecutionException, InterruptedException { - final org.webrtc.SessionDescription sessionDescription = this.webRTCWrapper.setLocalDescription().get(); + private SessionDescription setLocalSessionDescription() + throws ExecutionException, InterruptedException { + final org.webrtc.SessionDescription sessionDescription = + this.webRTCWrapper.setLocalDescription().get(); return SessionDescription.parse(sessionDescription.description); } @@ -1576,7 +1971,10 @@ private void closeWebRTCSessionAfterFailedConnection() { this.webRTCWrapper.close(); synchronized (this) { if (isTerminated()) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": no need to send session-terminate after failed connection. Other party already did"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": no need to send session-terminate after failed connection. Other party already did"); return; } sendSessionTerminate(Reason.CONNECTIVITY_ERROR); @@ -1624,14 +2022,18 @@ public ListenableFuture switchCamera() { } @Override - public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices) { - xmppConnectionService.notifyJingleRtpConnectionUpdate(selectedAudioDevice, availableAudioDevices); + public void onAudioDeviceChanged( + AppRTCAudioManager.AudioDevice selectedAudioDevice, + Set availableAudioDevices) { + xmppConnectionService.notifyJingleRtpConnectionUpdate( + selectedAudioDevice, availableAudioDevices); } private void updateEndUserState() { final RtpEndUserState endUserState = getEndUserState(); jingleConnectionManager.toneManager.transition(isInitiator(), endUserState, getMedia()); - xmppConnectionService.notifyJingleRtpConnectionUpdate(id.account, id.with, id.sessionId, endUserState); + xmppConnectionService.notifyJingleRtpConnectionUpdate( + id.account, id.with, id.sessionId, endUserState); } private void updateOngoingCallNotification() { @@ -1639,7 +2041,8 @@ private void updateOngoingCallNotification() { if (STATES_SHOWING_ONGOING_CALL.contains(state)) { final boolean reconnecting; if (state == State.SESSION_ACCEPTED) { - reconnecting = getPeerConnectionStateAsEndUserState() == RtpEndUserState.RECONNECTING; + reconnecting = + getPeerConnectionStateAsEndUserState() == RtpEndUserState.RECONNECTING; } else { reconnecting = false; } @@ -1654,58 +2057,102 @@ private void discoverIceServers(final OnIceServersDiscovered onIceServersDiscove final IqPacket request = new IqPacket(IqPacket.TYPE.GET); request.setTo(id.account.getDomain()); request.addChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY); - xmppConnectionService.sendIqPacket(id.account, request, (account, response) -> { - ImmutableList.Builder listBuilder = new ImmutableList.Builder<>(); - if (response.getType() == IqPacket.TYPE.RESULT) { - final Element services = response.findChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY); - final List children = services == null ? Collections.emptyList() : services.getChildren(); - for (final Element child : children) { - if ("service".equals(child.getName())) { - final String type = child.getAttribute("type"); - final String host = child.getAttribute("host"); - final String sport = child.getAttribute("port"); - final Integer port = sport == null ? null : Ints.tryParse(sport); - final String transport = child.getAttribute("transport"); - final String username = child.getAttribute("username"); - final String password = child.getAttribute("password"); - if (Strings.isNullOrEmpty(host) || port == null) { - continue; - } - if (port < 0 || port > 65535) { - continue; - } - if (Arrays.asList("stun", "stuns", "turn", "turns").contains(type) && Arrays.asList("udp", "tcp").contains(transport)) { - if (Arrays.asList("stuns", "turns").contains(type) && "udp".equals(transport)) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": skipping invalid combination of udp/tls in external services"); - continue; + xmppConnectionService.sendIqPacket( + id.account, + request, + (account, response) -> { + ImmutableList.Builder listBuilder = + new ImmutableList.Builder<>(); + if (response.getType() == IqPacket.TYPE.RESULT) { + final Element services = + response.findChild( + "services", Namespace.EXTERNAL_SERVICE_DISCOVERY); + final List children = + services == null + ? Collections.emptyList() + : services.getChildren(); + for (final Element child : children) { + if ("service".equals(child.getName())) { + final String type = child.getAttribute("type"); + final String host = child.getAttribute("host"); + final String sport = child.getAttribute("port"); + final Integer port = + sport == null ? null : Ints.tryParse(sport); + final String transport = child.getAttribute("transport"); + final String username = child.getAttribute("username"); + final String password = child.getAttribute("password"); + if (Strings.isNullOrEmpty(host) || port == null) { + continue; + } + if (port < 0 || port > 65535) { + continue; + } + if (Arrays.asList("stun", "stuns", "turn", "turns") + .contains(type) + && Arrays.asList("udp", "tcp").contains(transport)) { + if (Arrays.asList("stuns", "turns").contains(type) + && "udp".equals(transport)) { + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": skipping invalid combination of udp/tls in external services"); + continue; + } + final PeerConnection.IceServer.Builder iceServerBuilder = + PeerConnection.IceServer.builder( + String.format( + "%s:%s:%s?transport=%s", + type, + IP.wrapIPv6(host), + port, + transport)); + iceServerBuilder.setTlsCertPolicy( + PeerConnection.TlsCertPolicy + .TLS_CERT_POLICY_INSECURE_NO_CHECK); + if (username != null && password != null) { + iceServerBuilder.setUsername(username); + iceServerBuilder.setPassword(password); + } else if (Arrays.asList("turn", "turns").contains(type)) { + // The WebRTC spec requires throwing an + // InvalidAccessError when username (from libwebrtc + // source coder) + // https://chromium.googlesource.com/external/webrtc/+/master/pc/ice_server_parsing.cc + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": skipping " + + type + + "/" + + transport + + " without username and password"); + continue; + } + final PeerConnection.IceServer iceServer = + iceServerBuilder.createIceServer(); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": discovered ICE Server: " + + iceServer); + listBuilder.add(iceServer); + } } - final PeerConnection.IceServer.Builder iceServerBuilder = PeerConnection.IceServer - .builder(String.format("%s:%s:%s?transport=%s", type, IP.wrapIPv6(host), port, transport)); - iceServerBuilder.setTlsCertPolicy(PeerConnection.TlsCertPolicy.TLS_CERT_POLICY_INSECURE_NO_CHECK); - if (username != null && password != null) { - iceServerBuilder.setUsername(username); - iceServerBuilder.setPassword(password); - } else if (Arrays.asList("turn", "turns").contains(type)) { - //The WebRTC spec requires throwing an InvalidAccessError when username (from libwebrtc source coder) - //https://chromium.googlesource.com/external/webrtc/+/master/pc/ice_server_parsing.cc - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": skipping " + type + "/" + transport + " without username and password"); - continue; - } - final PeerConnection.IceServer iceServer = iceServerBuilder.createIceServer(); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": discovered ICE Server: " + iceServer); - listBuilder.add(iceServer); } } - } - } - final List iceServers = listBuilder.build(); - if (iceServers.size() == 0) { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": no ICE server found " + response); - } - onIceServersDiscovered.onIceServersDiscovered(iceServers); - }); + final List iceServers = listBuilder.build(); + if (iceServers.size() == 0) { + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": no ICE server found " + + response); + } + onIceServersDiscovered.onIceServersDiscovered(iceServers); + }); } else { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": has no external service discovery"); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": has no external service discovery"); onIceServersDiscovered.onIceServersDiscovered(Collections.emptyList()); } } @@ -1717,13 +2164,15 @@ private void finish() { this.jingleConnectionManager.setTerminalSessionState(id, getEndUserState(), getMedia()); this.jingleConnectionManager.finishConnectionOrThrow(this); } else { - throw new IllegalStateException(String.format("Unable to call finish from %s", this.state)); + throw new IllegalStateException( + String.format("Unable to call finish from %s", this.state)); } } private void writeLogMessage(final State state) { final long duration = getCallDuration(); - if (state == State.TERMINATED_SUCCESS || (state == State.TERMINATED_CONNECTIVITY_ERROR && duration > 0)) { + if (state == State.TERMINATED_SUCCESS + || (state == State.TERMINATED_CONNECTIVITY_ERROR && duration > 0)) { writeLogMessageSuccess(duration); } else { writeLogMessageMissed(); @@ -1777,18 +2226,11 @@ void setProposedMedia(final Set media) { public void fireStateUpdate() { final RtpEndUserState endUserState = getEndUserState(); - xmppConnectionService.notifyJingleRtpConnectionUpdate(id.account, id.with, id.sessionId, endUserState); + xmppConnectionService.notifyJingleRtpConnectionUpdate( + id.account, id.with, id.sessionId, endUserState); } private interface OnIceServersDiscovered { void onIceServersDiscovered(List iceServers); } - - private static class StateTransitionException extends Exception { - private final State state; - - private StateTransitionException(final State state) { - this.state = state; - } - } } From 372078629b23a39ff3ef1ce2d11081e489af8e5a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 25 Feb 2022 17:26:36 +0100 Subject: [PATCH 062/394] fix ice candidate sending when different credentials are used --- .../xmpp/jingle/JingleRtpConnection.java | 19 ++- .../xmpp/jingle/RtpContentMap.java | 150 ++++++++++++------ .../jingle/stanzas/IceUdpTransportInfo.java | 3 + 3 files changed, 117 insertions(+), 55 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index d834d81a1..fd918fa9b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -33,8 +33,6 @@ import java.util.Map; import java.util.Queue; import java.util.Set; -import java.util.Timer; -import java.util.TimerTask; import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -349,16 +347,16 @@ private void receiveTransportInfo( private boolean checkForIceRestart( final JinglePacket jinglePacket, final RtpContentMap rtpContentMap) { final RtpContentMap existing = getRemoteContentMap(); - final IceUdpTransportInfo.Credentials existingCredentials; + final Set existingCredentials; final IceUdpTransportInfo.Credentials newCredentials; try { existingCredentials = existing.getCredentials(); - newCredentials = rtpContentMap.getCredentials(); + newCredentials = rtpContentMap.getDistinctCredentials(); } catch (final IllegalStateException e) { Log.d(Config.LOGTAG, "unable to gather credentials for comparison", e); return false; } - if (existingCredentials.equals(newCredentials)) { + if (existingCredentials.contains(newCredentials)) { return false; } // TODO an alternative approach is to check if we already got an iq result to our @@ -1849,9 +1847,16 @@ void transitionOrThrow(final State target) { public void onIceCandidate(final IceCandidate iceCandidate) { final RtpContentMap rtpContentMap = isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap; - final String ufrag = rtpContentMap.getCredentials().ufrag; + final IceUdpTransportInfo.Credentials credentials; + try { + credentials = rtpContentMap.getCredentials(iceCandidate.sdpMid); + } catch (final IllegalArgumentException e) { + Log.d(Config.LOGTAG, "ignoring (not sending) candidate: " + iceCandidate, e); + return; + } + final String uFrag = credentials.ufrag; final IceUdpTransportInfo.Candidate candidate = - IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp, ufrag); + IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp, uFrag); if (candidate == null) { Log.d(Config.LOGTAG, "ignoring (not sending) candidate: " + iceCandidate); return; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java index 21684a165..e95a7e36d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -16,7 +16,6 @@ import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import eu.siacs.conversations.xmpp.jingle.stanzas.Content; @@ -39,7 +38,8 @@ public RtpContentMap(Group group, Map contents) { } public static RtpContentMap of(final JinglePacket jinglePacket) { - final Map contents = DescriptionTransport.of(jinglePacket.getJingleContents()); + final Map contents = + DescriptionTransport.of(jinglePacket.getJingleContents()); if (isOmemoVerified(contents)) { return new OmemoVerifiedRtpContentMap(jinglePacket.getGroup(), contents); } else { @@ -62,22 +62,30 @@ private static boolean isOmemoVerified(Map content } public static RtpContentMap of(final SessionDescription sessionDescription) { - final ImmutableMap.Builder contentMapBuilder = new ImmutableMap.Builder<>(); + final ImmutableMap.Builder contentMapBuilder = + new ImmutableMap.Builder<>(); for (SessionDescription.Media media : sessionDescription.media) { final String id = Iterables.getFirst(media.attributes.get("mid"), null); Preconditions.checkNotNull(id, "media has no mid"); contentMapBuilder.put(id, DescriptionTransport.of(sessionDescription, media)); } - final String groupAttribute = Iterables.getFirst(sessionDescription.attributes.get("group"), null); + final String groupAttribute = + Iterables.getFirst(sessionDescription.attributes.get("group"), null); final Group group = groupAttribute == null ? null : Group.ofSdpString(groupAttribute); return new RtpContentMap(group, contentMapBuilder.build()); } public Set getMedia() { - return Sets.newHashSet(Collections2.transform(contents.values(), input -> { - final RtpDescription rtpDescription = input == null ? null : input.description; - return rtpDescription == null ? Media.UNKNOWN : input.description.getMedia(); - })); + return Sets.newHashSet( + Collections2.transform( + contents.values(), + input -> { + final RtpDescription rtpDescription = + input == null ? null : input.description; + return rtpDescription == null + ? Media.UNKNOWN + : input.description.getMedia(); + })); } public List getNames() { @@ -90,7 +98,8 @@ void requireContentDescriptions() { } for (Map.Entry entry : this.contents.entrySet()) { if (entry.getValue().description == null) { - throw new IllegalStateException(String.format("%s is lacking content description", entry.getKey())); + throw new IllegalStateException( + String.format("%s is lacking content description", entry.getKey())); } } } @@ -106,15 +115,24 @@ void requireDTLSFingerprint(final boolean requireActPass) { for (Map.Entry entry : this.contents.entrySet()) { final IceUdpTransportInfo transport = entry.getValue().transport; final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint(); - if (fingerprint == null || Strings.isNullOrEmpty(fingerprint.getContent()) || Strings.isNullOrEmpty(fingerprint.getHash())) { - throw new SecurityException(String.format("Use of DTLS-SRTP (XEP-0320) is required for content %s", entry.getKey())); + if (fingerprint == null + || Strings.isNullOrEmpty(fingerprint.getContent()) + || Strings.isNullOrEmpty(fingerprint.getHash())) { + throw new SecurityException( + String.format( + "Use of DTLS-SRTP (XEP-0320) is required for content %s", + entry.getKey())); } final IceUdpTransportInfo.Setup setup = fingerprint.getSetup(); if (setup == null) { - throw new SecurityException(String.format("Use of DTLS-SRTP (XEP-0320) is required for content %s but missing setup attribute", entry.getKey())); + throw new SecurityException( + String.format( + "Use of DTLS-SRTP (XEP-0320) is required for content %s but missing setup attribute", + entry.getKey())); } if (requireActPass && setup != IceUdpTransportInfo.Setup.ACTPASS) { - throw new SecurityException("Initiator needs to offer ACTPASS as setup for DTLS-SRTP (XEP-0320)"); + throw new SecurityException( + "Initiator needs to offer ACTPASS as setup for DTLS-SRTP (XEP-0320)"); } } } @@ -135,41 +153,66 @@ JinglePacket toJinglePacket(final JinglePacket.Action action, final String sessi return jinglePacket; } - RtpContentMap transportInfo(final String contentName, final IceUdpTransportInfo.Candidate candidate) { + RtpContentMap transportInfo( + final String contentName, final IceUdpTransportInfo.Candidate candidate) { final RtpContentMap.DescriptionTransport descriptionTransport = contents.get(contentName); - final IceUdpTransportInfo transportInfo = descriptionTransport == null ? null : descriptionTransport.transport; + final IceUdpTransportInfo transportInfo = + descriptionTransport == null ? null : descriptionTransport.transport; if (transportInfo == null) { - throw new IllegalArgumentException("Unable to find transport info for content name " + contentName); + throw new IllegalArgumentException( + "Unable to find transport info for content name " + contentName); } final IceUdpTransportInfo newTransportInfo = transportInfo.cloneWrapper(); newTransportInfo.addChild(candidate); - return new RtpContentMap(null, ImmutableMap.of(contentName, new DescriptionTransport(null, newTransportInfo))); + return new RtpContentMap( + null, + ImmutableMap.of(contentName, new DescriptionTransport(null, newTransportInfo))); } RtpContentMap transportInfo() { return new RtpContentMap( null, - Maps.transformValues(contents, dt -> new DescriptionTransport(null, dt.transport.cloneWrapper())) - ); + Maps.transformValues( + contents, + dt -> new DescriptionTransport(null, dt.transport.cloneWrapper()))); } - public IceUdpTransportInfo.Credentials getCredentials() { - final Set allCredentials = ImmutableSet.copyOf(Collections2.transform( - contents.values(), - dt -> dt.transport.getCredentials() - )); - final IceUdpTransportInfo.Credentials credentials = Iterables.getFirst(allCredentials, null); + public IceUdpTransportInfo.Credentials getDistinctCredentials() { + final Set allCredentials = getCredentials(); + final IceUdpTransportInfo.Credentials credentials = + Iterables.getFirst(allCredentials, null); if (allCredentials.size() == 1 && credentials != null) { return credentials; } throw new IllegalStateException("Content map does not have distinct credentials"); } + public Set getCredentials() { + final Set credentials = + ImmutableSet.copyOf( + Collections2.transform( + contents.values(), dt -> dt.transport.getCredentials())); + if (credentials.isEmpty()) { + throw new IllegalStateException("Content map does not have any credentials"); + } + return credentials; + } + + public IceUdpTransportInfo.Credentials getCredentials(final String contentName) { + final DescriptionTransport descriptionTransport = this.contents.get(contentName); + if (descriptionTransport == null) { + throw new IllegalArgumentException( + String.format( + "Unable to find transport info for content name %s", contentName)); + } + return descriptionTransport.transport.getCredentials(); + } + public IceUdpTransportInfo.Setup getDtlsSetup() { - final Set setups = ImmutableSet.copyOf(Collections2.transform( - contents.values(), - dt -> dt.transport.getFingerprint().getSetup() - )); + final Set setups = + ImmutableSet.copyOf( + Collections2.transform( + contents.values(), dt -> dt.transport.getFingerprint().getSetup())); final IceUdpTransportInfo.Setup setup = Iterables.getFirst(setups, null); if (setups.size() == 1 && setup != null) { return setup; @@ -185,13 +228,18 @@ public boolean emptyCandidates() { return count == 0; } - public RtpContentMap modifiedCredentials(IceUdpTransportInfo.Credentials credentials, final IceUdpTransportInfo.Setup setup) { - final ImmutableMap.Builder contentMapBuilder = new ImmutableMap.Builder<>(); + public RtpContentMap modifiedCredentials( + IceUdpTransportInfo.Credentials credentials, final IceUdpTransportInfo.Setup setup) { + final ImmutableMap.Builder contentMapBuilder = + new ImmutableMap.Builder<>(); for (final Map.Entry content : contents.entrySet()) { final RtpDescription rtpDescription = content.getValue().description; IceUdpTransportInfo transportInfo = content.getValue().transport; - final IceUdpTransportInfo modifiedTransportInfo = transportInfo.modifyCredentials(credentials, setup); - contentMapBuilder.put(content.getKey(), new DescriptionTransport(rtpDescription, modifiedTransportInfo)); + final IceUdpTransportInfo modifiedTransportInfo = + transportInfo.modifyCredentials(credentials, setup); + contentMapBuilder.put( + content.getKey(), + new DescriptionTransport(rtpDescription, modifiedTransportInfo)); } return new RtpContentMap(this.group, contentMapBuilder.build()); } @@ -200,7 +248,8 @@ public static class DescriptionTransport { public final RtpDescription description; public final IceUdpTransportInfo transport; - public DescriptionTransport(final RtpDescription description, final IceUdpTransportInfo transport) { + public DescriptionTransport( + final RtpDescription description, final IceUdpTransportInfo transport) { this.description = description; this.transport = transport; } @@ -215,33 +264,38 @@ public static DescriptionTransport of(final Content content) { } else if (description instanceof RtpDescription) { rtpDescription = (RtpDescription) description; } else { - throw new UnsupportedApplicationException("Content does not contain rtp description"); + throw new UnsupportedApplicationException( + "Content does not contain rtp description"); } if (transportInfo instanceof IceUdpTransportInfo) { iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo; } else { - throw new UnsupportedTransportException("Content does not contain ICE-UDP transport"); + throw new UnsupportedTransportException( + "Content does not contain ICE-UDP transport"); } return new DescriptionTransport( - rtpDescription, - OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo) - ); + rtpDescription, OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo)); } - public static DescriptionTransport of(final SessionDescription sessionDescription, final SessionDescription.Media media) { + public static DescriptionTransport of( + final SessionDescription sessionDescription, final SessionDescription.Media media) { final RtpDescription rtpDescription = RtpDescription.of(sessionDescription, media); - final IceUdpTransportInfo transportInfo = IceUdpTransportInfo.of(sessionDescription, media); + final IceUdpTransportInfo transportInfo = + IceUdpTransportInfo.of(sessionDescription, media); return new DescriptionTransport(rtpDescription, transportInfo); } public static Map of(final Map contents) { - return ImmutableMap.copyOf(Maps.transformValues(contents, new Function() { - @NullableDecl - @Override - public DescriptionTransport apply(@NullableDecl Content content) { - return content == null ? null : of(content); - } - })); + return ImmutableMap.copyOf( + Maps.transformValues( + contents, + new Function() { + @NullableDecl + @Override + public DescriptionTransport apply(@NullableDecl Content content) { + return content == null ? null : of(content); + } + })); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java index 45260cafb..ee8d12b70 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; +import androidx.annotation.NonNull; + import com.google.common.base.Joiner; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; @@ -123,6 +125,7 @@ public int hashCode() { } @Override + @NonNull public String toString() { return MoreObjects.toStringHelper(this) .add("ufrag", ufrag) From 6d7058398ea529f0e16ef84ba415c61de7ff2973 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 7 Mar 2022 08:33:10 +0100 Subject: [PATCH 063/394] add changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2bb0a938..b7ef6c6fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +### Version 2.10.3 + +* Store files in location appropriate for Android 11 +* Attempt to reconnect call after network switch + ### Version 2.10.2 * Fix crash when rendering some quotes From 4a5e27130c000f8dbcdfde7e8b6ba6416cab039b Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 7 Mar 2022 08:42:40 +0100 Subject: [PATCH 064/394] pulled translations from transifex --- src/conversations/res/values-gl/strings.xml | 8 ++++---- src/main/res/values-bg/strings.xml | 3 +-- src/main/res/values-da-rDK/strings.xml | 3 +-- src/main/res/values-de/strings.xml | 3 +-- src/main/res/values-es/strings.xml | 3 +-- src/main/res/values-fi/strings.xml | 3 +-- src/main/res/values-gl/strings.xml | 5 ++--- src/main/res/values-it/strings.xml | 3 +-- src/main/res/values-ja/strings.xml | 3 +-- src/main/res/values-pl/strings.xml | 5 ++--- src/main/res/values-pt-rBR/strings.xml | 3 +-- src/main/res/values-ro-rRO/strings.xml | 3 +-- src/main/res/values-ru/strings.xml | 3 +-- src/main/res/values-sv/strings.xml | 3 +++ src/main/res/values-tr-rTR/strings.xml | 3 +-- src/main/res/values-vi/strings.xml | 3 +-- src/main/res/values-zh-rCN/strings.xml | 3 +-- 17 files changed, 24 insertions(+), 36 deletions(-) diff --git a/src/conversations/res/values-gl/strings.xml b/src/conversations/res/values-gl/strings.xml index 636f921b6..98d151721 100644 --- a/src/conversations/res/values-gl/strings.xml +++ b/src/conversations/res/values-gl/strings.xml @@ -1,15 +1,15 @@ - Escolle o teu provedor XMPP + Elixe o teu provedor XMPP Utilizar conversations.im Crear nova conta Xa posúes unha conta XMPP? Este pode ser o caso se xa estás a utilizar outro cliente XMPP ou utilizaches Conversations previamente. Se non é así podes crear unha nova conta agora mesmo.\nTruco: Algúns provedores de correo tamén proporcionan contas XMPP. XMPP é unha rede de mensaxería independente do provedor. Podes utilizar este cliente con calquera provedor XMPP da túa elección.\nMais para a tua conveniencia fixemos que fose doado crear unha conta en conversations.im¹; un provedor especialmente axeitado para utilizar con Conversations. - Convidáronte a %1$s. Guiarémoste no proceso para crear unha conta.\nAo escoller %1$s como provedor poderás comunicarte con usuarias de outros provedores cando lles deas o teu enderezo XMPP completo. - Convidáronte a %1$s. Escollemos un nome de usuaria por ti. Guiarémoste no proceso de crear unha conta.\nPoderás comunicarte con usuarias de outros provedores cando lles digas o teu enderezo XMPP completo. + Convidáronte a %1$s. Guiarémoste no proceso para crear unha conta.\nAo elexir %1$s como provedor poderás comunicarte con usuarias doutros provedores cando lles deas o teu enderezo XMPP completo. + Convidáronte a %1$s. Xa eleximos un nome de usuaria para ti. Guiarémoste no proceso de crear unha conta.\nPoderás comunicarte con usuarias doutros provedores cando lles digas o teu enderezo XMPP completo. O convite do teu servidor Código de aprovisionamento con formato non válido - Toca no botón compartir para convidar ó teu contacto a %1$s. + Toca no botón compartir para convidar ao teu contacto a %1$s. Se o contacto está preto de ti, pode escanear o código inferior para aceptar o teu convite. Únete a %1$s e conversa conmigo: %2$s Enviar convite a... diff --git a/src/main/res/values-bg/strings.xml b/src/main/res/values-bg/strings.xml index ee4687dfa..d4603cc3c 100644 --- a/src/main/res/values-bg/strings.xml +++ b/src/main/res/values-bg/strings.xml @@ -965,5 +965,4 @@ Създаването на резервно копие е стартирано. Ще получите известие, когато приключи. Видеото не може да бъде включено. Обикновен текстов документ - - + diff --git a/src/main/res/values-da-rDK/strings.xml b/src/main/res/values-da-rDK/strings.xml index 8c06e3f50..22f4bce2d 100644 --- a/src/main/res/values-da-rDK/strings.xml +++ b/src/main/res/values-da-rDK/strings.xml @@ -968,5 +968,4 @@ Sikkerhedskopieringen er startet. Du får en notifikation, når den er afsluttet. Kunne ikke aktivere video. Ren tekstdokument - - + diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 787025a67..7d8f45319 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -968,5 +968,4 @@ Die Sicherung wurde gestartet. Du bekommst eine Benachrichtigung, sobald sie fertig ist. Video kann nicht aktiviert werden. Textdokument - - + diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index 535209db8..c958b30b2 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -968,5 +968,4 @@ La copia de seguridad ha empezado. Recibirás una notificación cuando se haya completado. No se ha podido habilitar el vídeo. Documento de texto plano - - + diff --git a/src/main/res/values-fi/strings.xml b/src/main/res/values-fi/strings.xml index 3fa9ad89f..9a03e5808 100644 --- a/src/main/res/values-fi/strings.xml +++ b/src/main/res/values-fi/strings.xml @@ -914,5 +914,4 @@ Varmuuskopion teko aloitettu. Saat ilmoituksen kun se on valmis. Videon käyttöönotto epäonnistui Perustekstiasiakirja - - + diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index fb320cfaa..be14eb100 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -479,7 +479,7 @@ Axustes ampliados de conexión Mostar axustes de servidor e porto cando se configura unha conta xmpp.exemplo.com - Conéctate con certificado + Accede con certificado Non se puido procesar o certificado Gardando axustes Axustes de gardado no servidor @@ -968,5 +968,4 @@ Comezou a creación da copia de apoio. Recibirás unha notificación cando remate. Non se puido activar o vídeo. Documento de texto plano - - + diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index 3a760a44e..07a619dff 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -968,5 +968,4 @@ Il backup è iniziato. Riceverai una notifica una volta completato. Impossibile attivare il video. Documento di testo - - + diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml index 9963796ae..f94eb51e4 100644 --- a/src/main/res/values-ja/strings.xml +++ b/src/main/res/values-ja/strings.xml @@ -950,5 +950,4 @@ バックアップを開始しました。 バックアップが完了すると通知が届きます。 映像を有効化できません。 プレーンテキスト文書 - - + diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 7325816ed..2cba52246 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -431,7 +431,7 @@ Oferowanie %s Ukryj niedostępnych %s pisze... - %s przestał(a) pisać + %s już nie pisze %s piszą... %s przestali pisać Powiadomienia pisania @@ -995,5 +995,4 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Tworzenie kopii zapasowej się rozpoczęło. Dostaniesz powiadomienie kiedy się zakończy. Nie można włączyć wideo. Dokument zwykłego tekstu - - + diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index 5e15e47c1..9efea8ca8 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -968,5 +968,4 @@ O backup foi iniciado. Você receberá uma notificação assim que ele for concluído. Não foi possível habilitar o vídeo. Documento em texto puro - - + diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index d3ddbe6ca..93b11c506 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -981,5 +981,4 @@ Se creează copia de siguranță. Veți primi o notificare când acesta este completă. Nu s-a putut activa camera video. Document text - - + diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index 2c2f413d0..d9d728049 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -993,5 +993,4 @@ Резервное копирование было начато. Вы получите уведомление, как только оно будет завершено. Невозможно включить видео. Текстовые данные - - + diff --git a/src/main/res/values-sv/strings.xml b/src/main/res/values-sv/strings.xml index 138586237..d73433c36 100644 --- a/src/main/res/values-sv/strings.xml +++ b/src/main/res/values-sv/strings.xml @@ -537,6 +537,7 @@ Säkerhetsfel: Ogiltig filåtkomst! Ingen applikation hittades för att dela URI Dela URI med... + Acceptera och gå vidare Din fullständiga XMPP-adress kommer att vara: %s Skapa konto Använd min egen leverantör @@ -662,7 +663,9 @@ Igår Bekräfta värdnamn med DNSSEC Certifikatet innehåller ej en XMPP-adress + delvis Spela in video + Kopiera till urklipp Meddelande kopierat till urklipp Meddelande Godkänn okänt certifikat? diff --git a/src/main/res/values-tr-rTR/strings.xml b/src/main/res/values-tr-rTR/strings.xml index 72094ced9..f3e135b6b 100644 --- a/src/main/res/values-tr-rTR/strings.xml +++ b/src/main/res/values-tr-rTR/strings.xml @@ -968,5 +968,4 @@ Yedekleme başlatıldı. Tamamlandığı zaman bir bildirim alacaksınız. Video etkinleştirilemedi Düz metin dosyası - - + diff --git a/src/main/res/values-vi/strings.xml b/src/main/res/values-vi/strings.xml index 9fef998b0..76eb33c37 100644 --- a/src/main/res/values-vi/strings.xml +++ b/src/main/res/values-vi/strings.xml @@ -955,5 +955,4 @@ Việc sao lưu đã được bắt đầu. Bạn sẽ nhận một thông báo khi việc đó đã hoàn tất. Không thể bật video. Tài liệu văn bản thuần - - + diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index b3dcf959d..6323cea4d 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -955,5 +955,4 @@ 已启动备份。一旦完成,你会收到通知。 无法启用视频 纯文本文档 - - + From 882e7319edfcf848a3a622de99d2ae799d2da32c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 7 Mar 2022 09:16:13 +0100 Subject: [PATCH 065/394] do not build emoji flavors --- .github/workflows/android.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index badbbf5d7..e25e1be7a 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -22,14 +22,10 @@ jobs: run: mkdir libs && wget -O libs/libwebrtc-m92.aar https://gultsch.de/files/libwebrtc-m92.aar - name: Grant execute permission for gradlew run: chmod +x gradlew - - name: Build Quicksy (Compat) - run: ./gradlew assembleQuicksyFreeCompatDebug - - name: Build Quicksy (System) - run: ./gradlew assembleQuicksyFreeSystemDebug - - name: Build Conversations (Compat) - run: ./gradlew assembleConversationsFreeCompatDebug - - name: Build Conversations (System) - run: ./gradlew assembleConversationsFreeSystemDebug + - name: Build Quicksy + run: ./gradlew assembleQuicksyFreeDebug + - name: Build Conversations + run: ./gradlew assembleConversationsFreeDebug - uses: actions/upload-artifact@v2 with: name: Conversations all-flavors (debug) From eb6ae5b03c500a85282a2690c8008e51d1771435 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 7 Mar 2022 09:18:35 +0100 Subject: [PATCH 066/394] increase default pw length --- src/main/java/eu/siacs/conversations/utils/CryptoHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java index 4ee826c3c..a92d48825 100644 --- a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java @@ -36,7 +36,7 @@ public final class CryptoHelper { public static final Pattern UUID_PATTERN = Pattern.compile("[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}"); final public static byte[] ONE = new byte[]{0, 0, 0, 1}; private static final char[] CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz123456789+-/#$!?".toCharArray(); - private static final int PW_LENGTH = 10; + private static final int PW_LENGTH = 12; private static final char[] VOWELS = "aeiou".toCharArray(); private static final char[] CONSONANTS = "bcfghjklmnpqrstvwxyz".toCharArray(); private final static char[] hexArray = "0123456789abcdef".toCharArray(); From aef5292567099e683e9342eca9507d6c43ae3d0b Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 16 Feb 2022 17:19:00 +0100 Subject: [PATCH 067/394] Add handling of status code 333 This is used when something goes wrong with a MUC, e.g. a connection error made the MUC kick you out. In this case you generally want to try to rejoin. --- src/main/java/eu/siacs/conversations/entities/MucOptions.java | 1 + .../java/eu/siacs/conversations/parser/PresenceParser.java | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index d1f6525ce..34f437e1c 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -34,6 +34,7 @@ public class MucOptions { public static final String STATUS_CODE_AFFILIATION_CHANGE = "321"; public static final String STATUS_CODE_LOST_MEMBERSHIP = "322"; public static final String STATUS_CODE_SHUTDOWN = "332"; + public static final String STATUS_CODE_TECHNICAL_REASONS = "333"; private final Set users = new HashSet<>(); private final Conversation conversation; public OnRenameListener onRenameListener = null; diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index 9b3e38b3a..341e8d9a0 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -143,7 +143,9 @@ private void processConferencePresence(PresencePacket packet, Conversation conve } else if (codes.contains(MucOptions.STATUS_CODE_SHUTDOWN) && fullJidMatches) { mucOptions.setError(MucOptions.Error.SHUTDOWN); } else if (codes.contains(MucOptions.STATUS_CODE_SELF_PRESENCE)) { - if (codes.contains(MucOptions.STATUS_CODE_KICKED)) { + if (codes.contains(MucOptions.STATUS_CODE_TECHNICAL_REASONS)) { + mucOptions.setError(MucOptions.Error.UNKNOWN); + } else if (codes.contains(MucOptions.STATUS_CODE_KICKED)) { mucOptions.setError(MucOptions.Error.KICKED); } else if (codes.contains(MucOptions.STATUS_CODE_BANNED)) { mucOptions.setError(MucOptions.Error.BANNED); From f95ed284b51aef46bd9a490b31315ea6fcedae1d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 7 Mar 2022 09:21:52 +0100 Subject: [PATCH 068/394] bump copyright year --- src/main/res/values/about.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values/about.xml b/src/main/res/values/about.xml index 5953d543d..dd5d32a15 100644 --- a/src/main/res/values/about.xml +++ b/src/main/res/values/about.xml @@ -31,7 +31,7 @@ Conversations • the very last word in instant messaging. - \n\nCopyright © 2014-2021 Daniel Gultsch + \n\nCopyright © 2014-2022 Daniel Gultsch \n\nThis program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or From 2f07fccfce6bc961a17050b8fcb81912d3921c93 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 7 Mar 2022 12:43:55 +0100 Subject: [PATCH 069/394] show contact jid in call screen closes #4071 --- .../conversations/ui/RtpSessionActivity.java | 19 ++++++++++++++----- src/main/res/layout/activity_rtp_session.xml | 14 +++++++++++++- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 6320ef5b7..323a2f67b 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -400,7 +400,7 @@ public void onNewIntent(final Intent intent) { } } else if (asList(ACTION_MAKE_VIDEO_CALL, ACTION_MAKE_VOICE_CALL).contains(action)) { proposeJingleRtpSession(account, with, actionToMedia(action)); - binding.with.setText(account.getRoster().getContact(with).getDisplayName()); + setWith(account.getRoster().getContact(with)); } else { throw new IllegalStateException("received onNewIntent without sessionId"); } @@ -424,7 +424,7 @@ void onBackendConnected() { } } else if (asList(ACTION_MAKE_VIDEO_CALL, ACTION_MAKE_VOICE_CALL).contains(action)) { proposeJingleRtpSession(account, with, actionToMedia(action)); - binding.with.setText(account.getRoster().getContact(with).getDisplayName()); + setWith(account.getRoster().getContact(with)); } else if (Intent.ACTION_VIEW.equals(action)) { final String extraLastState = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE); final RtpEndUserState state = @@ -437,7 +437,7 @@ void onBackendConnected() { updateProfilePicture(state); invalidateOptionsMenu(); } - binding.with.setText(account.getRoster().getContact(with).getDisplayName()); + setWith(account.getRoster().getContact(with)); if (xmppConnectionService .getJingleConnectionManager() .fireJingleRtpConnectionStateUpdates()) { @@ -454,6 +454,15 @@ void onBackendConnected() { } } + private void setWidth() { + setWith(getWith()); + } + + private void setWith(final Contact contact) { + binding.with.setText(contact.getDisplayName()); + binding.withJid.setText(contact.getJid().asBareJid().toEscapedString()); + } + private void proposeJingleRtpSession( final Account account, final Jid with, final Set media) { checkMicrophoneAvailabilityAsync(); @@ -657,7 +666,7 @@ private boolean initializeActivityWithRunningRtpSession( requireRtpConnection().getState())) { putScreenInCallMode(); } - binding.with.setText(getWith().getDisplayName()); + setWidth(); updateVideoViews(currentState); updateStateDisplay(currentState, media); updateVerifiedShield(verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(currentState)); @@ -684,7 +693,7 @@ private void initializeWithTerminatedSessionState( updateCallDuration(); updateVerifiedShield(false); invalidateOptionsMenu(); - binding.with.setText(account.getRoster().getContact(with).getDisplayName()); + setWith(account.getRoster().getContact(with)); } private void reInitializeActivityWithRunningRtpSession( diff --git a/src/main/res/layout/activity_rtp_session.xml b/src/main/res/layout/activity_rtp_session.xml index 0bdca4776..73bfee30e 100644 --- a/src/main/res/layout/activity_rtp_session.xml +++ b/src/main/res/layout/activity_rtp_session.xml @@ -55,10 +55,22 @@ android:layout_marginLeft="16dp" android:layout_marginTop="0dp" android:layout_marginRight="16dp" - android:layout_marginBottom="32dp" + android:layout_marginBottom="8dp" android:textAppearance="@style/TextAppearance.Conversations.Display2" android:textColor="@color/white" tools:text="Juliet Capulet" /> + + From ceceead5051834ae02c30ca61c480ed5c3792c88 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 7 Mar 2022 13:10:57 +0100 Subject: [PATCH 070/394] =?UTF-8?q?show=20'using=20account=20=E2=80=A6'=20?= =?UTF-8?q?in=20incoming=20call=20screen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../conversations/ui/RtpSessionActivity.java | 23 ++++++++++++------- src/main/res/layout/activity_rtp_session.xml | 10 ++++++++ 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 323a2f67b..842811edb 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -434,7 +434,7 @@ void onBackendConnected() { updateButtonConfiguration(state); updateVerifiedShield(false); updateStateDisplay(state); - updateProfilePicture(state); + updateIncomingCallScreen(state); invalidateOptionsMenu(); } setWith(account.getRoster().getContact(with)); @@ -671,7 +671,7 @@ private boolean initializeActivityWithRunningRtpSession( updateStateDisplay(currentState, media); updateVerifiedShield(verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(currentState)); updateButtonConfiguration(currentState, media); - updateProfilePicture(currentState); + updateIncomingCallScreen(currentState); invalidateOptionsMenu(); return false; } @@ -689,7 +689,7 @@ private void initializeWithTerminatedSessionState( resetIntent(account, with, terminatedRtpSession.state, terminatedRtpSession.media); updateButtonConfiguration(state); updateStateDisplay(state); - updateProfilePicture(state); + updateIncomingCallScreen(state); updateCallDuration(); updateVerifiedShield(false); invalidateOptionsMenu(); @@ -790,11 +790,11 @@ private void updateVerifiedShield(final boolean verified) { this.binding.verified.setVisibility(verified ? View.VISIBLE : View.GONE); } - private void updateProfilePicture(final RtpEndUserState state) { - updateProfilePicture(state, null); + private void updateIncomingCallScreen(final RtpEndUserState state) { + updateIncomingCallScreen(state, null); } - private void updateProfilePicture(final RtpEndUserState state, final Contact contact) { + private void updateIncomingCallScreen(final RtpEndUserState state, final Contact contact) { if (state == RtpEndUserState.INCOMING_CALL || state == RtpEndUserState.ACCEPTING_CALL) { final boolean show = getResources().getBoolean(R.bool.show_avatar_incoming_call); if (show) { @@ -809,7 +809,14 @@ private void updateProfilePicture(final RtpEndUserState state, final Contact con } else { binding.contactPhoto.setVisibility(View.GONE); } + final Account account = contact == null ? getWith().getAccount() : contact.getAccount(); + binding.usingAccount.setVisibility(View.VISIBLE); + binding.usingAccount.setText( + getString( + R.string.using_account, + account.getJid().asBareJid().toEscapedString())); } else { + binding.usingAccount.setVisibility(View.GONE); binding.contactPhoto.setVisibility(View.GONE); } } @@ -1253,7 +1260,7 @@ public void onJingleRtpConnectionUpdate( verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(state)); updateButtonConfiguration(state, media); updateVideoViews(state); - updateProfilePicture(state, contact); + updateIncomingCallScreen(state, contact); invalidateOptionsMenu(); }); if (END_CARD.contains(state)) { @@ -1314,7 +1321,7 @@ private void updateRtpSessionProposalState( updateVerifiedShield(false); updateStateDisplay(state); updateButtonConfiguration(state); - updateProfilePicture(state); + updateIncomingCallScreen(state); invalidateOptionsMenu(); }); resetIntent(account, with, state, actionToMedia(currentIntent.getAction())); diff --git a/src/main/res/layout/activity_rtp_session.xml b/src/main/res/layout/activity_rtp_session.xml index 73bfee30e..4563d9a97 100644 --- a/src/main/res/layout/activity_rtp_session.xml +++ b/src/main/res/layout/activity_rtp_session.xml @@ -247,5 +247,15 @@ app:tint="?attr/icon_tint" /> + + From 56f01c29b9560879eb84ff6b2848fefda86a8cbe Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 10 Mar 2022 12:39:43 +0100 Subject: [PATCH 071/394] allow deletion of all files --- .../eu/siacs/conversations/persistance/FileBackend.java | 6 +++++- .../eu/siacs/conversations/ui/ConversationFragment.java | 7 ++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 5c23bf0fa..55b324533 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -362,8 +362,12 @@ public static boolean weOwnFile(final Uri uri) { } private static boolean weOwnFileLollipop(final Uri uri) { + final String path = uri.getPath(); + if (path == null) { + return false; + } try { - File file = new File(uri.getPath()); + File file = new File(path); FileDescriptor fd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY) .getFileDescriptor(); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 073e77cc3..7420ca50a 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1184,11 +1184,8 @@ private void populateContextMenu(ContextMenu menu) { cancelTransmission.setVisible(true); } if (m.isFileOrImage() && !deleted && !cancelable) { - final String path = m.getRelativeFilePath(); - if (path == null || !path.startsWith("/")) { - deleteFile.setVisible(true); - deleteFile.setTitle(activity.getString(R.string.delete_x_file, UIHelper.getFileDescriptionString(activity, m))); - } + deleteFile.setVisible(true); + deleteFile.setTitle(activity.getString(R.string.delete_x_file, UIHelper.getFileDescriptionString(activity, m))); } if (showError) { showErrorMessage.setVisible(true); From b1ec3a0e29c1f8adf928e7f025de492f57ce36ab Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 10 Mar 2022 15:53:51 +0100 Subject: [PATCH 072/394] use libwebrtc m99 --- .github/workflows/android.yml | 2 +- build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index e25e1be7a..0b737c568 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -19,7 +19,7 @@ jobs: java-version: '11' distribution: 'adopt' - name: Download WebRTC - run: mkdir libs && wget -O libs/libwebrtc-m92.aar https://gultsch.de/files/libwebrtc-m92.aar + run: mkdir libs && wget -O libs/libwebrtc-m99.aar https://gultsch.de/files/libwebrtc-m99.aar - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build Quicksy diff --git a/build.gradle b/build.gradle index 54186f6d2..61b32120f 100644 --- a/build.gradle +++ b/build.gradle @@ -75,7 +75,7 @@ dependencies { implementation 'com.google.guava:guava:30.1.1-android' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.36' - implementation fileTree(include: ['libwebrtc-m92.aar'], dir: 'libs') + implementation fileTree(include: ['libwebrtc-m99.aar'], dir: 'libs') } ext { From 5c4eccec13508a8687452adc96f0799813b6f3c3 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 10 Mar 2022 15:54:23 +0100 Subject: [PATCH 073/394] be smarter about what files can be deleted --- .../persistance/FileBackend.java | 48 +++++++++++++++---- .../ui/ConversationFragment.java | 7 ++- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 55b324533..404f521f4 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -33,6 +33,7 @@ import androidx.exifinterface.media.ExifInterface; import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; import com.google.common.io.ByteStreams; import java.io.ByteArrayOutputStream; @@ -83,21 +84,38 @@ public class FileBackend { private static final float IGNORE_PADDING = 0.15f; private final XmppConnectionService mXmppConnectionService; + private static final List STORAGE_TYPES; + + static { + final ImmutableList.Builder builder = + new ImmutableList.Builder() + .add( + Environment.DIRECTORY_DOWNLOADS, + Environment.DIRECTORY_PICTURES, + Environment.DIRECTORY_MOVIES, + Environment.DIRECTORY_DOCUMENTS); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + builder.add(Environment.DIRECTORY_RECORDINGS); + } + STORAGE_TYPES = builder.build(); + } + public FileBackend(XmppConnectionService service) { this.mXmppConnectionService = service; } public static long getFileSize(Context context, Uri uri) { - try { - final Cursor cursor = context.getContentResolver().query(uri, null, null, null, null); + try (final Cursor cursor = + context.getContentResolver().query(uri, null, null, null, null)) { if (cursor != null && cursor.moveToFirst()) { - long size = cursor.getLong(cursor.getColumnIndex(OpenableColumns.SIZE)); - cursor.close(); - return size; - } else { - return -1; + final int index = cursor.getColumnIndex(OpenableColumns.SIZE); + if (index == -1) { + return -1; + } + return cursor.getLong(index); } - } catch (Exception e) { + return -1; + } catch (final Exception ignored) { return -1; } } @@ -861,6 +879,20 @@ public File getStorageLocation(final String filename, final String mime) { return new File(appDirectory, filename); } + public static boolean inConversationsDirectory(final Context context, String path) { + final File fileDirectory = new File(path).getParentFile(); + for (final String type : STORAGE_TYPES) { + final File typeDirectory = + new File( + Environment.getExternalStoragePublicDirectory(type), + context.getString(R.string.app_name)); + if (typeDirectory.equals(fileDirectory)) { + return true; + } + } + return false; + } + public void setupRelativeFilePath( final Message message, final String filename, final String mime) { final File file = getStorageLocation(filename, mime); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 7420ca50a..1b334e5d4 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1184,8 +1184,11 @@ private void populateContextMenu(ContextMenu menu) { cancelTransmission.setVisible(true); } if (m.isFileOrImage() && !deleted && !cancelable) { - deleteFile.setVisible(true); - deleteFile.setTitle(activity.getString(R.string.delete_x_file, UIHelper.getFileDescriptionString(activity, m))); + final String path = m.getRelativeFilePath(); + if (path == null || !path.startsWith("/") || FileBackend.inConversationsDirectory(requireActivity(), path)) { + deleteFile.setVisible(true); + deleteFile.setTitle(activity.getString(R.string.delete_x_file, UIHelper.getFileDescriptionString(activity, m))); + } } if (showError) { showErrorMessage.setVisible(true); From 330980391cbfba90071760cd7b2e15eb82dc4d81 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 10 Mar 2022 16:14:26 +0100 Subject: [PATCH 074/394] pulled translations from transifex --- src/main/res/values-de/strings.xml | 10 +++++++++- src/main/res/values-gl/strings.xml | 10 +++++++++- src/main/res/values-zh-rCN/strings.xml | 10 +++++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 7d8f45319..aba514c43 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -622,6 +622,8 @@ Lösche privaten Speicher, in dem die Dateien gespeichert werden (sie können erneut vom Server heruntergeladen werden) Ich habe diesen Link aus einer vertrauenswürdigen Quelle erhalten Du bist dabei, die OMEMO-Schlüssel von %1$s nach dem Klick auf diesen Link zu überprüfen. Dies ist nur sicher, wenn du diesen Link von einer vertrauenswürdigen Quelle erhalten hast, der nur von %2$s veröffentlicht werden konnte. + Du bist dabei, die OMEMO-Schlüssel deines eigenen Kontos zu verifizieren. Dies ist nur sicher, wenn du diesem Link aus einer vertrauenswürdigen Quelle gefolgt bist, bei der nur du diesen Link veröffentlicht haben kannst. + Weiter Überprüfe OMEMO-Schlüssel Inaktive anzeigen Inaktive ausblenden @@ -904,6 +906,7 @@ Eingehender Videoanruf Verbinden Verbunden + Erneut verbinden Anruf annehmen Anruf beenden Annehmen @@ -919,6 +922,8 @@ Auflegen Laufender Anruf Laufender Videoanruf + Anruf erneut verbinden + Videoanruf erneut verbinden Deaktiviere Tor, um Anrufe zu tätigen Eingehender Anruf Eingehender Anruf · %s @@ -968,4 +973,7 @@ Die Sicherung wurde gestartet. Du bekommst eine Benachrichtigung, sobald sie fertig ist. Video kann nicht aktiviert werden. Textdokument - + Kontoregistrierungen werden nicht unterstützt + Keine XMPP-Adresse gefunden + + diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index be14eb100..a5ea07ed0 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -622,6 +622,8 @@ Baleirar a almacenaxe privada onde se gardan os ficheiros (poderán volver a descargarse desde o servidor) Seguín esta ligazón desde unha fonte de confianza Vas verificar as chaves OMEMO de %1$s despois de premer na ligazón. Esto só é seguro se seguiches esta ligazón desde unha fonte de confianza onde só %2$s a podería ter publicado. + Vas verificar as chaves OMEMO da túa propia conta. Esto só é seguro se seguiches esta ligazón desde unha orixe segura onde só tí poderías ter publicado esta ligazón. + Continuar Validar chaves OMEMO Mostrar inactivos Agochar inactivos @@ -904,6 +906,7 @@ Videochamada entrante Conectando Conectado + Reconectando Aceptando a chamada Rematando a chamada Responder @@ -919,6 +922,8 @@ Colgar Chamada en curso Videochamada en curso + Reconectando a chamada + Reconectando a videochamada Desactivar Tor para facer chamadas Chamada entrante Conversa de · %s @@ -968,4 +973,7 @@ Comezou a creación da copia de apoio. Recibirás unha notificación cando remate. Non se puido activar o vídeo. Documento de texto plano - + Non está permitido o rexistro de novas contas + Non se atopa un enderezo XMPP + + diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index 6323cea4d..fe11961a4 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -617,6 +617,8 @@ 清除保存私密文件的存储 (可以从服务器上重新下载) 此链接的源头是可信的 点击链接后将会开始校验%1$s的OMEMO密钥。只有%2$s发布的链接才是安全的。 + 您将验证您自己帐户的 OMEMO 密钥。只有当您从可信的来源跟踪此链接时,这才是安全的。“可信”指的是此链接只可能是你在来源中发布的。 + 继续 校验OMEMO密钥 显示不活跃设备 隐藏不活跃设备 @@ -893,6 +895,7 @@ 视频来电 正在连接 已连接 + 重新连接 正在接通来电 正在结束来电 应答 @@ -908,6 +911,8 @@ 挂断 正在进行的通话 正在进行的视频通话 + 重连通话 + 重连视频通话 禁用Tor以拨打电话 来电 来电 · %s @@ -955,4 +960,7 @@ 已启动备份。一旦完成,你会收到通知。 无法启用视频 纯文本文档 - + 不支持注册账户 + 未找到 XMPP 地址 + + From 78048bbd3ddeab23eb5104e83895b91f1306b5de Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Tue, 8 Mar 2022 15:14:08 -0500 Subject: [PATCH 075/394] Enable WebRTC-BindUsingInterfaceName/Enabled/ This makes 464XLAT networks (such as T-Mobile LTE) work. https://bugs.chromium.org/p/webrtc/issues/detail?id=10707 --- .../eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 0b990db43..8cd65447b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -233,7 +233,9 @@ private static boolean isFrontFacing(final CameraEnumerator cameraEnumerator, fi public void setup(final XmppConnectionService service, final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference) throws InitializationException { try { PeerConnectionFactory.initialize( - PeerConnectionFactory.InitializationOptions.builder(service).createInitializationOptions() + PeerConnectionFactory.InitializationOptions.builder(service) + .setFieldTrials("WebRTC-BindUsingInterfaceName/Enabled/") + .createInitializationOptions() ); } catch (final UnsatisfiedLinkError e) { throw new InitializationException("Unable to initialize PeerConnectionFactory", e); From 99e4c3d2e0c10d5ea81bd5b4ddb5dad9f295687f Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 10 Mar 2022 18:37:10 +0100 Subject: [PATCH 076/394] version bump to 2.10.3-beta.2 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 61b32120f..0598e5fe9 100644 --- a/build.gradle +++ b/build.gradle @@ -90,8 +90,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 30 - versionCode 42024 - versionName "2.10.3-beta" + versionCode 42025 + versionName "2.10.3-beta.2" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId From 3c1550b20857ade4d537ba63c136f5f8cc3ccaab Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 10 Mar 2022 18:40:30 +0100 Subject: [PATCH 077/394] show jid only for incoming calls during ringing --- .../conversations/ui/RtpSessionActivity.java | 26 ++++++++++++------- src/main/res/layout/activity_rtp_session.xml | 9 ++++--- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 842811edb..0e286c8dc 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -400,7 +400,7 @@ public void onNewIntent(final Intent intent) { } } else if (asList(ACTION_MAKE_VIDEO_CALL, ACTION_MAKE_VOICE_CALL).contains(action)) { proposeJingleRtpSession(account, with, actionToMedia(action)); - setWith(account.getRoster().getContact(with)); + setWith(account.getRoster().getContact(with), null); } else { throw new IllegalStateException("received onNewIntent without sessionId"); } @@ -424,7 +424,7 @@ void onBackendConnected() { } } else if (asList(ACTION_MAKE_VIDEO_CALL, ACTION_MAKE_VOICE_CALL).contains(action)) { proposeJingleRtpSession(account, with, actionToMedia(action)); - setWith(account.getRoster().getContact(with)); + setWith(account.getRoster().getContact(with), null); } else if (Intent.ACTION_VIEW.equals(action)) { final String extraLastState = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE); final RtpEndUserState state = @@ -437,7 +437,7 @@ void onBackendConnected() { updateIncomingCallScreen(state); invalidateOptionsMenu(); } - setWith(account.getRoster().getContact(with)); + setWith(account.getRoster().getContact(with), state); if (xmppConnectionService .getJingleConnectionManager() .fireJingleRtpConnectionStateUpdates()) { @@ -454,13 +454,19 @@ void onBackendConnected() { } } - private void setWidth() { - setWith(getWith()); + private void setWidth(final RtpEndUserState state) { + setWith(getWith(), state); } - private void setWith(final Contact contact) { + private void setWith(final Contact contact, final RtpEndUserState state) { binding.with.setText(contact.getDisplayName()); - binding.withJid.setText(contact.getJid().asBareJid().toEscapedString()); + if (Arrays.asList(RtpEndUserState.INCOMING_CALL, RtpEndUserState.ACCEPTING_CALL) + .contains(state)) { + binding.withJid.setText(contact.getJid().asBareJid().toEscapedString()); + binding.withJid.setVisibility(View.VISIBLE); + } else { + binding.withJid.setVisibility(View.GONE); + } } private void proposeJingleRtpSession( @@ -666,7 +672,7 @@ private boolean initializeActivityWithRunningRtpSession( requireRtpConnection().getState())) { putScreenInCallMode(); } - setWidth(); + setWidth(currentState); updateVideoViews(currentState); updateStateDisplay(currentState, media); updateVerifiedShield(verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(currentState)); @@ -685,7 +691,7 @@ private void initializeWithTerminatedSessionState( finish(); return; } - RtpEndUserState state = terminatedRtpSession.state; + final RtpEndUserState state = terminatedRtpSession.state; resetIntent(account, with, terminatedRtpSession.state, terminatedRtpSession.media); updateButtonConfiguration(state); updateStateDisplay(state); @@ -693,7 +699,7 @@ private void initializeWithTerminatedSessionState( updateCallDuration(); updateVerifiedShield(false); invalidateOptionsMenu(); - setWith(account.getRoster().getContact(with)); + setWith(account.getRoster().getContact(with), state); } private void reInitializeActivityWithRunningRtpSession( diff --git a/src/main/res/layout/activity_rtp_session.xml b/src/main/res/layout/activity_rtp_session.xml index 4563d9a97..7c52cf8a2 100644 --- a/src/main/res/layout/activity_rtp_session.xml +++ b/src/main/res/layout/activity_rtp_session.xml @@ -53,24 +53,27 @@ android:layout_height="wrap_content" android:layout_below="@id/status" android:layout_marginLeft="16dp" - android:layout_marginTop="0dp" android:layout_marginRight="16dp" - android:layout_marginBottom="8dp" android:textAppearance="@style/TextAppearance.Conversations.Display2" android:textColor="@color/white" tools:text="Juliet Capulet" /> + + From f9acc3bf7187ff73499d0c631d25533c89462550 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 11 Mar 2022 08:34:05 +0100 Subject: [PATCH 078/394] bump libraries --- build.gradle | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 0598e5fe9..118dab5f2 100644 --- a/build.gradle +++ b/build.gradle @@ -48,8 +48,8 @@ dependencies { implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'com.google.android.material:material:1.4.0' - implementation "androidx.emoji2:emoji2:1.1.0-rc01" - freeImplementation "androidx.emoji2:emoji2-bundled:1.1.0-rc01" + implementation "androidx.emoji2:emoji2:1.1.0" + freeImplementation "androidx.emoji2:emoji2-bundled:1.1.0" implementation 'org.bouncycastle:bcmail-jdk15on:1.64' //zxing stopped supporting Java 7 so we have to stick with 3.3.3 @@ -62,8 +62,8 @@ dependencies { implementation "com.wefika:flowlayout:0.4.1" implementation 'com.otaliastudios:transcoder:0.10.4' - implementation 'org.jxmpp:jxmpp-jid:1.0.2' - implementation 'org.osmdroid:osmdroid-android:6.1.10' + implementation 'org.jxmpp:jxmpp-jid:1.0.3' + implementation 'org.osmdroid:osmdroid-android:6.1.11' implementation 'org.hsluv:hsluv:0.2' implementation 'org.conscrypt:conscrypt-android:2.5.2' implementation 'me.drakeet.support:toastcompat:1.1.0' @@ -74,7 +74,7 @@ dependencies { implementation "com.squareup.okhttp3:okhttp:4.9.3" implementation 'com.google.guava:guava:30.1.1-android' - quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.36' + quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.44' implementation fileTree(include: ['libwebrtc-m99.aar'], dir: 'libs') } From 1969a23726bca8eff12a1eac01300e18b41e9430 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 11 Mar 2022 15:24:10 +0100 Subject: [PATCH 079/394] pulled translations from transifex --- src/main/res/values-it/strings.xml | 10 +++++++++- src/main/res/values-ro-rRO/strings.xml | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index 07a619dff..6fb2bc823 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -622,6 +622,8 @@ Svuota l\'archivio privato nella quale sono memorizzati i file (possono essere riscaricati dal server) Ho seguito questo link da una fonte fidata Stai per verificare le chiavi OMEMO di %1$s cliccando un link. Questo metodo è sicuro solo se hai seguito il link da una fonte fidata dove solo %2$s può averlo pubblicato. + Stai per verificare le chiavi OMEMO del tuo stesso account. Questo metodo è sicuro solo se hai seguito il link da una fonte fidata dove solo tu puoi averlo pubblicato. + Continua Verifica chiavi OMEMO Mostra inattivi Nascondi inattivi @@ -904,6 +906,7 @@ Chiamata video in arrivo Connessione Connesso + Riconnessione Accettazione chiamata Chiusura chiamata Rispondi @@ -919,6 +922,8 @@ Riaggancia Chiamata in corso Chiamata video in corso + Riconnessione chiamata + Riconnessione chiamata video Disattiva Tor per le chiamate Chiamata in arrivo Chiamata in arrivo · %s @@ -968,4 +973,7 @@ Il backup è iniziato. Riceverai una notifica una volta completato. Impossibile attivare il video. Documento di testo - + Le registrazioni di profili non sono supportate + Nessun indirizzo XMPP trovato + + diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index 93b11c506..253c97ad1 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -627,6 +627,8 @@ Locul unde sunt fișierele păstrate (pot fi descărcate de pe server din nou) Am urmat această legătură de la o sursă de încredere Urmează să verificați cheile OMEMO pentru %1$s după ce veți deschide legătura. Acest lucru se poate face în siguranță doar dacă ați primit legătura de la o sursă de încredere unde doar %2$s putea publica. + Urmează să verificați cheile OMEMO pentru contul dumneavoastră. Acest lucru se poate face în siguranță doar dacă ați primit legătura de la o sursă de încredere unde doar dumneavoastră ați fi putut publica. + Continuă Verificare chei OMEMO Arată inactive Ascunde inactive @@ -915,6 +917,7 @@ Apel video primit Conectare Conectat + Reconectare Se acceptă apelul Se încheie apelul Răspunde @@ -930,6 +933,8 @@ Închide Apel în curs Apel video în curs + Reconectare apel + Reconectare apel video Dezactivați Tor pentru a face apeluri Apel primit Apel primit · %s @@ -981,4 +986,7 @@ Se creează copia de siguranță. Veți primi o notificare când acesta este completă. Nu s-a putut activa camera video. Document text - + Nu este posibilă înregistrarea unui cont + Nu a fost găsită o adresă XMPP + + From 7731a864fd5095dd0cd795b47d3634499c8cd8e0 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 12 Mar 2022 17:57:32 +0100 Subject: [PATCH 080/394] catch security exception when importing backup --- .../java/eu/siacs/conversations/ui/ImportBackupActivity.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java b/src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java index 90c214ab4..6e4815159 100644 --- a/src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java +++ b/src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java @@ -134,6 +134,8 @@ private void openBackupFileFromUri(final Uri uri, final boolean finishOnCancel) } catch (final IOException | IllegalArgumentException e) { Log.d(Config.LOGTAG, "unable to open backup file " + uri, e); Snackbar.make(binding.coordinator, R.string.not_a_backup_file, Snackbar.LENGTH_LONG).show(); + } catch (final SecurityException e) { + Snackbar.make(binding.coordinator, R.string.sharing_application_not_grant_permission, Snackbar.LENGTH_LONG).show(); } } From ed9886050607515aca3849066ea13d8ff6fd6bce Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 13 Mar 2022 07:37:41 +0100 Subject: [PATCH 081/394] pulled translations from transifex --- src/main/res/values-pl/strings.xml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 2cba52246..b1c0db084 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -632,6 +632,8 @@ Wyczyść prywatny magazyn gdzie trzymane są pliki (mogą zostać pobrane ponownie z serwera) Trafiłem na ten link w zaufanym źródle Zaraz zweryfikujesz klucz OMEMO %1$s klikając w link. Jest to bezpieczne jedynie, kiedy link pochodzi z zaufanego źródła gdzie tylko %2$s mógł go opublikować. + Weryfikujesz właśnie klucze OMEMO własnego konta. To jest bezpieczne tylko jeśli kliknąłeś łącze w miejscu w którym jedynie ty mogłeś je zamieścić. + Kontynuuj Zweryfikuj klucze OMEMO Pokaż nieaktywne Ukryj nieaktywne @@ -927,6 +929,7 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Wideorozmowa przychodząca Łączenie Połączony + Ponowne łączenie Akceptowanie połączenia Kończenie połączenia Połącz @@ -942,6 +945,8 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Rozłącz Połączenie wychodzące Wideorozmowa wychodząca + Ponowne łączenie rozmowy + Ponowne łączenie rozmowy wideo Wyłącz Tor aby dzwonić Połączenie przychodzące Połączenie przychodzące · %s @@ -995,4 +1000,7 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Tworzenie kopii zapasowej się rozpoczęło. Dostaniesz powiadomienie kiedy się zakończy. Nie można włączyć wideo. Dokument zwykłego tekstu - + Rejestracja kont nie jest wspierana + Nie znaleziono adresu XMPP + + From fbbd2edd94c9397b72bc4d2a4cdf07fec4a3360c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 14 Mar 2022 08:59:29 +0100 Subject: [PATCH 082/394] version bump to 2.10.3 + changelog --- CHANGELOG.md | 1 + build.gradle | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7ef6c6fa..b54d2c479 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Store files in location appropriate for Android 11 * Attempt to reconnect call after network switch +* Show caller JID and account JID in incoming call screen ### Version 2.10.2 diff --git a/build.gradle b/build.gradle index 118dab5f2..f81d87512 100644 --- a/build.gradle +++ b/build.gradle @@ -90,8 +90,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 30 - versionCode 42025 - versionName "2.10.3-beta.2" + versionCode 42026 + versionName "2.10.3" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId From 89428b0ad353e7ffd5ab71e0993b80c6f5c5ddbd Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 14 Mar 2022 09:08:27 +0100 Subject: [PATCH 083/394] pulled translations from transifex --- src/main/res/values-el/strings.xml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-el/strings.xml b/src/main/res/values-el/strings.xml index 16a2a8742..d6a8cd209 100644 --- a/src/main/res/values-el/strings.xml +++ b/src/main/res/values-el/strings.xml @@ -132,6 +132,8 @@ Στέλνοντας ίχνη στοίβας βοηθάτε την συνεχόμενη ανάπτυξη Επιβεβαίωση μηνυμάτων Επιτρέψτε στις επαφές σας να γνωρίζουν όταν έχετε λάβει και διαβάσει τα μηνύματά τους + Αποτροπή στιγμιοτύπων οθόνης + Απόκρυψη περιεχομένων εφαρμογής στην εναλλαγή εφαρμογών και αποκλεισμός στιγμιοτύπων οθόνης Διεπαφή χρήστη Το OpenKeychain ανέφερε κάποιο σφάλμα. Σφάλμα στο κλειδί κρυπτογράφησης. @@ -414,6 +416,7 @@ ήχος βίντεο εικόνα + διανυσματικά γραφικά έγγραφο PDF Εφαρμογή Android Επαφή @@ -619,6 +622,8 @@ Καθαρισμός ιδιωτικού χώρου όπου αποθηκεύονται αρχεία (Μπορούν να μεταφορτωθούν ξανά από τον διακομιστή) Ακολούθησα αυτόν τον σύνδεσμο από μια έμπιστη πηγή Πρόκειται να επαληθεύσετε τα κλειδιά OMEMO της επαφής %1$s ακολουθώντας έναν σύνδεσμο. Αυτό είναι ασφαλές μόνο αν ακολουθήσατε τον σύνδεσμο από μια έμπιστη πηγή όπου μόνο η επαφή %2$s μπορεί να τον δημοσίευσε. + Πρόκειται να επαληθεύσετε τα κλειδιά OMEMO του δικού σας λογαριασμού. Αυτό είναι ασφαλές μόνο αν ακολουθήσατε τον σύνδεσμο από έμπιστη πηγή, όπου μόνο εσείς μπορεί να δημοσιεύσατε τον σύνδεσμο. + Συνέχεια Επιβεβαίωση κλειδιών OMEMO Εμφάνιση ανενεργών Απόκρυψη ανενεργών @@ -901,6 +906,7 @@ Εισερχόμενη βιντεοκλήση Γίνεται σύνδεση Συνδέθηκε + Επανασύνδεση Αποδοχή κλήσης Τερματισμός κλήσης Απάντηση @@ -912,9 +918,12 @@ Απώλεια σύνδεσης Αποσυρμένη κλήση Αποτυχία εφαρμογής + Πρόβλημα επαλήθευσης Τερματισμός κλήσης Κλήση σε εξέλιξη Βιντεοκλήση σε εξέλιξη + Επανασύνδεση κλήσης + Επανασύνδεση βίντεοκλήσης Απενεργοποίηστε το Tor για να κάνετε κλήσεις Εισερχόμενη κλήση Εισερχόμενη κλήση · %s @@ -963,4 +972,8 @@ Κανένας από τους ενεργούς λογαριασμούς δεν υποστηρίζει αυτό το χαρακτηριστικό Το αντίγραφο ασφαλείας δημιουργείται. Θα λάβετε ειδοποίηση όταν ολοκληρωθεί. Αδυναμία ενεργοποίησης βίντεο. - + Έγγραφο απλού κειμένου + Δεν υποστηρίζονται εγγραφές λογαριασμών + Δεν βρέθηκε διεύθυνση XMPP + + From 7c6ab7febca12cf629bca44fa929271d7accfa42 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 22 Mar 2022 11:45:43 +0100 Subject: [PATCH 084/394] fix ability to use GoogleMaps ShareLocationPlugin --- build.gradle | 2 +- src/main/AndroidManifest.xml | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f81d87512..a8c58a5d5 100644 --- a/build.gradle +++ b/build.gradle @@ -33,7 +33,7 @@ configurations { dependencies { implementation 'androidx.viewpager:viewpager:1.0.0' - playstoreImplementation('com.google.firebase:firebase-messaging:23.0.0') { + playstoreImplementation('com.google.firebase:firebase-messaging:23.0.2') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index f3922675d..2d0c6b2ff 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -52,6 +52,12 @@ + + + + + + From fbf1cacae3959c96c7beed4e73993d64564403b5 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 24 Mar 2022 17:53:18 +0100 Subject: [PATCH 085/394] remove hint about yearly fee for server --- .../siacs/conversations/ui/MagicCreateActivity.java | 2 -- .../res/layout/activity_pick_server.xml | 12 ------------ src/conversations/res/layout/magic_create.xml | 13 ------------- src/conversations/res/values/strings.xml | 2 +- src/main/res/values-w360dp/fineprint.xml | 4 ---- src/main/res/values/fineprint.xml | 4 ---- 6 files changed, 1 insertion(+), 36 deletions(-) delete mode 100644 src/main/res/values-w360dp/fineprint.xml delete mode 100644 src/main/res/values/fineprint.xml diff --git a/src/conversations/java/eu/siacs/conversations/ui/MagicCreateActivity.java b/src/conversations/java/eu/siacs/conversations/ui/MagicCreateActivity.java index 3419d8fc9..6f0386672 100644 --- a/src/conversations/java/eu/siacs/conversations/ui/MagicCreateActivity.java +++ b/src/conversations/java/eu/siacs/conversations/ui/MagicCreateActivity.java @@ -66,13 +66,11 @@ protected void onCreate(final Bundle savedInstanceState) { if (username != null && domain != null) { binding.title.setText(R.string.your_server_invitation); binding.instructions.setText(getString(R.string.magic_create_text_fixed, domain)); - binding.finePrint.setVisibility(View.INVISIBLE); binding.username.setEnabled(false); binding.username.setText(this.username); updateFullJidInformation(this.username); } else if (domain != null) { binding.instructions.setText(getString(R.string.magic_create_text_on_x, domain)); - binding.finePrint.setVisibility(View.INVISIBLE); } binding.createAccount.setOnClickListener(v -> { try { diff --git a/src/conversations/res/layout/activity_pick_server.xml b/src/conversations/res/layout/activity_pick_server.xml index 16be52ec4..d55ea78cc 100644 --- a/src/conversations/res/layout/activity_pick_server.xml +++ b/src/conversations/res/layout/activity_pick_server.xml @@ -84,18 +84,6 @@ android:padding="8dp" android:src="@drawable/main_logo" /> - - diff --git a/src/conversations/res/layout/magic_create.xml b/src/conversations/res/layout/magic_create.xml index cc0337062..f6e0436a5 100644 --- a/src/conversations/res/layout/magic_create.xml +++ b/src/conversations/res/layout/magic_create.xml @@ -95,19 +95,6 @@ android:padding="8dp" android:src="@drawable/main_logo" /> - - diff --git a/src/conversations/res/values/strings.xml b/src/conversations/res/values/strings.xml index f9aaec9ee..fffee31d6 100644 --- a/src/conversations/res/values/strings.xml +++ b/src/conversations/res/values/strings.xml @@ -4,7 +4,7 @@ Use conversations.im Create new account Do you already have an XMPP account? This might be the case if you are already using a different XMPP client or have used Conversations before. If not you can create a new XMPP account right now.\nHint: Some email providers also provide XMPP accounts. - XMPP is a provider independent instant messaging network. You can use this client with what ever XMPP server you choose.\nHowever for your convenience we made it easy to create an account on conversations.im¹; a provider specially suited for the use with Conversations. + XMPP is a provider independent instant messaging network. You can use this client with what ever XMPP server you choose.\nHowever for your convenience we made it easy to create an account on conversations.im; a provider specially suited for the use with Conversations. You have been invited to %1$s. We will guide you through the process of creating an account.\nWhen picking %1$s as a provider you will be able to communicate with users of other providers by giving them your full XMPP address. You have been invited to %1$s. A username has already been picked for you. We will guide you through the process of creating an account.\nYou will be able to communicate with users of other providers by giving them your full XMPP address. Your server invitation diff --git a/src/main/res/values-w360dp/fineprint.xml b/src/main/res/values-w360dp/fineprint.xml deleted file mode 100644 index fa28fbed4..000000000 --- a/src/main/res/values-w360dp/fineprint.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - ¹ Optional conversations.im account €8/year. First 6 month free. - \ No newline at end of file diff --git a/src/main/res/values/fineprint.xml b/src/main/res/values/fineprint.xml deleted file mode 100644 index 55eeccb6d..000000000 --- a/src/main/res/values/fineprint.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - ¹ Optional conversations.im account €8/year. 6 month free. - \ No newline at end of file From 8834bc5084a8fb42ecfa813b166d6c7e23f58a7b Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 24 Mar 2022 17:53:32 +0100 Subject: [PATCH 086/394] pulled translations from transifex --- src/main/res/values-fr/strings.xml | 8 +++++++- src/main/res/values-gl/strings.xml | 14 +++++++------- src/main/res/values-pt-rBR/strings.xml | 12 ++++++++++-- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml index 256baacab..8e177693e 100644 --- a/src/main/res/values-fr/strings.xml +++ b/src/main/res/values-fr/strings.xml @@ -132,6 +132,7 @@ En envoyant des rapports de crash vous aidez le développement de Conversations Confirmation de lecture Informer vos contacts quand vous avez reçu et lu leurs messages + Interdire les captures d’écran Interface OpenKeychain a signalé une erreur. Mauvaise clé pour le chiffrement. @@ -615,6 +616,7 @@ Vide le stockage privé, où les fichiers sont conservés (ils peuvent être re-téléchargés depuis le serveur) J\'ai obtenu ce lien d\'une source de confiance Vous êtes sur le point de vérifier les clés OMEMO de %1$s en cliquant sur un lien. Cette procédure n\'est sécurisée que si le lien en question n\'a pu être publié que par %2$s et que vous l\'avez obtenu d\'une source digne de confiance. + Continuer Vérifier les clés OMEMO Afficher les comptes inactifs Cacher les comptes inactifs @@ -955,4 +957,8 @@ Aucune application trouvée Inviter à Conversations Impossible de lire l\'invitation - + Impossible d’activer la vidéo. + La création de nouveaux comptes n’est pas prise en charge + Aucune adresse XMPP trouvée + + diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index a5ea07ed0..234d5807e 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -186,7 +186,7 @@ Habilitar Seguro? Ao eliminar a conta eliminas todo o historial de conversas - Grabar audio + Gravar audio Enderezo XMPP Bloquear enderezo XMPP usuaria@exemplo.com @@ -205,8 +205,8 @@ XEP-0163: PEP (Avatars / OMEMO) XEP-0363: HTTP File Upload XEP-0357: Push - dispoñible - non dispoñible + Dispoñible + Fallo Anuncios de chave pública non notificados acaba de estar dispoñible visto hai un minuto @@ -697,8 +697,8 @@ O escaner de código QR necesita acceso á cámara. Desprazarse ata a parte inferior Desprazarse cara abaixo logo de enviar unha mensaxe - Editar a Menxase de Estado - Editar a menxase de estado + Editar a Mensaxe de Estado + Editar a mensaxe de estado Desactivar a encriptación %1$s non pode enviar mensaxes cifradas a %2$s. Podería deberse a que o teu contacto utiliza un servidor sen actualizar ou un cliente que non pode xestionar OMEMO. Non se obtivo a lista de dispositivos @@ -719,7 +719,7 @@ Pequena Mediana Grande - A menxase non foi encriptada para este disposivivo + A mensaxe non foi encriptada para este disposivivo Fallo ao descifrar a mensaxe OMEMO desfacer Compartir Localización está desactivado @@ -815,7 +815,7 @@ Non se atopou o servidor. Algo fallou ao xestionar a túa solicitude. Entrada da usuaria non válida - Non dispoñible temporalmente. Inténteo máis tarde. + Non dispoñible temporalmente. Inténtao máis tarde. Se conexión a rede. Inténteo de novo en %s Taxa de transferencia limitada diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index 9efea8ca8..2fbbc4809 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -621,7 +621,9 @@ Limpar o armazenamento privado Limpar o armazenamento privado onde os arquivos são mantidos (eles podem ser baixados do servidor novamente) Eu segui este link a partir de uma fonte segura - Você está prestes a verificar as chaves OMEMO de %1$s após ter clicado em um link. Isso só é seguro se você acesso esse link a partir de uma fonte segura, onde somente %2$s poderia tê-lo publicado. + Você está prestes a verificar as chaves OMEMO de %1$s após ter clicado em um link. Isso só é seguro se você acessou esse link a partir de uma fonte segura, onde somente %2$s poderia tê-lo publicado. + Você está prestes a verificar as chaves OMEMO de sua própria conta. Isso só é seguro se você acessou esse link a partir de uma fonte segura, onde somente você poderia tê-lo publicado. + Continuar Verificar chaves OMEMO Exibir os inativos Ocultar os inativos @@ -904,6 +906,7 @@ Recebendo chamada de vídeo Conectando Conectado + Reconectando Atendendo chamada Encerrando chamada Atender @@ -919,6 +922,8 @@ Desligar Chamada em andamento Chamada de vídeo em andamento + Reconectando a chamada + Reconectando a vídeo-chamada Desabilitar o Tor para fazer chamadas Chamada recebida Chamada recebida · %s @@ -968,4 +973,7 @@ O backup foi iniciado. Você receberá uma notificação assim que ele for concluído. Não foi possível habilitar o vídeo. Documento em texto puro - + O registro de contas não está ativo + Não foi encontrado nenhum endereço XMPP + + From 5943f1ad3ee9101738339d26fac7b865e2f64f50 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 25 Mar 2022 08:03:18 +0100 Subject: [PATCH 087/394] version bump to 2.10.4 + changelog --- CHANGELOG.md | 5 +++++ build.gradle | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b54d2c479..b445c52ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +### Version 2.10.4 + +* Fix interaction with Google Maps Share Location Plugin +* Remove footnote with regards to server fee + ### Version 2.10.3 * Store files in location appropriate for Android 11 diff --git a/build.gradle b/build.gradle index a8c58a5d5..f1b134848 100644 --- a/build.gradle +++ b/build.gradle @@ -90,8 +90,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 30 - versionCode 42026 - versionName "2.10.3" + versionCode 42027 + versionName "2.10.4" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId From 36756fbd410d72e64b2e579eb383befaa806a407 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 26 Mar 2022 08:25:45 +0100 Subject: [PATCH 088/394] catch two rare exceptions to fix crash --- .../eu/siacs/conversations/persistance/FileBackend.java | 2 +- .../siacs/conversations/services/MessageArchiveService.java | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 404f521f4..821899eb7 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -722,7 +722,7 @@ private String getExtensionFromUri(final Uri uri) { if (cursor != null && cursor.moveToFirst()) { filename = cursor.getString(0); } - } catch (final SecurityException | IllegalArgumentException e) { + } catch (final Exception e) { filename = null; } if (filename == null) { diff --git a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java index b382022b9..79cad9520 100644 --- a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java +++ b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java @@ -256,7 +256,11 @@ private void execute(final Query query) { //do nothing } else { Log.d(Config.LOGTAG, a.getJid().asBareJid().toString() + ": error executing mam: " + p.toString()); - finalizeQuery(query, true); + try { + finalizeQuery(query, true); + } catch (final IllegalStateException e) { + //ignored + } } }); } else { From de7eb2b5c7b28e538c05a0729109395504df7ecf Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 26 Mar 2022 08:43:25 +0100 Subject: [PATCH 089/394] remove footnote hint in translations --- src/conversations/res/values-bg/strings.xml | 2 +- src/conversations/res/values-bn-rIN/strings.xml | 2 +- src/conversations/res/values-ca/strings.xml | 2 +- src/conversations/res/values-da-rDK/strings.xml | 2 +- src/conversations/res/values-de/strings.xml | 2 +- src/conversations/res/values-el/strings.xml | 2 +- src/conversations/res/values-es/strings.xml | 2 +- src/conversations/res/values-eu/strings.xml | 2 +- src/conversations/res/values-fr/strings.xml | 2 +- src/conversations/res/values-gl/strings.xml | 2 +- src/conversations/res/values-hu/strings.xml | 2 +- src/conversations/res/values-id/strings.xml | 2 +- src/conversations/res/values-ja/strings.xml | 2 +- src/conversations/res/values-nl/strings.xml | 2 +- src/conversations/res/values-pl/strings.xml | 2 +- src/conversations/res/values-pt-rBR/strings.xml | 2 +- src/conversations/res/values-ro-rRO/strings.xml | 2 +- src/conversations/res/values-sk/strings.xml | 2 +- src/conversations/res/values-sr/strings.xml | 2 +- src/conversations/res/values-tr-rTR/strings.xml | 2 +- src/conversations/res/values-uk/strings.xml | 2 +- src/conversations/res/values-vi/strings.xml | 2 +- src/conversations/res/values-zh-rCN/strings.xml | 2 +- 23 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/conversations/res/values-bg/strings.xml b/src/conversations/res/values-bg/strings.xml index 7ef32e025..92667523d 100644 --- a/src/conversations/res/values-bg/strings.xml +++ b/src/conversations/res/values-bg/strings.xml @@ -5,7 +5,7 @@ Създаване не нов профил Имате ли вече XMPP профил? Може да имате, ако вече използвате друг клиент на XMPP или сте използвали Conversations и преди. Ако не, можете да създадете нов XMPP профил сега.\nСъвет: някои доставчици на е-поща също предоставят XMPP профили.   - XMPP е мрежа за общуване чрез мигновени съобщения, която не е обвързана с конкретен доставчик. Можете да използвате клиента с всеки сървър, който работи с XMPP.\nЗа Ваше удобство, обаче, ние предоставяме лесен начин да си създадете профил в conversations.im¹ — сървър, пригоден да работи най-добре с Conversations. + XMPP е мрежа за общуване чрез мигновени съобщения, която не е обвързана с конкретен доставчик. Можете да използвате клиента с всеки сървър, който работи с XMPP.\nЗа Ваше удобство, обаче, ние предоставяме лесен начин да си създадете профил в conversations.im — сървър, пригоден да работи най-добре с Conversations. Получихте покана за %1$s. Ще Ви преведем през процеса на създаване на профил.\nИзбирайки %1$s за доставчик, Вие ще можете да общувате и с потребители на други доставчици, като им предоставите своя пълен XMPP адрес. Получихте покана за %1$s. Вече Ви избрахме потребителско име. Ще Ви преведем през процеса на създаване на профил.\nЩе можете да общувате и с потребители на други доставчици, като им предоставите своя пълен XMPP адрес. Вашата покана за сървъра diff --git a/src/conversations/res/values-bn-rIN/strings.xml b/src/conversations/res/values-bn-rIN/strings.xml index c99df7cfe..382343a37 100644 --- a/src/conversations/res/values-bn-rIN/strings.xml +++ b/src/conversations/res/values-bn-rIN/strings.xml @@ -4,7 +4,7 @@ conversations.im-ই ব্যবহার করা যাক নতুন অ্যকাউন্ট তৈরী করা যাক আপনার কি একটা XMPP অ্যকাউন্ট ইতিমধ্যে করা আছে? সেরকমটা হতেই পারে যদি এর আগে আপনি কোনো অন্য XMPP প্রোগ্রাম বা অ্যাপ ব্যবহার করে থাকেন। এই মুহুর্তে আরেকটা অ্যকাউন্ট তৈরী করা সম্ভব না।‌\nHint: মাঝে মাঝে ইমেল অ্যকাউন্ট খুললেও এরকম অ্যকাউন্ট নিজে থেকেই তৈরী হয়ে যায়। - XMPP কোনো একটি নির্দিষ্ট সংস্থার উপরে নির্ভরশীল নয়। এই অ্যপটি আপনি যেকোনো সংস্থার XMPP সার্ভারের সাথে ব্যবহার করতে পারেন।\nমনে রাখবেন, সুধুমাত্র আপনার সুবিধার্থেই conversations.im¹ -এ আপনার জন্যে একটি অ্যকাউন্ট তৈরী করে দেওয়া হয়েছে। Conversations অ্যপটি এই সার্ভারের সাথে সবথেকে বেশী কার্যকারী। + XMPP কোনো একটি নির্দিষ্ট সংস্থার উপরে নির্ভরশীল নয়। এই অ্যপটি আপনি যেকোনো সংস্থার XMPP সার্ভারের সাথে ব্যবহার করতে পারেন।\nমনে রাখবেন, সুধুমাত্র আপনার সুবিধার্থেই conversations.im -এ আপনার জন্যে একটি অ্যকাউন্ট তৈরী করে দেওয়া হয়েছে। Conversations অ্যপটি এই সার্ভারের সাথে সবথেকে বেশী কার্যকারী। আপনাকে %1$s-এ আমন্ত্রিত করা হয়েছে। অ্যকাউন্ট তৈরী করার সময় আপনাকে সাহায্য করা হবে।\n%1$s ব্যবহার করলেও, অন্য সেবা-প্রদানকারী সংস্থার ব্যবহারকারীদের সাথে আপনি কথা বলতে পারবেন, আপনার সম্পূর্ণ XMPP অ্যড্রেস তাদেরকে বলে দিয়ে। আপনাকে %1$s-এ নিমন্ত্রণ করা হয়েছে। একটি username-ও আপনার জন্যে নির্দিষ্ট করে রাখা হয়েছে। অ্যকাউন্ট তৈরী করার সময় আপনাকে সাহায্য করা হবে।\nঅন্য XMPP সেবা প্রদানকারী সংস্থার ব্যবহারকারীদের সাথে আপনিও কথা বলতে পারবেন, আপনার সম্পূর্ণ XMPP অ্যড্রেস তাদেরকে বলে দিয়ে। আপনার নিমন্ত্রণপত্র, সার্ভার থেকে diff --git a/src/conversations/res/values-ca/strings.xml b/src/conversations/res/values-ca/strings.xml index be3d17103..7606e4708 100644 --- a/src/conversations/res/values-ca/strings.xml +++ b/src/conversations/res/values-ca/strings.xml @@ -5,7 +5,7 @@ Fer servir conversations.im Crear un compte nou Ja tens un compte XMPP? Aquest podria ser el cas si ja estàs usant un client XMPP diferent o has usat Converses abans. Si no, pots crear un nou compte XMPP ara mateix.\nPista: Alguns proveïdors de correu electrònic també proporcionen comptes XMPP. - XMPP és una xarxa de missatgeria instantània independent del proveïdor. Pots usar aquest client amb qualsevol servidor XMPP que triïs. No obstant això, per a la teva conveniència, hem fet fàcil la creació d\'un compte en Conversaciones.im¹; un proveïdor especialment adequat per a l\'ús amb Conversations. + XMPP és una xarxa de missatgeria instantània independent del proveïdor. Pots usar aquest client amb qualsevol servidor XMPP que triïs. No obstant això, per a la teva conveniència, hem fet fàcil la creació d\'un compte en Conversaciones.im; un proveïdor especialment adequat per a l\'ús amb Conversations. Has estat convidat a %1$s. Et guiarem a través del procés de creació d\'un compte.\nEn triar%1$s com a proveïdor podràs comunicar-se amb els usuaris d\'altres proveïdors donant-los la seva adreça XMPP completa. Has estat convidat a %1$s . Ja s\'ha triat un nom d\'usuari per a tu. Et guiarem en el procés de creació d\'un compte. Podràs comunicar-te amb usuaris d\'altres proveïdors donant-los la teva adreça XMPP completa. La teva invitació al servidor diff --git a/src/conversations/res/values-da-rDK/strings.xml b/src/conversations/res/values-da-rDK/strings.xml index a55288db9..fb5992a1b 100644 --- a/src/conversations/res/values-da-rDK/strings.xml +++ b/src/conversations/res/values-da-rDK/strings.xml @@ -4,7 +4,7 @@ Brug conversations.im Opret ny konto Har du allerede en XMPP-konto? Dette kan være tilfældet, hvis du allerede bruger en anden XMPP-klient eller har brugt Conversations før. Hvis ikke, kan du lige nu oprette en ny XMPP-konto.\nTip: Nogle e-mail-udbydere leverer også XMPP-konti. - XMPP er et udbyderuafhængigt onlinemeddelelsesnetværk. Du kan bruge denne klient med hvilken XMPP-server du end vælger.\nMen for din nemhedsskyld har vi gjort vi det let at oprette en konto på conversations.im¹; en udbyder, der er specielt velegnet til brug med Conversations. + XMPP er et udbyderuafhængigt onlinemeddelelsesnetværk. Du kan bruge denne klient med hvilken XMPP-server du end vælger.\nMen for din nemhedsskyld har vi gjort vi det let at oprette en konto på conversations.im; en udbyder, der er specielt velegnet til brug med Conversations. Du er blevet inviteret til %1$s. Vi guider dig gennem processen med at oprette en konto.\nNår du vælger %1$s som udbyder, kan du kommunikere med brugere fra andre udbydere ved at give dem din fulde XMPP-adresse. Du er blevet inviteret til %1$s. Der er allerede valgt et brugernavn til dig. Vi guider dig gennem processen med at oprette en konto.\nDu vil være i stand til at kommunikere med brugere fra andre udbydere ved at give dem din fulde XMPP-adresse. Din server invitation diff --git a/src/conversations/res/values-de/strings.xml b/src/conversations/res/values-de/strings.xml index e1f217512..2fd0319a9 100644 --- a/src/conversations/res/values-de/strings.xml +++ b/src/conversations/res/values-de/strings.xml @@ -4,7 +4,7 @@ Benutze conversations.im Neues Konto erstellen Hast du bereits ein XMPP-Konto? Dies kann der Fall sein, wenn du bereits einen anderen XMPP-Client verwendest oder bereits Conversations verwendet hast. Wenn nicht, kannst du jetzt ein neues XMPP-Konto erstellen.\nTipp: Einige E-Mail-Anbieter bieten auch XMPP-Konten an. - XMPP ist ein anbieterunabhängiges Instant Messaging Netzwerk. Du kannst diesen Client mit jedem beliebigen XMPP-Server nutzen.\nUm es dir leicht zu machen, haben wir die Möglichkeit geschaffen, ein Konto auf conversations.im¹ anzulegen; ein Anbieter, der speziell für die Verwendung mit Conversations geeignet ist. + XMPP ist ein anbieterunabhängiges Instant Messaging Netzwerk. Du kannst diesen Client mit jedem beliebigen XMPP-Server nutzen.\nUm es dir leicht zu machen, haben wir die Möglichkeit geschaffen, ein Konto auf conversations.im anzulegen; ein Anbieter, der speziell für die Verwendung mit Conversations geeignet ist. Du wurdest zu %1$s eingeladen. Wir führen dich durch den Prozess der Kontoerstellung.\nWenn du %1$s als Provider wählst, kannst du mit Nutzern anderer Anbieter kommunizieren, indem du ihnen deine vollständige XMPP-Adresse gibst. Du wurdest zu %1$seingeladen. Ein Benutzername ist bereits für dich ausgewählt worden. Wir führen dich durch den Prozess der Kontoerstellung.\nDu kannst mit Nutzern anderer Anbieter kommunizieren, indem du ihnen deine vollständige XMPP-Adresse gibst. Deine Einladung für den Server diff --git a/src/conversations/res/values-el/strings.xml b/src/conversations/res/values-el/strings.xml index 7c87e66a3..bb7bcadf0 100644 --- a/src/conversations/res/values-el/strings.xml +++ b/src/conversations/res/values-el/strings.xml @@ -4,7 +4,7 @@ Χρήση του conversations.im Δημιουργία νέου λογαριασμού Έχετε ήδη λογαριασμό XMPP; Αυτό μπορεί να συμβαίνει αν ήδη χρησιμοποιείτε ένα άλλο πρόγραμμα XMPP ή έχετε χρησιμοποιήσει το Conversations παλιότερα. Αν όχι, μπορείτε να δημιουργήσετε ένα νέο λογαριασμό XMPP τώρα.\nΧρήσιμη πληροφορία: Κάποιοι πάροχοι e-mail παρέχουν επίσης και λογαριασμούς XMPP. - Το XMPP είναι ένα δίκτυο άμεσης ανταλλαγής μηνυμάτων ανεξάρτητο παρόχου. Μπορείτε να χρησιμοποιήσετε αυτό το πρόγραμμα με όποιον διακομιστή XMPP επιθυμείτε.\nΓια διευκόλυνση πάντως μπορείτε να δημιουργήσετε έναν λογαριασμό στο conversations.im¹, έναν πάροχο ειδικά σχεδιασμένο για χρήση με το Conversations. + Το XMPP είναι ένα δίκτυο άμεσης ανταλλαγής μηνυμάτων ανεξάρτητο παρόχου. Μπορείτε να χρησιμοποιήσετε αυτό το πρόγραμμα με όποιον διακομιστή XMPP επιθυμείτε.\nΓια διευκόλυνση πάντως μπορείτε να δημιουργήσετε έναν λογαριασμό στο conversations.im, έναν πάροχο ειδικά σχεδιασμένο για χρήση με το Conversations. Έχετε προσκληθεί στο %1$s. Θα σας καθοδηγήσουμε στη διαδικασία δημιουργίας λογαριασμού.\nΕπιλέγοντας τον %1$s ως πάροχο θα μπορείτε να επικοινωνείτε με χρήστες άλλων παρόχων δίνοντάς τους την πλήρη διεύθυνση XMPP σας. Έχετε προσκληθεί στο %1$s. Ένα όνομα χρήστη έχει ήδη επιλεγεί για εσάς. Θα σας καθοδηγήσουμε στη διαδικασία δημιουργίας λογαριασμού.\nΘα μπορείτε να επικοινωνείτε με χρήστες άλλων παρόχων δίνοντάς τους την πλήρη διεύθυνση XMPP σας. Η πρόσκλησή σας στον διακομιστή diff --git a/src/conversations/res/values-es/strings.xml b/src/conversations/res/values-es/strings.xml index 690bd8f5a..b5ca254b5 100644 --- a/src/conversations/res/values-es/strings.xml +++ b/src/conversations/res/values-es/strings.xml @@ -4,7 +4,7 @@ Usa conversations.im Crear nueva cuenta ¿Ya tienes una cuenta XMPP? Este puede ser el caso si ya estás usando un cliente XMPP diferente o has usado Conversations anteriormente. Si no es así, puedes crear una nueva cuenta XMPP ahora mismo.\nConsejo: Algunos proveedores de email también ofrecen una cuenta XMPP. - XMPP es una red de mensajería instantánea independiente del proveedor. Puedes usar este cliente con cualquier servidor XMPP que elijas.\nSin embargo, para tu conveniencia, hacemos de forma sencilla la creación de una cuenta en conversations.im¹; un proveedor especializado para el uso con Conversations + XMPP es una red de mensajería instantánea independiente del proveedor. Puedes usar este cliente con cualquier servidor XMPP que elijas.\nSin embargo, para tu conveniencia, hacemos de forma sencilla la creación de una cuenta en conversations.im; un proveedor especializado para el uso con Conversations Has sido invitado a %1$s. Te guiaremos durante el proceso de creación de la cuenta.\nCuando selecciones %1$s como proveedor podrás comunicarte con usuarios de otros servidores proporcionándoles tu dirección XMPP completa. Has sido invitado a %1$s. Un nombre de usuario ya ha sido escogido para ti. Te guiaremos durante el proceso de creación de la cuenta.\nPodrás comunicarte con otros usuarios de otros servidores proporcionándoles tu dirección XMPP completa. Tu invitación al servidor diff --git a/src/conversations/res/values-eu/strings.xml b/src/conversations/res/values-eu/strings.xml index 4ab74b6ab..bf9555311 100644 --- a/src/conversations/res/values-eu/strings.xml +++ b/src/conversations/res/values-eu/strings.xml @@ -4,5 +4,5 @@ Erabili conversations.im Kontu berria sortu XMPP kontu bat badaukazu dagoeneko? Horrela izan daiteke beste XMPP aplikazio bat erabiltzen baduzu edo Conversations lehenago erabili baduzu. Bestela XMPP kontu berri bat sortu dezakezu oraintxe bertan.\nIradokizuna: email hornitzaile batzuek XMPP kontuak hornitzen dituzte ere. - XMPP hornitzailez independientea den bat-bateko mezularitza sare bat da. Aplikazio hau nahi duzun XMPP zerbitzariarekin erabili dezakezu.\nHala ere zure erosotasunerako conversations.im¹-en, Conversationsekin bereziki erabiltzeko egokia den hornitzaile batean, kontu bat sortzea erraz egin dugu. + XMPP hornitzailez independientea den bat-bateko mezularitza sare bat da. Aplikazio hau nahi duzun XMPP zerbitzariarekin erabili dezakezu.\nHala ere zure erosotasunerako conversations.im-en, Conversationsekin bereziki erabiltzeko egokia den hornitzaile batean, kontu bat sortzea erraz egin dugu. \ No newline at end of file diff --git a/src/conversations/res/values-fr/strings.xml b/src/conversations/res/values-fr/strings.xml index 3d7d1c4a8..47badf219 100644 --- a/src/conversations/res/values-fr/strings.xml +++ b/src/conversations/res/values-fr/strings.xml @@ -4,7 +4,7 @@ Utiliser conversations.im Créer un nouveau compte Avez-vous déjà un compte XMPP ? Cela peut être le cas si vous utilisez déjà un autre client XMPP ou si vous avez déjà utilisé Conversations auparavant. Sinon, vous pouvez créer un nouveau compte XMPP dès maintenant.\nRemarque : Certains fournisseurs de messagerie proposent également des comptes XMPP. - XMPP est un réseau de messagerie instantanée indépendant du fournisseur. Vous pouvez utiliser ce client avec n’importe quel serveur XMPP de votre choix.\nToutefois, pour votre commodité, nous avons facilité la création d’un compte sur conversations.im¹ ; un fournisseur spécialement conçu pour Conversations. + XMPP est un réseau de messagerie instantanée indépendant du fournisseur. Vous pouvez utiliser ce client avec n’importe quel serveur XMPP de votre choix.\nToutefois, pour votre commodité, nous avons facilité la création d’un compte sur conversations.im ; un fournisseur spécialement conçu pour Conversations. Vous avez été invité à %1$s. Nous allons vous guider à travers le processus de création d’un compte.\nEn choisissant %1$s comme fournisseur, vous pourrez communiquer avec les utilisateurs des autres fournisseurs en leur donnant votre adresse XMPP complète. Vous avez été invité à %1$s. Un nom d’utilisateur a déjà été choisi pour vous. Nous allons vous guider à travers le processus de création d’un compte.\nVous pourrez communiquer avec les utilisateurs des autres fournisseurs en leur donnant votre adresse XMPP complète. Votre invitation au serveur diff --git a/src/conversations/res/values-gl/strings.xml b/src/conversations/res/values-gl/strings.xml index 98d151721..ed3299863 100644 --- a/src/conversations/res/values-gl/strings.xml +++ b/src/conversations/res/values-gl/strings.xml @@ -4,7 +4,7 @@ Utilizar conversations.im Crear nova conta Xa posúes unha conta XMPP? Este pode ser o caso se xa estás a utilizar outro cliente XMPP ou utilizaches Conversations previamente. Se non é así podes crear unha nova conta agora mesmo.\nTruco: Algúns provedores de correo tamén proporcionan contas XMPP. - XMPP é unha rede de mensaxería independente do provedor. Podes utilizar este cliente con calquera provedor XMPP da túa elección.\nMais para a tua conveniencia fixemos que fose doado crear unha conta en conversations.im¹; un provedor especialmente axeitado para utilizar con Conversations. + XMPP é unha rede de mensaxería independente do provedor. Podes utilizar este cliente con calquera provedor XMPP da túa elección.\nMais para a tua conveniencia fixemos que fose doado crear unha conta en conversations.im; un provedor especialmente axeitado para utilizar con Conversations. Convidáronte a %1$s. Guiarémoste no proceso para crear unha conta.\nAo elexir %1$s como provedor poderás comunicarte con usuarias doutros provedores cando lles deas o teu enderezo XMPP completo. Convidáronte a %1$s. Xa eleximos un nome de usuaria para ti. Guiarémoste no proceso de crear unha conta.\nPoderás comunicarte con usuarias doutros provedores cando lles digas o teu enderezo XMPP completo. O convite do teu servidor diff --git a/src/conversations/res/values-hu/strings.xml b/src/conversations/res/values-hu/strings.xml index 8e405d0e1..f4c180889 100644 --- a/src/conversations/res/values-hu/strings.xml +++ b/src/conversations/res/values-hu/strings.xml @@ -4,7 +4,7 @@ A conversations.im használata Új fiók létrehozása Már rendelkezik XMPP-fiókkal? Ez az eset állhat fenn, ha már egy másik XMPP-klienst használ, vagy ha már korábban használta a Conversations alkalmazást. Ha nem, akkor most létrehozhat egy új XMPP-fiókot.\nTipp: egyes e-mail szolgáltatók is biztosítanak XMPP-fiókokat. - Az XMPP egy szolgáltatófüggetlen, azonnali üzenetküldő hálózat. Ezt a kliensprogramot bármely XMPP-kiszolgálóhoz használhatja.\nAzonban a kényelem érdekében megkönnyítettük a conversations.im¹ szolgáltatón való fióklétrehozást, ami kifejezetten a Conversations alkalmazással történő használatra lett tervezve. + Az XMPP egy szolgáltatófüggetlen, azonnali üzenetküldő hálózat. Ezt a kliensprogramot bármely XMPP-kiszolgálóhoz használhatja.\nAzonban a kényelem érdekében megkönnyítettük a conversations.im szolgáltatón való fióklétrehozást, ami kifejezetten a Conversations alkalmazással történő használatra lett tervezve. Meghívást kapott a(z) %1$s kiszolgálóra. Végig fogjuk vezetni egy fiók létrehozásának folyamatán.\nHa a(z) %1$s kiszolgálót választja szolgáltatóként, akkor képes lesz más szolgáltatók felhasználóival is kommunikálni, ha megadja nekik a teljes XMPP-címét. Meghívást kapott a(z) %1$s kiszolgálóra. Már kiválasztottak Önnek egy felhasználónevet. Végig fogjuk vezetni egy fiók létrehozásának folyamatán.\nKépes lesz más szolgáltatók felhasználóival is kommunikálni, ha megadja nekik a teljes XMPP-címét. Az Ön kiszolgálómeghívása diff --git a/src/conversations/res/values-id/strings.xml b/src/conversations/res/values-id/strings.xml index fb4121cc3..16a58436c 100644 --- a/src/conversations/res/values-id/strings.xml +++ b/src/conversations/res/values-id/strings.xml @@ -4,7 +4,7 @@ Gunakan conversations.im Buat akun baru Anda sudah memiliki akun XMPP? Ini mungkin terjadi jika Anda sudah menggunakan aplikasi XMPP yang berbeda atau pernah menggunakan Conversations sebelumnya. Jika tidak, Anda dapat membuat akun XMPP baru. \ NPetunjuk: Beberapa penyedia layanan email juga menyediakan akun XMPP. - XMPP adalah jaringan penyedia pesan instan independen. Anda dapat menggunakan aplikasi ini dengan server XMPP pilihan Anda. \ NNamun demi kenyamanan Anda, kami permudah untuk membuat akun di Conversations.im¹; provider yang sangat cocok digunakan dengan Conversations. + XMPP adalah jaringan penyedia pesan instan independen. Anda dapat menggunakan aplikasi ini dengan server XMPP pilihan Anda. \ NNamun demi kenyamanan Anda, kami permudah untuk membuat akun di Conversations.im; provider yang sangat cocok digunakan dengan Conversations. Anda telah diundang ke %1$s. Kami akan memandu Anda melalui proses pembuatan akun. \nSaat memilih %1$s sebagai penyedia, Anda akan dapat berkomunikasi dengan pengguna provider lain dengan memberikan alamat XMPP lengkap Anda kepada mereka. Anda telah diundang ke%1$s. Username telah dipilihkan untuk Anda. Kami akan memandu Anda melalui proses pembuatan akun. \nAnda dapat berkomunikasi dengan pengguna provider lain dengan memberi mereka alamat XMPP lengkap Anda. Undangan server Anda diff --git a/src/conversations/res/values-ja/strings.xml b/src/conversations/res/values-ja/strings.xml index a36b4a119..2d240bedc 100644 --- a/src/conversations/res/values-ja/strings.xml +++ b/src/conversations/res/values-ja/strings.xml @@ -4,7 +4,7 @@ conversations.im を利用する 新規アカウントを作成 XMPP アカウントをお持ちですか?既にほかの XMPP クライアントを利用しているか、 Conversations を利用したことがある場合はこちら。初めての方は、今すぐ新規 XMPP アカウントを作成できます。\nヒント: e メールのプロバイダーが XMPP アカウントも提供している場合があります。 - XMPP は、プロバイダーに依存しないインスタントメッセージのプロトコルです。 XMPP サーバーならどこでも、このクライアントを使用することができます。\nよろしければ、 Conversations に最適化されたプロバイダー conversations.im¹ で簡単にアカウントを作成することもできます。 + XMPP は、プロバイダーに依存しないインスタントメッセージのプロトコルです。 XMPP サーバーならどこでも、このクライアントを使用することができます。\nよろしければ、 Conversations に最適化されたプロバイダー conversations.im で簡単にアカウントを作成することもできます。 %1$s へ招待されました。アカウント作成手順をご案内します。 \n%1$s をプロバイダーに選択してほかのプロバイダーのユーザーと会話するには、 XMPP のフルアドレスを相手にお知らせください。 %1$s へ招待されました。ユーザー名は既に選択されています。アカウント作成手順をご案内します。 \nほかのプロバイダーのユーザーと会話するには、 XMPP のフルアドレスを相手にお知らせください。 サーバーの招待 diff --git a/src/conversations/res/values-nl/strings.xml b/src/conversations/res/values-nl/strings.xml index a92dca5b2..968a276d1 100644 --- a/src/conversations/res/values-nl/strings.xml +++ b/src/conversations/res/values-nl/strings.xml @@ -4,7 +4,7 @@ Conversations.im gebruiken Nieuwe account registreren Heb je al een XMPP-account? Als je al een andere XMPP-cliënt gebruikt, of Conversations vroeger al eens hebt gebruikt, is dit waarschijnlijk het geval. Zo niet, kan je nu een nieuwe XMPP-account aanmaken.\nTip: sommige e-mailproviders bieden ook XMPP-accounts aan. - XMPP is een provider-onafhankelijk berichtennetwerk. Je kan deze cliënt gebruiken met eender welke XMPP-server.\nOm het je gemakkelijker te maken kun je simpelweg een account aanmaken op conversations.im¹; een provider speciaal geschikt voor Conversations. + XMPP is een provider-onafhankelijk berichtennetwerk. Je kan deze cliënt gebruiken met eender welke XMPP-server.\nOm het je gemakkelijker te maken kun je simpelweg een account aanmaken op conversations.im; een provider speciaal geschikt voor Conversations. Je ontving een uitnodiging voor %1$s. We zullen je helpen een account aan te maken.\nWanneer je %1$s als je provider kiest kan je met gebruikers van andere providers communiceren door hen je volledige XMPP-adres te geven. Je ontving een uitnodiging voor %1$s. Er werd reeds een gebruikersnaam voor jou gekozen. We zullen je helpen een account aan te maken.\nJe zal met gebruikers van andere providers communiceren door hen je volledige XMPP-adres te geven. Je server uitnodiging diff --git a/src/conversations/res/values-pl/strings.xml b/src/conversations/res/values-pl/strings.xml index 4e8b5d619..ac746f8e6 100644 --- a/src/conversations/res/values-pl/strings.xml +++ b/src/conversations/res/values-pl/strings.xml @@ -4,7 +4,7 @@ Użyj conversations.im Stwórz nowe konto Czy masz już konto XMPP? Tak może być jeśli używasz już innego klienta XMPP lub używałeś już Conversations. Jeśli nie możesz stworzyć nowe konto XMPP teraz.\nPodpowiedź: Niektórzy dostawcy poczty oferują również konta XMPP. - XMPP to niezależna od dostawcy sieć komunikacji błyskawicznej. Możesz użyć tego klienta z dowolnym serwerem XMPP.\nDla twojej wygody jednak ułatwiliśmy stworzenie konta na conversations.im¹; dostawcy specjalnie dostosowanego do pracy z Conversations. + XMPP to niezależna od dostawcy sieć komunikacji błyskawicznej. Możesz użyć tego klienta z dowolnym serwerem XMPP.\nDla twojej wygody jednak ułatwiliśmy stworzenie konta na conversations.im; dostawcy specjalnie dostosowanego do pracy z Conversations. Zostałeś zaproszony do %1$s. Poprowadzimy ciebie przez proces tworzenia konta.\nWybierając %1$s jako dostawcę będziesz mógł komunikować się z innymi użytkownikami podając swój pełny adres XMPP. Zostałeś zaproszony do %1$s. Nazwa użytkownika została już dla ciebie wybrana. Poprowadzimy ciebie przez proces tworzenia konta.\nBęziesz mógł komunikować się z innymi użytkownikami podając swój adres XMPP. Zaproszenie twojego serwera diff --git a/src/conversations/res/values-pt-rBR/strings.xml b/src/conversations/res/values-pt-rBR/strings.xml index 1d51c86b6..0a4b54191 100644 --- a/src/conversations/res/values-pt-rBR/strings.xml +++ b/src/conversations/res/values-pt-rBR/strings.xml @@ -4,7 +4,7 @@ Usar o conversations.im Criar uma nova conta Você já possui uma conta XMPP? Esse pode ser o seu caso caso já esteja usando um outro cliente XMPP ou tenha usado o Conversations antes. Caso contrário, você pode criar uma nova conta XMPP agora.\nDica: alguns provedores de e-mail também fornecem contas XMPP. - O XMPP é uma rede de mensageria instantânea independente de provedor. Você pode usar esse cliente com qualquer servidor XMPP que você escolher.\nEntretanto, para sua conveniência, nós simplificamos o processo de criação de uma conta em conversations.im¹, um provedor especialmente configurado para se usar com o Conversations. + O XMPP é uma rede de mensageria instantânea independente de provedor. Você pode usar esse cliente com qualquer servidor XMPP que você escolher.\nEntretanto, para sua conveniência, nós simplificamos o processo de criação de uma conta em conversations.im, um provedor especialmente configurado para se usar com o Conversations. Você foi convidado para %1$s. Nós iremos guiá-lo ao longo do processo de criação de uma conta.\nAo escolher %1$s como um provedor você conseguirá se comunicar com usuários de outros provedores dando a eles seu endereço XMPP completo. Você foi convidado para %1$s. Um nome de usuário já foi escolhido para você. Nós iremos guiá-lo ao longo do processo de criação de uma conta.\nVocê conseguirá se comunicar com usuários de outros provedores dando a eles seu endereço XMPP completo. Seu convite do servidor diff --git a/src/conversations/res/values-ro-rRO/strings.xml b/src/conversations/res/values-ro-rRO/strings.xml index 232b7d9f1..baefb00c6 100644 --- a/src/conversations/res/values-ro-rRO/strings.xml +++ b/src/conversations/res/values-ro-rRO/strings.xml @@ -4,7 +4,7 @@ Folosește conversations.im Creează un cont nou Aveți deja un cont XMPP? S-ar putea să fie așa dacă deja utilizați un alt client XMPP sau dacă ați folosit Conversations în trecut. Dacă nu, puteți crea un cont nou XMPP chiar acum.\nIdee: Unii furnizori de e-mail oferă de asemenea și conturi XMPP. - XMPP este o rețea de mesagerie instant ce nu depinde de un anumit furnizor. Aveți posibilitatea să utilizați acest client cu orice server XMPP doriți.\nTotuși, pentru confortul dumneavoastră, am facilitat crearea unui cont pe conversations.im¹; un furnizor potrivit pentru utilizarea cu aplicația Conversations. + XMPP este o rețea de mesagerie instant ce nu depinde de un anumit furnizor. Aveți posibilitatea să utilizați acest client cu orice server XMPP doriți.\nTotuși, pentru confortul dumneavoastră, am facilitat crearea unui cont pe conversations.im; un furnizor potrivit pentru utilizarea cu aplicația Conversations. Ați fost invitați la %1$s. Vă vom ghida prin procesul de creare al unui cont.\nCând alegeți %1$s ca furnizor veți putea comunica cu utilizatorii altor furnizori oferindu-le adresa dumneavoastră completă XMPP. Ați fost invitați la %1$s. Un nume de utilizator a fost deja ales pentru dumneavoastră. Vă vom ghida prin procesul de creare al unui cont.\nVeți putea comunica cu utilizatorii altor furnizori oferindu-le adresa dumneavoastră completă XMPP. Invitația serverului dumneavoastră diff --git a/src/conversations/res/values-sk/strings.xml b/src/conversations/res/values-sk/strings.xml index 4897be11a..ed58bbefb 100644 --- a/src/conversations/res/values-sk/strings.xml +++ b/src/conversations/res/values-sk/strings.xml @@ -4,7 +4,7 @@ Použiť conversations.im Vytvoriť nové konto Máte už svoje XMPP konto? Môže to tak byť v prípade, že už používate iného klienta XMPP alebo ste predtým používali Conversations. Ak nie, môžete si vytvoriť nové XMPP konto práve teraz.\nHint: Niektorí poskytovatelia emailu zároveň poskytujú aj XMPP kontá. - XMPP je sieť pre okamžité správy nezávislá od poskytovateľa. Tohto klienta môžete používať s akýmkoľvek XMPP serverom, ktorý si vyberiete..\nAvšak pre vaše pohodlie sme zjednodušili vytvorenie konta na conversations.im¹; poskytovateľ špeciálne vhodný na používanie s Conversations. + XMPP je sieť pre okamžité správy nezávislá od poskytovateľa. Tohto klienta môžete používať s akýmkoľvek XMPP serverom, ktorý si vyberiete..\nAvšak pre vaše pohodlie sme zjednodušili vytvorenie konta na conversations.im; poskytovateľ špeciálne vhodný na používanie s Conversations. Boli ste pozvaný do %1$s. Prevedieme vás procesom vytvorenia konta..\nPo výbere %1$s ako poskytovateľa, budete môcť komunikovať s užívateľmi iných poskytovateľov tak, že im dáte vašu úplnú XMPP adresu. Boli ste pozvaný do %1$s . Užívateľské meno vám už bolo vopred vybrané. Prevedieme vás procesom vytvorenia konta..\nBudete môcť komunikovať s užívateľmi iných poskytovateľov tak, že im dáte vašu úplnú XMPP adresu. Ťuknite na tlačidlo zdieľať na odoslanie pozvánky do %1$s vášmu kontaktu. diff --git a/src/conversations/res/values-sr/strings.xml b/src/conversations/res/values-sr/strings.xml index b9e417ed4..e668ed7e6 100644 --- a/src/conversations/res/values-sr/strings.xml +++ b/src/conversations/res/values-sr/strings.xml @@ -4,6 +4,6 @@ Користи conversations.im Направи нови налог Да ли већ имате ИксМПП налог? Извесно је да га имате ако користите неки ИксМПП клијент или сте раније користили Конверзацију. Ако немате, сада можете направити нови ИксМПП налог.\nСавет: неки поштански провајдери такође омогућавају и ИксМПП налоге. - ИксМПП је мрежа брзих порука, независна од провајдера. Овај клијент можете користити уз било који сервер по вашем избору.\nДа бисмо вам олакшали, омогућили смо креирање налога на conversations.im¹; провајдеру специјално прилаг.ођеном за коришћење уз Конверзацију + ИксМПП је мрежа брзих порука, независна од провајдера. Овај клијент можете користити уз било који сервер по вашем избору.\nДа бисмо вам олакшали, омогућили смо креирање налога на conversations.im; провајдеру специјално прилаг.ођеном за коришћење уз Конверзацију Ваша серверска позивница \ No newline at end of file diff --git a/src/conversations/res/values-tr-rTR/strings.xml b/src/conversations/res/values-tr-rTR/strings.xml index 43b327ef1..6fb383cf7 100644 --- a/src/conversations/res/values-tr-rTR/strings.xml +++ b/src/conversations/res/values-tr-rTR/strings.xml @@ -4,7 +4,7 @@ conversations.im kullan Yeni hesap oluştur Zaten bir XMPP hesabınız var mı? Bunun sebebi, zaten başka bir XMPP istemcisi kullanıyor oluşunuz veya Conversations\'ı önceden kullanmış olmanız olabilir. Eğer durum bu değilse şimdi yeni bir XMPP hesabı oluşturabilirsiniz.\nİpucu: Bağzı e-posta sağlayıcıları da XMPP hesapları kullanabilir. - XMPP; anlık yazışmalar için bağımsız bir sağlayıcıdır. Bu istemciyi istediğiniz herhangi bir XMPP sunucusu ile birlikte kullanabilirsiniz.\nAncak kullanım rahatlığı adına sizin için conversations.im¹; Conversations için özellikle tasarlanmış bir sağlayıcıda hesap açmanızı kolaylaştırdık. + XMPP; anlık yazışmalar için bağımsız bir sağlayıcıdır. Bu istemciyi istediğiniz herhangi bir XMPP sunucusu ile birlikte kullanabilirsiniz.\nAncak kullanım rahatlığı adına sizin için conversations.im; Conversations için özellikle tasarlanmış bir sağlayıcıda hesap açmanızı kolaylaştırdık. %1$s sağlayıcısına davet edildiniz. Sizi hesap oluşturulması konusunda yönlendireceğiz.\n%1$s bir sağlayıcı olark seçildiğinde, başka sağlayıcılar kullanan kullanıcılarla, onlara tam XMPP adresinizi vererek iletişim kurabileceksiniz. %1$s sağlayıcısına davet edildiniz. Sizin için zaten bir kullanıcı adı seçildi. Sizi hesap oluşturulması konusunda yönlendireceğiz.\nBaşka sağlayıcılar kullanan kullanıcılarla, onlara tam XMPP adresinizi vererek iletişim kurabileceksiniz. Sunucu davetiyeniz diff --git a/src/conversations/res/values-uk/strings.xml b/src/conversations/res/values-uk/strings.xml index f618c0cea..3b855ab5c 100644 --- a/src/conversations/res/values-uk/strings.xml +++ b/src/conversations/res/values-uk/strings.xml @@ -4,7 +4,7 @@ Скористатися conversations.im Створити новий обліковий запис Вже маєте обліковий запис XMPP? Можливо, користуєтеся іншою програмою XMPP або користувалися цією програмою раніше. Якщо ні, можете створити новий обліковий запис XMPP просто зараз.\nЗверніть увагу, що деякі постачальники електронної пошти у той же час надають облікові записи XMPP. - XMPP — це мережа обміну повідомленнями, незалежна від постачальників. Можете використовувати цю програму з будь-яким XMPP сервером, який оберете.\nПроте, для зручності, ми спростили створення облікового запису на conversations.im¹ — у постачальника, який спеціально налаштований на роботу з цією програмою. + XMPP — це мережа обміну повідомленнями, незалежна від постачальників. Можете використовувати цю програму з будь-яким XMPP сервером, який оберете.\nПроте, для зручності, ми спростили створення облікового запису на conversations.im — у постачальника, який спеціально налаштований на роботу з цією програмою. Вас запросили до %1$s. Ми проведемо вас крок за кроком, щоб створити обліковий запис.\nОбираючи %1$s в якості свого постачальника, ви зможете спілкуватися з користувачами інших постачальників, для цього повідомте їм свою повну адресу XMPP. Вас запросили до %1$s. Для вас створено ім\'я користувача. Ми проведемо вас крок за кроком, щоб створити обліковий запис.\nВи зможете спілкуватися з користувачами інших постачальників, для цього повідомите їм свою повну адресу XMPP. Ваше запрошення до сервера diff --git a/src/conversations/res/values-vi/strings.xml b/src/conversations/res/values-vi/strings.xml index ff010bd3e..f80ceacf8 100644 --- a/src/conversations/res/values-vi/strings.xml +++ b/src/conversations/res/values-vi/strings.xml @@ -4,7 +4,7 @@ Sử dụng conversations.im Tạo tài khoản mới Bạn đã có tài khoản XMPP chưa? Điều này có thể đúng nếu bạn đang dùng một ứng dụng khách cho XMPP khác hoặc đã sử dụng Conversations trước đó. Nếu không, bạn có thể tạo tài khoản XMPP mới ngay bây giờ.\nGợi ý: Một số nhà cung cấp email cũng cung cấp tài khoản XMPP. - XMPP là một mạng nhắn tin ngay lập tức không phụ thuộc vào nhà cung cấp. Bạn có thể sử dụng ứng dụng khách này với bất kỳ máy chủ XMPP nào mà bạn chọn.\nTuy nhiên, vì sự thuận tiện của bạn, chúng tôi đã làm cho việc tạo tài khoản trên conversations.im¹ được dễ dàng; một nhà cung cấp đặc biệt phù hợp với việc sử dụng Conversations. + XMPP là một mạng nhắn tin ngay lập tức không phụ thuộc vào nhà cung cấp. Bạn có thể sử dụng ứng dụng khách này với bất kỳ máy chủ XMPP nào mà bạn chọn.\nTuy nhiên, vì sự thuận tiện của bạn, chúng tôi đã làm cho việc tạo tài khoản trên conversations.im được dễ dàng; một nhà cung cấp đặc biệt phù hợp với việc sử dụng Conversations. Bạn đã được mời vào %1$s. Chúng tôi sẽ hướng dẫn bạn trong quá trình tạo tài khoản.\nKhi chọn %1$s là nhà cung cấp, bạn sẽ có thể giao tiếp với những người dùng của các nhà cung cấp khác bằng cách đưa cho họ địa chỉ XMPP đầy đủ của bạn. Bạn đã được mời vào %1$s. Một tên người dùng đã được chọn sẵn cho bạn. Chúng tôi sẽ hướng dẫn bạn trong quá trình tạo tài khoản.\nBạn sẽ có thể giao tiếp với những người dùng của các nhà cung cấp khác bằng cách đưa cho họ địa chỉ XMPP đầy đủ của bạn. Lời mời vào máy chủ của bạn diff --git a/src/conversations/res/values-zh-rCN/strings.xml b/src/conversations/res/values-zh-rCN/strings.xml index 8fc58fe8f..39254955a 100644 --- a/src/conversations/res/values-zh-rCN/strings.xml +++ b/src/conversations/res/values-zh-rCN/strings.xml @@ -4,7 +4,7 @@ 使用conversations.im 创建新账户 您已经拥有一个XMPP账户了吗?如果您之前使用过其他的XMPP客户端的话,那么您已经拥有这种账户了。如果没有账户的话,您可以现在创建一个。\n提示:有些电子邮件服务也提供XMPP账户。 - XMPP是独立于提供程序的即时消息网络。 您可以将此客户端与所选的任何XMPP服务器一起使用。\ n不过,为了您的方便,我们很容易在对话中创建帐户。im¹; 特别适合与“对话”配合使用的提供商。 + XMPP是独立于提供程序的即时消息网络。 您可以将此客户端与所选的任何XMPP服务器一起使用。\ n不过,为了您的方便,我们很容易在对话中创建帐户。im; 特别适合与“对话”配合使用的提供商。 您已受邀参加%1$s。 我们将指导您完成创建帐户的过程。\n选择%1$s作为提供者后,您可以通过提供其他人的完整XMPP地址与其他提供者的用户进行交流。 您已受邀参加%1$s。 已经为您选择了一个用户名。 我们将指导您完成创建帐户的过程。\n您可以通过向其他提供商的用户提供完整的XMPP地址来与他们进行交流。 你的服务器邀请 From 93c591634684b31660d852a92b5ee576a52ca9eb Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 28 Mar 2022 10:12:58 +0200 Subject: [PATCH 090/394] bump version code --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f1b134848..b15db6067 100644 --- a/build.gradle +++ b/build.gradle @@ -90,7 +90,7 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 30 - versionCode 42027 + versionCode 42028 versionName "2.10.4" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" From 7e762eb799abe0d4f172d04eb714b97e838a8b1f Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 30 Mar 2022 09:03:19 +0200 Subject: [PATCH 091/394] ensure downloaded file does not exceed Content-Length reported by HEAD --- .../http/HttpDownloadConnection.java | 23 ++++++++++++++++--- src/main/res/values/strings.xml | 1 + 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java index 5623c0be7..31ba810a4 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java @@ -102,11 +102,15 @@ public void init(boolean interactive) { if (this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL && this.file.getKey() == null) { this.message.setEncryption(Message.ENCRYPTION_NONE); } - //TODO add auth tag size to knownFileSize final Long knownFileSize = message.getFileParams().size; Log.d(Config.LOGTAG,"knownFileSize: "+knownFileSize+", body="+message.getBody()); if (knownFileSize != null && interactive) { - this.file.setExpectedSize(knownFileSize); + if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL + && this.file.getKey() != null) { + this.file.setExpectedSize(knownFileSize + 16); + } else { + this.file.setExpectedSize(knownFileSize); + } download(true); } else { checkFileSize(interactive); @@ -216,6 +220,8 @@ private void showToastForException(final Exception e) { mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_connect); } else if (e instanceof FileWriterException) { mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_write_file); + } else if (e instanceof InvalidFileException) { + mXmppConnectionService.showErrorToastInUi(R.string.download_failed_invalid_file); } else { mXmppConnectionService.showErrorToastInUi(R.string.download_failed_file_not_found); } @@ -428,9 +434,12 @@ private void download() throws Exception { transmitted += count; try { outputStream.write(buffer, 0, count); - } catch (IOException e) { + } catch (final IOException e) { throw new FileWriterException(file); } + if (transmitted > expected) { + throw new InvalidFileException(String.format("File exceeds expected size of %d", expected)); + } updateProgress(Math.round(((double) transmitted / expected) * 100)); } outputStream.flush(); @@ -458,4 +467,12 @@ private static void throwOnInvalidCode(final Response response) throws IOExcepti throw new IOException(String.format(Locale.ENGLISH, "HTTP Status code was %d", code)); } } + + private static class InvalidFileException extends IOException { + + private InvalidFileException(final String message) { + super(message); + } + + } } diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 8b5e67eb2..20c7cbef8 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -463,6 +463,7 @@ Download failed: File not found Download failed: Could not connect to host Download failed: Could not write file + Download failed: Invalid file Tor network unavailable Bind failure The server is not responsible for this domain From 09cf5feefa3a8a1ab21c84cb2208075ef216fc4a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 30 Mar 2022 09:25:05 +0200 Subject: [PATCH 092/394] limit posh files to 10k --- .../services/MemorizingTrustManager.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java b/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java index b51b8de41..520348943 100644 --- a/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java +++ b/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java @@ -43,6 +43,7 @@ import com.google.common.base.Charsets; import com.google.common.base.Joiner; +import com.google.common.io.ByteStreams; import com.google.common.io.CharStreams; import org.json.JSONArray; @@ -77,6 +78,7 @@ import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; +import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.XmppDomainVerifier; import eu.siacs.conversations.entities.MTMDecision; @@ -391,13 +393,13 @@ private void checkCertTrusted(X509Certificate[] chain, String authType, String d final List fingerprints = getPoshFingerprints(domain); if (hash != null && fingerprints.size() > 0) { if (fingerprints.contains(hash)) { - Log.d("mtm", "trusted cert fingerprint of " + domain + " via posh"); + Log.d(Config.LOGTAG, "trusted cert fingerprint of " + domain + " via posh"); return; } else { - Log.d("mtm", "fingerprint " + hash + " not found in " + fingerprints); + Log.d(Config.LOGTAG, "fingerprint " + hash + " not found in " + fingerprints); } if (getPoshCacheFile(domain).delete()) { - Log.d("mtm", "deleted posh file for " + domain + " after not being able to verify"); + Log.d(Config.LOGTAG, "deleted posh file for " + domain + " after not being able to verify"); } } } @@ -410,7 +412,7 @@ private void checkCertTrusted(X509Certificate[] chain, String authType, String d } } - private List getPoshFingerprints(String domain) { + private List getPoshFingerprints(final String domain) { final List cached = getPoshFingerprintsFromCache(domain); if (cached == null) { return getPoshFingerprintsFromServer(domain); @@ -424,13 +426,13 @@ private List getPoshFingerprintsFromServer(String domain) { } private List getPoshFingerprintsFromServer(String domain, String url, int maxTtl, boolean followUrl) { - Log.d("mtm", "downloading json for " + domain + " from " + url); + Log.d(Config.LOGTAG, "downloading json for " + domain + " from " + url); final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(master); final boolean useTor = QuickConversationsService.isConversations() && preferences.getBoolean("use_tor", master.getResources().getBoolean(R.bool.use_tor)); try { final List results = new ArrayList<>(); final InputStream inputStream = HttpConnectionManager.open(url, useTor); - final String body = CharStreams.toString(new InputStreamReader(inputStream, Charsets.UTF_8)); + final String body = CharStreams.toString(new InputStreamReader(ByteStreams.limit(inputStream,10_000), Charsets.UTF_8)); final JSONObject jsonObject = new JSONObject(body); int expires = jsonObject.getInt("expires"); if (expires <= 0) { @@ -457,7 +459,7 @@ private List getPoshFingerprintsFromServer(String domain, String url, in writeFingerprintsToCache(domain, results, 1000L * expires + System.currentTimeMillis()); return results; } catch (final Exception e) { - Log.d("mtm", "error fetching posh " + e.getMessage()); + Log.d(Config.LOGTAG, "error fetching posh",e); return new ArrayList<>(); } } @@ -495,7 +497,7 @@ private List getPoshFingerprintsFromCache(String domain) { file.delete(); return null; } else { - Log.d("mtm", "posh fingerprints expire in " + (expiresIn / 1000) + "s"); + Log.d(Config.LOGTAG, "posh fingerprints expire in " + (expiresIn / 1000) + "s"); } final List result = new ArrayList<>(); final JSONArray jsonArray = jsonObject.getJSONArray("fingerprints"); @@ -512,7 +514,6 @@ private List getPoshFingerprintsFromCache(String domain) { } private X509Certificate[] getAcceptedIssuers() { - LOGGER.log(Level.FINE, "getAcceptedIssuers()"); return defaultTrustManager == null ? new X509Certificate[0] : defaultTrustManager.getAcceptedIssuers(); } From eadb1e127b81005b8d83a86197e6c71ce0115fcc Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 30 Mar 2022 09:59:42 +0200 Subject: [PATCH 093/394] disable knownFileSize on re-download for pgp encrypted files --- .../siacs/conversations/http/HttpDownloadConnection.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java index 31ba810a4..032496842 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java @@ -102,7 +102,12 @@ public void init(boolean interactive) { if (this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL && this.file.getKey() == null) { this.message.setEncryption(Message.ENCRYPTION_NONE); } - final Long knownFileSize = message.getFileParams().size; + final Long knownFileSize; + if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + knownFileSize = null; + } else { + knownFileSize = message.getFileParams().size; + } Log.d(Config.LOGTAG,"knownFileSize: "+knownFileSize+", body="+message.getBody()); if (knownFileSize != null && interactive) { if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL From 95e3a6769d6cdc08ff86d70fb8cb561974346501 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 30 Mar 2022 18:45:18 +0200 Subject: [PATCH 094/394] retrieve uncompressed file size in HEAD request --- .../siacs/conversations/http/HttpDownloadConnection.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java index 032496842..850c0683d 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java @@ -322,6 +322,7 @@ private long retrieveFileSize() throws IOException { ); final Request request = new Request.Builder() .url(URL.stripFragment(mUrl)) + .addHeader("Accept-Encoding", "identity") .head() .build(); mostRecentCall = client.newCall(request); @@ -347,11 +348,11 @@ private long retrieveFileSize() throws IOException { throw new IOException("Server reported negative file size"); } return size; - } catch (IOException e) { + } catch (final IOException e) { Log.d(Config.LOGTAG, "io exception during HEAD " + e.getMessage()); throw e; - } catch (NumberFormatException e) { - throw new IOException(); + } catch (final NumberFormatException e) { + throw new IOException(e); } } From 4af1fe39c79d4cf05e16776876ac386ceee288dd Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 31 Mar 2022 09:41:55 +0200 Subject: [PATCH 095/394] version bump to 2.10.5 + changelog --- CHANGELOG.md | 5 +++++ build.gradle | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b445c52ce..81c2d9e1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +### Version 2.10.5 + +* Security: Stop downloading files that exceed advertised file size +* Security: Limit POSH files to 10K + ### Version 2.10.4 * Fix interaction with Google Maps Share Location Plugin diff --git a/build.gradle b/build.gradle index b15db6067..766ac5e94 100644 --- a/build.gradle +++ b/build.gradle @@ -90,8 +90,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 30 - versionCode 42028 - versionName "2.10.4" + versionCode 42031 + versionName "2.10.5" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId From 9f3e328f54cc45663cbf895d2d1e73d383de8ca9 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 7 Apr 2022 08:05:12 +0200 Subject: [PATCH 096/394] pulled translations from transifex --- src/main/res/values-de/strings.xml | 3 ++- src/main/res/values-gl/strings.xml | 35 +++++++++++++------------- src/main/res/values-ja/strings.xml | 7 +++++- src/main/res/values-pl/strings.xml | 1 + src/main/res/values-pt-rBR/strings.xml | 1 + src/main/res/values-ro-rRO/strings.xml | 1 + src/main/res/values-ru/strings.xml | 2 +- src/main/res/values-zh-rCN/strings.xml | 1 + 8 files changed, 31 insertions(+), 20 deletions(-) diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index aba514c43..a928266b1 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -463,8 +463,9 @@ Ungültiger Benutzername Download fehlgeschlagen: Server nicht gefunden Download fehlgeschlagen: Datei nicht gefunden - Download fehlgeschlagen: keine Verbindung zum Host + Download fehlgeschlagen: Keine Verbindung zum Host Download fehlgeschlagen: Datei konnte nicht gespeichert werden + Download fehlgeschlagen: Ungültige Datei Tor-Netzwerk nicht verfügbar Verbindungsfehler Der Server ist nicht für diese Domain verantwortlich diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 234d5807e..61687bc0a 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -37,7 +37,7 @@ enviando… - Descifrando a mensaxe. Por favor agarde... + Descifrando a mensaxe. Agarda por favor... Mensaxe cifrado con OpenPGP O alcume xa está en uso Alcume non válido @@ -84,7 +84,7 @@ Erro ao enviar Preparándose para enviar a imaxe Preparándose para enviar imaxes - Compartindo ficheiros. Por favor agarde... + Compartindo ficheiros. Agarda por favor... Baleirar historial Eliminar historial da conversa ¿Queres eliminar as mensaxes desta conversa?\n\nAviso: Esto non lle afecta as mensaxes gardadas noutros dispositivos ou servidores. @@ -105,7 +105,7 @@ OpenKeychain para cifrar e descifrar as mensaxes e xestionar a túas chaves públicas.

Está baixo licenza GPLv3+ e dispoñible en F-Droid e Google Play.

(Reinicia %1$s após a instalación.)]]>
Reiniciar Instalar - Por favor instale OpenKeychain + Instala OpenKeychain por favor ofrecendo… agardando... Clave OpenPGP non atopada @@ -254,10 +254,10 @@ Saír Contacto engadido a túa lista de contactos Volver a engadir - %s leu ata este punto - %s leu ate este punto - %1$s + %2$d outras leron ata este punto - Todas leron ate este punto + %s leu até aquí + %s leron até aquí + %1$s + %2$d outras leron até aquí + Todas leron até aquí Publicar Toca no avatar para escoller a imaxe na galería Publicando... @@ -459,17 +459,18 @@ Enviar mensaxe privada %1$s deixou a conversa en grupo Identificador - Nome de usuaria + Identificador Este non é un identificador válido Fallou a descarga: non se atopou o servidor Fallou a descarga: non se atopou o ficheiro Fallou a descarga: Non se puido conectar ao servidor Fallou a descarga: non se escribeu o ficheiro + Fallou a descarga: ficheiro non válido Sen acceso a rede Tor Fallou a ligazón O servidor non corresponde a este dominio Roto - Disponibilidade + Dispoñibilidade Ausente cando o dispositivo está bloqueado Mostrar como Ausente cando o dispositivo está bloqueado En modolo silencioso, Ocupado @@ -544,8 +545,8 @@ O teu enderezo XMPP completo será: %s Crear conta Utilizar o meu propio proveedor - Elixe un identificador - Xestionar a disponibilidade manualmente + Elixe un nome de usuaria + Xestionar a dispoñibilidade manualmente Configura a túa dispoñibilidade ao editar a mensaxe de estado. Mensaxe de estado Dispoñible para conversar @@ -743,7 +744,7 @@ Copiar enderezo XMPP Compartición de ficheiro HTTP para S3 Busca directa - Na pantalla \'Iniciar Conversa\' abrir teclado e por o cursor no campo de busca + Na pantalla \'Iniciar Conversa\' abrir teclado e pór o cursor no campo de busca Avatar da conversa de grupo O servidor non soporta o avatar na conversa de grupo Só o dono pode cambiar o avatar da conversa de grupo @@ -794,13 +795,13 @@ Validar %s %s.]]> Enviamosche outro SMS cun código de 6 díxitos. - Por favor, introduza o pin de 6 díxitos inferior. + Escribe o PIN de 6 díxitos. Reenviar SMS Reenviar SMS (%s) - Agarde por favor (%s) + Agarda por favor (%s) atrás Copiado automático desde o portapapeis. - Por favor, introduza o pin de 6 díxitos. + Por favor, escribe o pin de 6 díxitos. Seguro que quere cancelar o proceso de rexistro? Si Non @@ -852,8 +853,8 @@ Nome da canle Enderezo XMPP Por favor, escribe un nome para a canle - Por favor, proporcione un enderezo XMPP - Esto é un enderezo XMPP. Por favor, proporcione un nome. + Por favor, escribe un enderezo XMPP + Esto é un enderezo XMPP. Por favor, escribe un nome. Creando canle pública... Esta canle xa existe Entraches nunha canle existente diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml index f94eb51e4..452ef18f8 100644 --- a/src/main/res/values-ja/strings.xml +++ b/src/main/res/values-ja/strings.xml @@ -615,6 +615,8 @@ ファイルが保存されているプライベートストレージを消去します (サーバーから再ダウンロードできます) 信頼できるソースからこのリンクをたどりました リンクをクリックした後、%1$s の OMEMO 鍵を検証しようとしています。 これは、%2$s がこのリンクを公開した、信頼できるソースからこのリンクをたどった場合にのみ安全です。 + あなたが所有するアカウントの OMEMO 鍵を検証しようとしています。 これは、あなたがこのリンクを公開した、信頼できるソースからこのリンクをたどった場合にのみ安全です。 + 続行 OMEMO 鍵を検証 非アクティブを表示 非アクティブを非表示 @@ -950,4 +952,7 @@ バックアップを開始しました。 バックアップが完了すると通知が届きます。 映像を有効化できません。 プレーンテキスト文書 - + アカウント登録はサポートされていません + XMPPアドレスがみつかりません + + diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index b1c0db084..b3ce11037 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -473,6 +473,7 @@ Pobieranie nieudane: Nie odnaleziono pliku Pobieranie nieudane: Nie można połączyć z hostem Pobieranie niepowiodło się: brak możliwości zapisu pliku + Pobieranie nieudane: Nieprawidłowy plik Sieć TOR jest niedostepna Błąd połączenia (zasób) Serwer nie odpowiada domenie diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index 2fbbc4809..79d6ea798 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -465,6 +465,7 @@ Não foi possível fazer o download: arquivo não encontrado Não foi possível fazer o download: não foi possível conectar ao host Falha no download: não foi possível salvar o arquivo + Falha no download: arquivo inválido Rede Tor não disponível Falha na associação O servidor não responde por esse domínio diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index 253c97ad1..1ed0d4caa 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -469,6 +469,7 @@ Descărcare eșuată: Fișierul nu a fost găsit Descărcare eșuată: Nu s-a putut realiza conexiunea cu gazda. Descărcare eșuată: Nu s-a putut scrie fișierul + Descărcarea a eșuat: Fișier invalid Rețeaua Tor nu este disponibilă Eroare de conexiune Serverul nu este responsabil pentru acest domeniu diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index d9d728049..4e4880dd6 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -420,7 +420,7 @@ Показывать клавишу ввода Поменять кнопку смайликов на кнопку ввода аудио - звук + видео изображение векторная графика PDF-документ diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index fe11961a4..6c11f159d 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -461,6 +461,7 @@ 下载失败:未找到文件 下载失败:无法连接到服务器 下载失败:不能写入文件 + 下载失败:无效文件 Tor网络不可用 绑定失败 服务器不能为域名做出响应 From ec02e8a198159e08673e8cd918c72ad09785b587 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 7 Apr 2022 10:47:19 +0200 Subject: [PATCH 097/394] work around platform bug when getting restrict background fixes #4305 --- .../eu/siacs/conversations/ui/XmppActivity.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 4ca49fa50..6ac8f7279 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -46,6 +46,7 @@ import androidx.annotation.BoolRes; import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; import androidx.annotation.StringRes; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog.Builder; @@ -447,12 +448,22 @@ protected boolean isAffectedByDataSaver() { final ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); return cm != null && cm.isActiveNetworkMetered() - && cm.getRestrictBackgroundStatus() == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED; + && getRestrictBackgroundStatus(cm) == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED; } else { return false; } } + @RequiresApi(api = Build.VERSION_CODES.N) + private static int getRestrictBackgroundStatus(@NonNull final ConnectivityManager connectivityManager) { + try { + return connectivityManager.getRestrictBackgroundStatus(); + } catch (final Exception e) { + Log.d(Config.LOGTAG,"platform bug detected. Unable to get restrict background status",e); + return -1; + } + } + private boolean usingEnterKey() { return getBooleanPreference("display_enter_key", R.bool.display_enter_key); } From bf8afe0396adb5b67f0f1df7e05beff356c6e5a2 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 8 Apr 2022 15:54:53 +0200 Subject: [PATCH 098/394] check domain name against DNSName to avoid rare crashes --- .../java/eu/siacs/conversations/ui/EditAccountActivity.java | 1 + src/main/java/eu/siacs/conversations/utils/Resolver.java | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 921756a70..e9c0bce39 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -208,6 +208,7 @@ public void onClick(final View v) { jid = Jid.ofEscaped(binding.accountJid.getText().toString(), getUserModeDomain(), null); } else { jid = Jid.ofEscaped(binding.accountJid.getText().toString()); + Resolver.checkDomain(jid); } } catch (final NullPointerException | IllegalArgumentException e) { if (mUsernameMode) { diff --git a/src/main/java/eu/siacs/conversations/utils/Resolver.java b/src/main/java/eu/siacs/conversations/utils/Resolver.java index a3796b253..463d6eb73 100644 --- a/src/main/java/eu/siacs/conversations/utils/Resolver.java +++ b/src/main/java/eu/siacs/conversations/utils/Resolver.java @@ -37,6 +37,7 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xmpp.Jid; public class Resolver { @@ -84,6 +85,10 @@ public static List fromHardCoded(final String hostname, final int port) return Collections.singletonList(result); } + public static void checkDomain(final Jid jid) { + DNSName.from(jid.getDomain()); + } + public static boolean invalidHostname(final String hostname) { try { DNSName.from(hostname); From eb9f6653ad56ee1bb35e0dc3c613f8df9c227035 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 8 Apr 2022 15:55:16 +0200 Subject: [PATCH 099/394] null check axolotl service when getting trust --- src/main/java/eu/siacs/conversations/entities/Message.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index aa197aa44..fa1819124 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -20,6 +20,7 @@ import java.util.concurrent.CopyOnWriteArraySet; import eu.siacs.conversations.Config; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; import eu.siacs.conversations.http.URL; import eu.siacs.conversations.services.AvatarService; @@ -917,7 +918,8 @@ public String getFingerprint() { } public boolean isTrusted() { - FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint); + final AxolotlService axolotlService = conversation.getAccount().getAxolotlService(); + final FingerprintStatus s = axolotlService != null ? axolotlService.getFingerprintTrust(axolotlFingerprint) : null; return s != null && s.isTrusted(); } From e3cae4cb1d4bb72e53ca103f53cae5924c6d0aa1 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 11 Apr 2022 10:58:37 +0200 Subject: [PATCH 100/394] bump agp --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 766ac5e94..85d62121c 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.1.2' + classpath 'com.android.tools.build:gradle:7.1.3' } } From d7637192e2254392cc0870c63a0a4d4515720e61 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 21 Apr 2022 17:01:06 +0200 Subject: [PATCH 101/394] fix NPE during bookmark creation closes #4312 fixes #4211 thank you @singpolyma --- .../siacs/conversations/services/XmppConnectionService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 42b699e46..0f121b0b3 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1793,7 +1793,9 @@ public void processModifiedBookmark(Bookmark bookmark) { public void createBookmark(final Account account, final Bookmark bookmark) { account.putBookmark(bookmark); final XmppConnection connection = account.getXmppConnection(); - if (connection.getFeatures().bookmarks2()) { + if (connection == null) { + Log.d(Config.LOGTAG, account.getJid().asBareJid()+": no connection. ignoring bookmark creation"); + } else if (connection.getFeatures().bookmarks2()) { final Element item = mIqGenerator.publishBookmarkItem(bookmark); pushNodeAndEnforcePublishOptions(account, Namespace.BOOKMARKS2, item, bookmark.getJid().asBareJid().toEscapedString(), PublishOptions.persistentWhitelistAccessMaxItems()); } else if (connection.getFeatures().bookmarksConversion()) { From 544b46ffe1a19bb263a8ff268b89518566bb5627 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 21 Apr 2022 17:04:01 +0200 Subject: [PATCH 102/394] Revert "flush stanzas in batches" This reverts commit 6bd552f6a32ca93826cb491f9b4bd757f9698227. fixes #4313 This turned out to be a rather unnecessary optimization that might cause problems with wake locks (the app is no longer awake after the 400ms timeout) --- .../eu/siacs/conversations/xml/TagWriter.java | 22 ++++--------------- 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xml/TagWriter.java b/src/main/java/eu/siacs/conversations/xml/TagWriter.java index 2c2b8ac2c..4f429377a 100644 --- a/src/main/java/eu/siacs/conversations/xml/TagWriter.java +++ b/src/main/java/eu/siacs/conversations/xml/TagWriter.java @@ -8,15 +8,12 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; import eu.siacs.conversations.Config; import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; public class TagWriter { - private static final int FLUSH_DELAY = 400; - private OutputStreamWriter outputStream; private boolean finished = false; private final LinkedBlockingQueue writeQueue = new LinkedBlockingQueue(); @@ -24,8 +21,6 @@ public class TagWriter { private final Thread asyncStanzaWriter = new Thread() { - private final AtomicInteger batchStanzaCount = new AtomicInteger(0); - @Override public void run() { stanzaWriterCountDownLatch = new CountDownLatch(1); @@ -34,21 +29,12 @@ public void run() { break; } try { - final AbstractStanza stanza = writeQueue.poll(FLUSH_DELAY, TimeUnit.MILLISECONDS); - if (stanza != null) { - batchStanzaCount.incrementAndGet(); - outputStream.write(stanza.toString()); - } else { - final int batch = batchStanzaCount.getAndSet(0); - if (batch > 1) { - Log.d(Config.LOGTAG, "flushing " + batch + " stanzas"); - } + AbstractStanza output = writeQueue.take(); + outputStream.write(output.toString()); + if (writeQueue.size() == 0) { outputStream.flush(); - final AbstractStanza nextStanza = writeQueue.take(); - batchStanzaCount.incrementAndGet(); - outputStream.write(nextStanza.toString()); } - } catch (final Exception e) { + } catch (Exception e) { break; } } From 3274baee950f70cc6050344cda369502f5529265 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 21 Apr 2022 17:11:55 +0200 Subject: [PATCH 103/394] pulled translations from transifex --- src/main/res/values-gl/strings.xml | 10 +++++----- src/main/res/values-tr-rTR/strings.xml | 11 ++++++++++- src/quicksy/res/values-gl/strings.xml | 2 +- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 61687bc0a..419dcf82b 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -151,7 +151,7 @@ Non se puido converter o ficheiro de imaxe Arquivo non atopado Erro xeral de I/O. ¿Quedaches sen espazo no disco? - A app utilizada para seleccionar esta imaxe non deu permisos suficientes para ler o ficheiro.\n\nUsa un xestor de ficheiros diferente para elexir a imaxe + A app utilizada para seleccionar esta imaxe non deu permisos suficientes para ler o ficheiro.\n\nUsa un xestor de ficheiros diferente para escoller a imaxe A app que usaches para compartir este ficheiro non concedeu os permisos suficientes. Descoñecido Desactivado temporalmente @@ -259,7 +259,7 @@ %1$s + %2$d outras leron até aquí Todas leron até aquí Publicar - Toca no avatar para escoller a imaxe na galería + Toca no avatar para elixir a imaxe na galería Publicando... O servidor rexeitou a túa publicación Non se puido converter a imaxe @@ -453,7 +453,7 @@ Acción rápida Ningunha Utilizadas recentemente - Escolle a acción rápida + Elixe a acción rápida Buscar contactos Buscar marcadores Enviar mensaxe privada @@ -569,7 +569,7 @@ Permitelle aos teus contactos saber cando estás a usar Conversations Privacidade Decorado - Escolle a gama de cores + Elixe a gama de cores Automático Claro Escuro @@ -837,7 +837,7 @@ Orixinal (non comprimido) Abrir con... Imaxe de perfil en Conversations - Elexir conta + Elixir conta Restablecer copia de apoio Restablecer Escribe o contrasinal da conta %s para restablecer a copia. diff --git a/src/main/res/values-tr-rTR/strings.xml b/src/main/res/values-tr-rTR/strings.xml index f3e135b6b..266255aed 100644 --- a/src/main/res/values-tr-rTR/strings.xml +++ b/src/main/res/values-tr-rTR/strings.xml @@ -465,6 +465,7 @@ İndirme başarısız: Dosya bulunamadı İndirme başarısız: Sunucuya bağlanılamadı İndirme başarısız: Dosya yazılamıyor + İndirme başarısız: Geçersiz dosya Tor ağına erişilemiyor Bağlantı başarısız Sunucu bu alan adı için sorumlu değil @@ -622,6 +623,8 @@ Dosyaların tutulduğu özel depolama alanını temizle (Sunucu üzerinden tekrar indirilebilir) Bu bağlantıyı güvenilir bir kaynaktan takip ettim Bir bağlantıyı tıkladıktan sonra %1$s in OMEMO anahtarını doğrulamış olacaksınız. Bu yalnızca bağlantının %2$s tarafından yayınladığından eminseniz güvenlidir. + Hesabınızın OMEMO anahtarını doğrulamış olacaksınız. Bu yalnızca bağlantıya yalnızca sizin yayınlayabileceğiniz bir kaynaktan eriştiyseniz güvenlidir. + Devam et OMEMO anahtarlarını doğrula Aktif olmayanları göster Aktif olmayanları sakla @@ -904,6 +907,7 @@ Gelen görüntülü arama Bağlanıyor Bağlandı + Tekrar bağlanılıyor Arama kabül ediliyor Arama sonlandırılıyor Cevapla @@ -919,6 +923,8 @@ Çağrıyı sonlandır Devam eden arama Deaam eden görüntülü arama + Aramaya tekrar bağlanıyılor + Görüntülü aramaya tekrar bağlanılıyor Arama yapmak için Tor\'u devre dışı bırak Gelen arama Gelen arama. %s @@ -968,4 +974,7 @@ Yedekleme başlatıldı. Tamamlandığı zaman bir bildirim alacaksınız. Video etkinleştirilemedi Düz metin dosyası - + Hesap kayıtları desteklenmemektedir. + Herhangi bir XMPP adresi bulunamadı + + diff --git a/src/quicksy/res/values-gl/strings.xml b/src/quicksy/res/values-gl/strings.xml index 0e02425a5..22b78d01b 100644 --- a/src/quicksy/res/values-gl/strings.xml +++ b/src/quicksy/res/values-gl/strings.xml @@ -1,6 +1,6 @@ - O período de tempo que Quicksy permanece acalado tras ver actividade en outro dispositivo + O período de tempo que Quicksy permanece acalado tras ver actividade noutro dispositivo Enviando trazas do rexistro estás axudando ao desenvolvemento de Quicksy Permitir a todos os teus contactos saber cando estás a utilizar Quicksy Para seguir recibindo notificacións, mesmo coa pantalla apagada, precisas engadir a Quicksy na lista de apps protexidas. From cf4e9794317463309b93d154ac237d3e9c28743a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 21 Apr 2022 18:31:53 +0200 Subject: [PATCH 104/394] version bump firebasse-messaging lib --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 85d62121c..86b0314c0 100644 --- a/build.gradle +++ b/build.gradle @@ -33,7 +33,7 @@ configurations { dependencies { implementation 'androidx.viewpager:viewpager:1.0.0' - playstoreImplementation('com.google.firebase:firebase-messaging:23.0.2') { + playstoreImplementation('com.google.firebase:firebase-messaging:23.0.3') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' From 85f06f1cd6a3e019a2ec8c8b664b823bf1989a80 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 2 May 2022 08:29:51 +0200 Subject: [PATCH 105/394] do not merge failed decryptions fixes #4314 --- .../java/eu/siacs/conversations/entities/Message.java | 10 ++++++++-- .../conversations/services/XmppConnectionService.java | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index fa1819124..e50ffc73c 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -14,6 +14,7 @@ import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Set; @@ -633,9 +634,8 @@ public boolean mergeable(final Message message) { message.getEncryption() != Message.ENCRYPTION_PGP && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED && this.getType() == message.getType() && - //this.getStatus() == message.getStatus() && isStatusMergeable(this.getStatus(), message.getStatus()) && - this.getEncryption() == message.getEncryption() && + isEncryptionMergeable(this.getEncryption(),message.getEncryption()) && this.getCounterpart() != null && this.getCounterpart().equals(message.getCounterpart()) && this.edited() == message.edited() && @@ -668,6 +668,12 @@ private static boolean isStatusMergeable(int a, int b) { ); } + private static boolean isEncryptionMergeable(final int a, final int b) { + return a == b + && Arrays.asList(ENCRYPTION_NONE, ENCRYPTION_DECRYPTED, ENCRYPTION_AXOLOTL) + .contains(a); + } + public void setCounterparts(List counterparts) { this.counterparts = counterparts; } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 0f121b0b3..7965a4e31 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -4128,7 +4128,7 @@ public void notifyJingleRtpConnectionUpdate(AppRTCAudioManager.AudioDevice selec } public void updateAccountUi() { - for (OnAccountUpdate listener : threadSafeList(this.mOnAccountUpdates)) { + for (final OnAccountUpdate listener : threadSafeList(this.mOnAccountUpdates)) { listener.onAccountUpdate(); } } From 86bb3df8d4ebd0c2d50422d80479cfeccd813ed7 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 2 May 2022 08:32:58 +0200 Subject: [PATCH 106/394] pulled translations from transifex --- src/main/res/values-it/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index 6fb2bc823..af7c4b93d 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -465,6 +465,7 @@ Scaricamento fallito: file non trovato Scaricamento fallito: impossibile connettersi all\'host Scaricamento fallito: scrittura del file impossibile + Scaricamento fallito: file non valido Rete Tor non disponibile Bind fallito Il server non è responsabile per questo dominio From d1dcc577106a18996aa15e7a43afc636fcf30d0d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 2 May 2022 08:33:27 +0200 Subject: [PATCH 107/394] version bump to 2.10.6 --- CHANGELOG.md | 4 ++++ build.gradle | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81c2d9e1f..304fac6d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### Version 2.10.6 + +* Minor bug fixes + ### Version 2.10.5 * Security: Stop downloading files that exceed advertised file size diff --git a/build.gradle b/build.gradle index 86b0314c0..371b1ea87 100644 --- a/build.gradle +++ b/build.gradle @@ -90,8 +90,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 30 - versionCode 42031 - versionName "2.10.5" + versionCode 42032 + versionName "2.10.6" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId From d5ac6e35fcc62c886ae6cca1e58899eb1cb4f869 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 13 May 2022 08:28:06 +0200 Subject: [PATCH 108/394] bump agp --- build.gradle | 4 ++-- gradle/wrapper/gradle-wrapper.properties | 2 +- src/conversations/AndroidManifest.xml | 3 +-- src/main/AndroidManifest.xml | 3 +-- src/playstore/AndroidManifest.xml | 1 - src/quicksy/AndroidManifest.xml | 3 +-- 6 files changed, 6 insertions(+), 10 deletions(-) diff --git a/build.gradle b/build.gradle index 371b1ea87..2e1870b2f 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.1.3' + classpath 'com.android.tools.build:gradle:7.2.0' } } @@ -85,6 +85,7 @@ ext { } android { + namespace 'eu.siacs.conversations' compileSdkVersion 31 defaultConfig { @@ -229,7 +230,6 @@ android { disable 'MissingTranslation', 'InvalidPackage', 'AppCompatResource' } - android.applicationVariants.all { variant -> variant.outputs.each { output -> def baseAbiVersionCode = project.ext.abiCodes.get(output.getFilter(com.android.build.OutputFile.ABI)) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 162dd9b7f..e639f29f3 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip diff --git a/src/conversations/AndroidManifest.xml b/src/conversations/AndroidManifest.xml index 62396bed1..bf2297949 100644 --- a/src/conversations/AndroidManifest.xml +++ b/src/conversations/AndroidManifest.xml @@ -1,7 +1,6 @@ + xmlns:tools="http://schemas.android.com/tools"> + xmlns:tools="http://schemas.android.com/tools"> diff --git a/src/playstore/AndroidManifest.xml b/src/playstore/AndroidManifest.xml index 6e65b581c..6deb7d2a4 100644 --- a/src/playstore/AndroidManifest.xml +++ b/src/playstore/AndroidManifest.xml @@ -1,6 +1,5 @@ diff --git a/src/quicksy/AndroidManifest.xml b/src/quicksy/AndroidManifest.xml index 1c30d2f28..f82377f01 100644 --- a/src/quicksy/AndroidManifest.xml +++ b/src/quicksy/AndroidManifest.xml @@ -1,7 +1,6 @@ + xmlns:tools="http://schemas.android.com/tools"> Date: Tue, 14 Jun 2022 08:39:55 +0200 Subject: [PATCH 109/394] support sasl/temporary-auth-failure if the server is unable to query the database throwing a temporary-auth-failure might be more appropriate --- .../siacs/conversations/entities/Account.java | 3 +++ .../conversations/xmpp/XmppConnection.java | 23 ++++++++++++------- src/main/res/values/strings.xml | 1 + 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 37f8114e8..dc354adc4 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -627,6 +627,7 @@ public enum State { ONLINE(false), NO_INTERNET(false), UNAUTHORIZED, + TEMPORARY_AUTH_FAILURE, SERVER_NOT_FOUND, REGISTRATION_SUCCESSFUL(false), REGISTRATION_FAILED(true, false), @@ -732,6 +733,8 @@ public int getReadableId() { return R.string.payment_required; case MISSING_INTERNET_PERMISSION: return R.string.missing_internet_permission; + case TEMPORARY_AUTH_FAILURE: + return R.string.account_status_temporary_auth_failure; default: return R.string.account_status_unknown; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index c3a3b1532..06195aaed 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -12,6 +12,8 @@ import androidx.annotation.NonNull; +import com.google.common.base.Strings; + import org.xmlpull.v1.XmlPullParserException; import java.io.ByteArrayInputStream; @@ -489,20 +491,25 @@ private void processStream() throws XmlPullParserException, IOException { } else if (nextTag.isStart("failure")) { final Element failure = tagReader.readElement(nextTag); if (Namespace.SASL.equals(failure.getNamespace())) { - final String text = failure.findChildContent("text"); - if (failure.hasChild("account-disabled") && text != null) { - Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(text); + if (failure.hasChild("temporary-auth-failure")) { + throw new StateChangingException(Account.State.TEMPORARY_AUTH_FAILURE); + } else if (failure.hasChild("account-disabled")) { + final String text = failure.findChildContent("text"); + if ( Strings.isNullOrEmpty(text)) { + throw new StateChangingException(Account.State.UNAUTHORIZED); + } + final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(text); if (matcher.find()) { final HttpUrl url; try { url = HttpUrl.get(text.substring(matcher.start(), matcher.end())); - if (url.isHttps()) { - this.redirectionUrl = url; - throw new StateChangingException(Account.State.PAYMENT_REQUIRED); - } - } catch (IllegalArgumentException e) { + } catch (final IllegalArgumentException e) { throw new StateChangingException(Account.State.UNAUTHORIZED); } + if (url.isHttps()) { + this.redirectionUrl = url; + throw new StateChangingException(Account.State.PAYMENT_REQUIRED); + } } } throw new StateChangingException(Account.State.UNAUTHORIZED); diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 20c7cbef8..ee5cbce81 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -976,5 +976,6 @@ Plain text document Account registrations are not supported No XMPP address found + Temporary authentication failure From 30dff9ac054b1a1baf19025f635d626ff70e99d5 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 14 Jun 2022 08:48:04 +0200 Subject: [PATCH 110/394] pulled translations from transifex --- src/conversations/res/values-nl/strings.xml | 5 +- src/conversations/res/values-szl/strings.xml | 16 + src/main/res/values-da-rDK/strings.xml | 8 + src/main/res/values-de/strings.xml | 3 +- src/main/res/values-el/strings.xml | 3 +- src/main/res/values-es/strings.xml | 21 + src/main/res/values-fr/strings.xml | 24 +- src/main/res/values-gl/strings.xml | 3 +- src/main/res/values-it/strings.xml | 16 +- src/main/res/values-ja/strings.xml | 6 +- src/main/res/values-nl/strings.xml | 19 + src/main/res/values-pl/strings.xml | 3 +- src/main/res/values-pt-rBR/strings.xml | 16 +- src/main/res/values-pt/strings.xml | 2 + src/main/res/values-ro-rRO/strings.xml | 3 +- src/main/res/values-szl/strings.xml | 1025 ++++++++++++++++++ src/main/res/values-tr-rTR/strings.xml | 3 +- src/main/res/values-zh-rCN/strings.xml | 3 +- src/quicksy/res/values-szl/strings.xml | 12 + 19 files changed, 1168 insertions(+), 23 deletions(-) create mode 100644 src/conversations/res/values-szl/strings.xml create mode 100644 src/main/res/values-szl/strings.xml create mode 100644 src/quicksy/res/values-szl/strings.xml diff --git a/src/conversations/res/values-nl/strings.xml b/src/conversations/res/values-nl/strings.xml index 968a276d1..f04a6b2de 100644 --- a/src/conversations/res/values-nl/strings.xml +++ b/src/conversations/res/values-nl/strings.xml @@ -8,4 +8,7 @@ Je ontving een uitnodiging voor %1$s. We zullen je helpen een account aan te maken.\nWanneer je %1$s als je provider kiest kan je met gebruikers van andere providers communiceren door hen je volledige XMPP-adres te geven. Je ontving een uitnodiging voor %1$s. Er werd reeds een gebruikersnaam voor jou gekozen. We zullen je helpen een account aan te maken.\nJe zal met gebruikers van andere providers communiceren door hen je volledige XMPP-adres te geven. Je server uitnodiging - \ No newline at end of file + Tik op de delen knop om een uitnodiging te versturen naar %1$s + Als je contactpersoon in de buurt is, kan deze ook onderstaande code scannen om de uitnodiging te aanvaarden. + Deel de uitnodiging met ... + \ No newline at end of file diff --git a/src/conversations/res/values-szl/strings.xml b/src/conversations/res/values-szl/strings.xml new file mode 100644 index 000000000..6e0134d06 --- /dev/null +++ b/src/conversations/res/values-szl/strings.xml @@ -0,0 +1,16 @@ + + + Wybier liferanta XMPP + Użyj conversations.im + Stwōrz nowe kōnto + Mosz już kōnto XMPP? Tak może być, jeźli już używosz inkszego klijynta XMPP aboś używoł abo używała wcześnij Conversations. Jak niy, to możesz stworzić teroz nowe kōnto XMPP.\nDorada: Niykerzi liferańcio emaili dowajōm tyż kōnta XMPP. + XMPP to je nec wartkich wiadōmości niyzależny ôd liferanta. Możesz używać tego klijynta ze serwerym XMPP, jaki sie wybieresz.\nAle dlo twojij wygody ułacniyli my tworzynie kōnt na conversations.im; liferańcie ekstra dopasowanym do używanio ze Conversations. + Mosz zaproszynie na %1$s. Pokludzymy cie bez proces tworzynio kōnta.\nPo wybraniu %1$s za liferanta, poradzisz kōmunikować sie ze używoczami ôd inkszych liferantōw bez danie im swojij połnyj adresy XMPP. + Mosz zaproszynie na %1$s. Miano ôd używocza już je do ciebie wybrane. Pokludzymy cie bez proces tworzynio kōnta.\nBydzie szło kōmunikować sie ze używoczami ôd inkszych liferantōw bez danie im swojij połnyj adresy XMPP. + Twoje zaproszynie na serwer + Niynoleżnie sformatowany kod lifrowanio + Tyknij knefla dzielynio sie, żeby posłać kōntaktowi zaproszynie na %1$s. + Jeźli kōntakt je blisko, to może tyż zeskanować kod niżyj, żeby zaakceptować twoje zaproszynie. + Pōdź na %1$s i pogodej zy mnōm: %2$s + Poślij zaproszynie do… + \ No newline at end of file diff --git a/src/main/res/values-da-rDK/strings.xml b/src/main/res/values-da-rDK/strings.xml index 22f4bce2d..c07353a6a 100644 --- a/src/main/res/values-da-rDK/strings.xml +++ b/src/main/res/values-da-rDK/strings.xml @@ -465,6 +465,7 @@ Download mislykkes: Fil ikke fundet Download mislykkes: Kunne ikke forbinde til vært Download mislykkes: Kunne ikke skrive til fil + Download mislykkes: Ugyldig fil TOR netværk er utilgængelig Bind fejl Serveren er ikke ansvarlig for dette domæne @@ -622,6 +623,8 @@ Tøm privat lagerplads, hvor filer opbevares (De kan downloades igen fra serveren) Jeg fulgte dette link fra en pålidelig kilde Du er ved bekræfte OMEMO-nøgler af %1$s efter du har klikket på et link. Dette er kun sikkert, hvis du fulgte linket fra troværdig kilde hvor kun %2$s kunne have offentliggjort dette link. + Du er ved at bekræfte dine OMEMO-nøgler til din konto. Det er kun sikkert, hvis du fulgte linket fra en troværdig kilde, hvor kun du kan have offentliggjort dette link. + Fortsæt Beskræft OMEMO-nøgler Vis inaktive Skjul inaktive @@ -904,6 +907,7 @@ Indkommende videoopkald Forbinder Forbundet + Forbinder igen Accepter opkald Afslut opkald Svar @@ -919,6 +923,8 @@ Læg på Udgående opkald Igangværende videoopkald + Forbinder igen opkald + Forbinder igen videoopkald Deaktiver TOR for at lave opkald Indkommende opkald Indkommende opkald · %s @@ -968,4 +974,6 @@ Sikkerhedskopieringen er startet. Du får en notifikation, når den er afsluttet. Kunne ikke aktivere video. Ren tekstdokument + Kontoregistrering er ikke understøttet + Ingen XMPP-adresse fundet diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index a928266b1..b54309ed2 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -976,5 +976,4 @@ Textdokument Kontoregistrierungen werden nicht unterstützt Keine XMPP-Adresse gefunden - - + diff --git a/src/main/res/values-el/strings.xml b/src/main/res/values-el/strings.xml index d6a8cd209..22b188af4 100644 --- a/src/main/res/values-el/strings.xml +++ b/src/main/res/values-el/strings.xml @@ -975,5 +975,4 @@ Έγγραφο απλού κειμένου Δεν υποστηρίζονται εγγραφές λογαριασμών Δεν βρέθηκε διεύθυνση XMPP - - + diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index c958b30b2..d2c5b8afd 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -33,6 +33,9 @@ %d conversación sin leer + %dconversaciones sin leer + + %dconversaciones sin leer @@ -447,6 +450,7 @@ Cancelar %d certificado eliminado + %d certificados eliminados %d certificados eliminados Cambiar el botón de “Enviar” por el botón de acción rápida @@ -465,6 +469,7 @@ Error al descargar: Archivo no encontrado Error al descargar: No se ha podido conectar con el servidor Falló la descarga: No se puede escribir el fichero + Error al descargar: Archivo no válido Red Tor no disponible. Fallo de enlace El servidor no es responsable de este dominio @@ -504,6 +509,7 @@ %1$d de %2$d cuentas conectadas %d mensaje + %d mensajes %d mensajes Cargar más mensajes @@ -622,6 +628,8 @@ Limpiar datos privados de ficheros descargados (Pueden volver a descargarse desde el servidor) Enlace desde una fuente de confianza Vas a verificar las claves OMEMO de %1$s después de hacer click en el enlace. Esto solo es seguro si conseguiste este enlace desde una fuente de confianza donde solo %2$s pudo haber publicado el enlace + Está a punto de verificar las claves OMEMO de su propia cuenta. Esto solamente es seguro si ha seguido este enlace desde una fuente segura, donde solo usted lo haya publicado. + Continuar Verificar claves OMEMO Mostrar inactivos Ocultar inactivos @@ -629,26 +637,32 @@ ¿Estás seguro de que quieres eliminar la verificación de este dispositivo?\nEste dispositivo y los mensajes que lleguen desde allí serán marcados como \"No confiables\". %d segundo + %d segundos %d segundos %d minuto + %d minutos %d minutos %d hora + %d horas %d horas %d día + %d días %d días %d semana + %d semanas %d semanas %d mes + %d meses %d meses Borrado automático de mensajes @@ -904,6 +918,7 @@ Videollamada entrante Conectando Conectado + Reconectando Aceptar llamada Terminar llamada Contestar @@ -919,6 +934,8 @@ Colgar Llamada saliente Video llamada saliente + Reconectando llamada + Reconectando video llamada Deshabilitar Tor para hacer llamadas Llamada entrante Llamada entrante · %s @@ -952,10 +969,12 @@ Añadir contacto, crear o unirse a un grupo de chat, o descubrir canales Ver %1$d Participante + Ver %1$d Participantes Ver %1$d Participantes Un mensaje no se ha podido entregar + Algunos mensajes no se han podido entregar Algunos mensajes no se han podido entregar Envíos fallidos @@ -968,4 +987,6 @@ La copia de seguridad ha empezado. Recibirás una notificación cuando se haya completado. No se ha podido habilitar el vídeo. Documento de texto plano + Los registros de cuenta no están soportados + Dirección XMPP no encontrada diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml index 8e177693e..807a45f52 100644 --- a/src/main/res/values-fr/strings.xml +++ b/src/main/res/values-fr/strings.xml @@ -33,6 +33,9 @@ %d conversation non lue + %d conversations non lues + + %d conversations non lues @@ -444,6 +447,7 @@ Annuler %d certificat supprimé + %d certificats supprimés %d certificats supprimés Remplacer le bouton « Envoyer » par une action rapide @@ -462,12 +466,14 @@ Échec du téléchargement : impossible de trouver le fichier Échec du téléchargement : impossible de se connecter à l\'hôte Échec du téléchargement : Écriture impossible + Échec du téléchargement : Fichier non valide Réseau Tor inaccessible La liaison a échoué Le serveur n\'est pas responsable pour ce domaine Détraqué Disponibilité Absent quand l\'appareil est verrouillé + Afficher comme étant absent lorsque l\'appareil est verrouillé Occupé en mode silence Occupé lorsque l\'appareil est en mode silencieux Indisponible en mode vibreur @@ -500,6 +506,7 @@ %1$d compte(s) sur %2$d connecté(s) %d message + %d messages %d messages Charger plus de messages @@ -624,26 +631,32 @@ Êtes-vous sûr de vouloir retirer la vérification pour cet appareil ?\nCet appareil et les messages qui en proviennent seront marqués comme « indignes de confiance ». %d seconde + %d secondes %d secondes %d minute + %d minutes %d minutes %d heure + %d heures %d heures %d jour + %d jours %d jours %d semaine + %d semaines %d semaines %d mois + %d mois %d mois Suppression messages auto @@ -899,6 +912,7 @@ Appel vidéo entrant Connexion en cours Connecté + Reconnexion en cours Accepter les appels Fin d\'appel Décrocher @@ -910,9 +924,12 @@ Connexion perdue Appel annulé Échec de l\'application + Vérification du problème Raccrocher Appel en cours Appel vidéo en cours + En cours de reconnexion de l\'appel + En cours de reconnexion de l\'appel vidéo Désactivez Tor afin de passer des appels Appel entrant Appel entrant · %s @@ -946,10 +963,12 @@ Ajouter un contact, créer ou joindre un groupe de discussion, ou découvrir les salons Voir %1$d participant + Voir %1$d participants Voir %1$d participants Certains messages n\'ont pas pu être distribués + Certains messages n\'ont pu être distribués Certains messages n\'ont pu être distribués Échec lors de la livraison @@ -957,8 +976,9 @@ Aucune application trouvée Inviter à Conversations Impossible de lire l\'invitation + Le serveur ne prend pas en charge la génération d\'invitations + Aucun compte actif ne prend en charge cette fonctionalité Impossible d’activer la vidéo. La création de nouveaux comptes n’est pas prise en charge Aucune adresse XMPP trouvée - - + diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 419dcf82b..76b0f937a 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -976,5 +976,4 @@ Documento de texto plano Non está permitido o rexistro de novas contas Non se atopa un enderezo XMPP - - + diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index af7c4b93d..a665e79c7 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -33,6 +33,9 @@ %d conversazione non letta + %d conversazioni non lette + + %d conversazioni non lette @@ -447,6 +450,7 @@ Annulla Cancellato il %d certificato + Cancellati %d certificati Cancellati %d certificati Sostituisci il tasto \"Invio\" con un\'azione rapida @@ -505,6 +509,7 @@ %1$d su %2$d profili connessi %d messaggio + %d messaggi %d messaggi Carica altri messaggi @@ -632,26 +637,32 @@ Sei sicuro di volere rimuovere la verifica di questo dispositivo?\nIl dispositivo e i messaggi provenienti da esso verranno segnati come \"Non fidato\". %d secondo + %d secondi %d secondi %d minuto + %d minuti %d minuti %d ora + %d ore %d ore %d giorno + %d giorni %d giorni %d settimana + %d settimane %d settimane %d mese + %d mesi %d mesi Eliminazione automatica dei messaggi @@ -958,10 +969,12 @@ Aggiungi un contatto, crea o visita una chat di gruppo, o scopri canali Vedi %1$d partecipante + Vedi %1$d partecipanti Vedi %1$d partecipanti Un messaggio non è stato recapitato + Alcuni messaggi non sono stati recapitati Alcuni messaggi non sono stati recapitati Recapiti falliti @@ -976,5 +989,4 @@ Documento di testo Le registrazioni di profili non sono supportate Nessun indirizzo XMPP trovato - - + diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml index 452ef18f8..a207bb71c 100644 --- a/src/main/res/values-ja/strings.xml +++ b/src/main/res/values-ja/strings.xml @@ -217,6 +217,8 @@ OpenPGP 鍵 ID OMEMO フィンガープリント v\\OMEMO フィンガープリント + OMEMO フィンガープリント (メッセージ起源) + v\\OMEMO フィンガープリント (メッセージ起源) 他のデバイス OMEMO フィンガープリントを信頼 鍵の取得中… @@ -459,6 +461,7 @@ ダウンロードに失敗しました: ファイルが見つかりません ダウンロードに失敗しました: ホストに接続できませんでした ダウンロードに失敗しました: ファイルに書き込みできません + ダウンロード失敗: 無効なファイル Tor ネットワークが利用できません バインド失敗 そのサーバーはこのドメインに責任を持ちません @@ -954,5 +957,4 @@ プレーンテキスト文書 アカウント登録はサポートされていません XMPPアドレスがみつかりません - - + diff --git a/src/main/res/values-nl/strings.xml b/src/main/res/values-nl/strings.xml index 32915d274..b08726ee8 100644 --- a/src/main/res/values-nl/strings.xml +++ b/src/main/res/values-nl/strings.xml @@ -4,6 +4,7 @@ Nieuw gesprek Accounts beheren Account beheren + Gesprek sluiten Contactgegevens Gespreksgegevens Kanaalinformatie @@ -28,6 +29,13 @@ zojuist 1 min. geleden %d min. geleden + + %d ongelezen gesprek + + + %d ongelezen gesprekken + + versturen… Bericht aan het ontsleutelen. Even geduld… OpenPGP-versleuteld bericht @@ -63,6 +71,8 @@ Deblokkeren Opslaan Oké + %1$s is gecrasht + Door crashrapportages via uw XMPP account te sturen help je de ontwikkeling van %1$s. Nu versturen Niet opnieuw vragen Verbinding maken met account mislukt @@ -155,6 +165,7 @@ OpenPGP-publieke sleutel publiceren OpenPGP-publieke sleutel verwijderen Weet je zeker dat je je OpenPGP-publieke sleutel uit je aanwezigheidsaankondiging wil verwijderen?\nJe contacten zullen je geen OpenPGP-versleutelde berichten meer kunnen sturen. + OpenPGP-publieke sleutel gepubliceerd. Account inschakelen Weet je het zeker? Stem opnemen @@ -178,8 +189,11 @@ niet beschikbaar Ontbrekende publieke sleutel-aankondigingen zojuist voor het laatst gezien + een minuut geleden voor het laatst gezien %d minuten geleden voor het laatst gezien + een uur geleden voor het laatst gezien %d uur geleden voor het laatst gezien + een dag geleden voor het laatst gezien %d dagen geleden voor het laatst gezien OpenPGP-sleutel-ID OMEMO-vingerafdruk @@ -220,6 +234,7 @@ %s hebben tot hier gelezen Iedereen heeft tot hier gelezen Publiceer + Tik op avatar om een foto uit de galerij te kiezen Publiceren… De server weigerde de publicatie van je afbeelding Fout bij opslaan van avatar @@ -230,6 +245,7 @@ Verbind Deze account bestaat al Volgende + Sessie is tot stand gekomen Overslaan Meldingen uitschakelen Inschakelen @@ -297,6 +313,7 @@ %s aangeboden om te downloaden Bestandsoverdracht annuleren bestandsoverdracht geannuleerd + Geen app om bestand te openen Dynamische tags Toon enkel-lezen tags onder contacten Meldingen inschakelen @@ -314,6 +331,7 @@ Wachtwoord wijzigen Huidig wachtwoord Nieuw wachtwoord + Wachtwoord mag niet leeg zijn Alle accounts inschakelen Alle accounts uitschakelen Actie uitvoeren met @@ -370,6 +388,7 @@ Laat je contacten weten wanneer je ze een nieuw bericht schrijft Locatie versturen Locatie weergeven + Geen app om locatie weer te geven Locatie Gesprek gesloten Privégroep verlaten diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index b3ce11037..4b49a6501 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -1003,5 +1003,4 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Dokument zwykłego tekstu Rejestracja kont nie jest wspierana Nie znaleziono adresu XMPP - - + diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index 79d6ea798..9bcf1aaea 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -33,6 +33,9 @@ %d conversa não lida + %d conversas não lidas + + %d conversas não lidas @@ -447,6 +450,7 @@ Cancelar %d certificado cancelado + %d certificados cancelados %d certificados cancelados Troca o botão \"Enviar\" pelo de ação rápida @@ -505,6 +509,7 @@ %1$d de %2$d contas conectadas %d mensagem + %d mensagens %d mensagens Carregar mais mensagens @@ -632,26 +637,32 @@ Tem certeza que deseja remover a verificação para este dispositivo?\nEste dispositivo e as mensagens oriundas dele serão marcadas como \"não confiáveis\". %d segundo + %d segundos %d segundos %d minuto + %d minutos %d minutos %d hora + %d horas %d horas %d dia + %d dias %d dias %d semana + %d semanas %d semanas %d mês + %d meses %d meses Exclusão automática de mensagens @@ -958,10 +969,12 @@ Adicionar contato, criar ou associar-se a uma conversa em grupo ou descobrir canais Ver %1$d participante + Ver %1$d participantes Ver %1$d participantes Não foi possível enviar a mensagem + Não foi possível enviar algumas mensagens Não foi possível enviar algumas mensagens Entregas não efetuadas @@ -976,5 +989,4 @@ Documento em texto puro O registro de contas não está ativo Não foi encontrado nenhum endereço XMPP - - + diff --git a/src/main/res/values-pt/strings.xml b/src/main/res/values-pt/strings.xml index 1d7ea055f..125a0af4c 100644 --- a/src/main/res/values-pt/strings.xml +++ b/src/main/res/values-pt/strings.xml @@ -308,6 +308,7 @@ Cancelar %d certificado apagado + %d certificados apagados %d certificados apagados Ação rápida @@ -348,6 +349,7 @@ %1$d de %2$d contas conectadas %d mensagem + %d mensagens %d mensagens Carregar mais mensagens diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index 1ed0d4caa..a57f9d0eb 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -989,5 +989,4 @@ Document text Nu este posibilă înregistrarea unui cont Nu a fost găsită o adresă XMPP - - + diff --git a/src/main/res/values-szl/strings.xml b/src/main/res/values-szl/strings.xml new file mode 100644 index 000000000..0ed32a533 --- /dev/null +++ b/src/main/res/values-szl/strings.xml @@ -0,0 +1,1025 @@ + + + Sztelōnki + Nowo godka + Sztelōnki kōnt + Sztelōnki kōnta + Zawrzij godka + Informacyje kōntaktu + Informacyje kōnferyncyje + Informacyje kanału + Przidej kōnto + Edytuj miano + Przidej do kōntaktōw + Skasuj z rostera + Zablokuj kōntakt + Ôdblokuj kōntakt + Ôdblokuj dōmyna + Ôdblokuj dōmyna + Zablokuj kōntakt + Ôdblokuj kōntakt + Sztelōnki kōnt + Sztelōnki + Udostympnij we godce + Zacznij godka + Ôbier kōntakt + Ôbier kōntakty + Udostympnij bez + Czorno lista + przed chwilōm + minuta tymu + %d minut tymu + + %d niyprzeczytano kōnwersacyjo + + + %d niyprzeczytane kōnwersacyje + + + %d niyprzeczytanych kōnwersacyji + + + wysyłanie… + Ôdszyfrowowanie wiadōmości. To weźnie ino chwila… + Wiadōmość zaszyfrowano OpenPGP + Miano je już zajynte + Niynoleżny pseudōnim + Admin + Posiedziciel + Moderatōr + Uczestnik + Gość + Chcesz wymazać kōntakt %s ze listy\? Godki ze tym kōntaktym niy bydōm wymazane. + Na zicher chcesz zablokować wiadōmości ôd kōntaktu %s\? + Na zicher chcesz ôdblokować wiadōmości ôd kōntaktu %s\? + Zablokować wszyjske kōntakty ze %s\? + Ôdblokować wszyjske kōntakty ze %s\? + Kōntakt zablokowany + Zablokowane + Chcesz wymazać zokłodka %s\? Godki z niōm niy bydōm wymazane. + Zaregistruj nowe kōnto na serwerze + Umiyń hasło na serwerze + Udostympnij… + Zacznij godka + Zaproś kōntakt + Zaproś + Kōntakty + Kōntakt + Pociep + Nasztaluj + Przidej + Edytuj + Wymaż + Zablokuj + Ôdblokuj + Spamiyntej + OK + We %1$s doszło do awaryje + Jak używosz swojigo kōnta XMPP do wysyłanio sztrekōw sztapla, to pōmogosz przi budowaniu %1$s. + Wyślij teroz + Niy pytej zaś + Niy idzie połōnczyć z kōntym + Niy idzie połōnczyć z mockōm kōnt + Tyknij, coby sztelować swoje kōnta + Przidej zbiōr + Przidać tyn kōntakt do twojij listy kōntaktōw\? + Przidej kōntakt + wysyłanie sie niy podarziło + Rychtowanie do wysłanio ôbrozka + Rychtowanie do wysłanio ôbrozkōw + Udostympnianie zbiorōw. Czekej… + Wymaż historyjo + Wymaż historyjo kōnwersacyje + Chcesz wymazać wszyjske wiadōmości we tyj godce\? +\n +\nPozōr: To niy wpływo na wiadōmości trzimane na inkszych maszinach abo serwerach. + Wymaż zbiōr + Na zicher wymazać tyn zbiōr\? +\n +\nPozōr: To niy wpływo na kopije zbioru trzimane na inkszych maszinach abo serwerach. + Zawrzij potym tyż kōnwersacyjo + Ôbier maszina + Wyślij wiadōmość bez szyfrowanio + Wyślij wiadōmość + Wyślij wiadōmość do kōntaktu %s + Wyślij wiadōmość zaszyfrowano OMEMO + Wyślij wiadōmość zaszyfrowano v\\OMEMO + Wyślij zaszyfrowano wiadōmość (OpenPGP) + Przemianek je umiyniōny + Wyślij bez szyfrowanio + Niy idzie ôdszyfrować. Możno niy mosz noleznego prywatnego klucza. + OpenKeychain + %1$s używo <b>OpenKeychain</b>, żeby szyfrować i ôdszyfrowować wiadōmości i zarzōndzać twojimi publicznymi kluczami.<br><br>OpenKeychain je na licyncyji GPLv3+ i je dostympny we F-Droid abo Google Play.<br><br><small>(Puść %1$s na nowo po zainstalowaniu.)</small> + Zresztartuj + Zainstaluj + Zainstaluj OpenKeychain + ôferowanie… + czekanie… + Niy szło znojś klucza OpenPGP + Niy idzie zaszyfrować twojij wiadōmości, bo tyn kōntakt niy ôgłoszo swojigo publicznego klucza. +\n +\nPoproś kōntakt, żeby nasztalowoł OpenPGP. + Niy szło znojś kluczy OpenPGP + Niy idzie zaszyfrować twojij wiadōmości bo twoje kōntakty niy ôgłoszajōm swojich kluczy publicznych. +\n +\nPoproś, żeby nasztalowały OpenPGP. + Bazowe + Akceptuj zbiory + Autōmatycznie akceptuj zbiory myńsze niż… + Przidowki + Powiadōmiynie + Wibracyje + Wibruj, jak przidzie wiadōmość + Powiadōmiynie diodōm LED + Migej diodōm, jak przidzie wiadōmość + Zwōnek + Klang powiadōmiyń + Klang powiadōmiyń ô nowych wiadōmościach + Zwōnek przi przichodzōncych połōnczyniach + Czas bez powiadōmiyń + Dugość czasu, kej powiadōmiynia sōm wyciszōne po wykryciu aktywności na jednyj z twojich inkszych maszin. + Rozszyrzōne + Niy wysyłej reportōw awaryje + Jak wysyłosz sztreki sztapla, to pōmogosz przi budowaniu + Potwierdzynia wiadōmości + Przizwōl na wysyłanie do ôsōb ze listy kōntaktōw informacyje ô twojim dostaniu i przeczytaniu wiadōmości ôd nich + Blokuj zopisy ekranu + Skryj zawartość aplikacyje we szaltrze aplikacyji i zablokuj zopisy ekranu + UI + OpenKeychain zgłosiyło błōnd. + Zły klucz szyfrowanio. + Akceptować + Doszło do błyndu + Błōnd + Twoje kōnto + Wysyłej powiadōmiynia ôbecności + Dostowej powiadōmiynia ôbecności + Poproś ô powiadōmiynia ôbecności + Ôbier ôbroz + Zrōb fotografijo + Autōmatyczno zwolo na subskrypcyjo + Ôbrany zbiōr to niy ôbroz + Błōnd kōnwersyje ôbrazu + Niy szło znojś zbioru + Ôgōlny feler wchodu/wychodu. Możno skōńczōł sie plac na dane\? + Aplikacyjo użyto do ôbioru ôbrazu niy przizwolyła na ôdczyt zbioru. +\n +\nÔbier ôbroz przi użyciu inkszego mynedżera zbiorōw. + Aplikacyjo użyto do udostympniynio tego zbioru niy dała stykajōncych uprawniyń. + Niyznōmy + Tymczasowo zastawiōne + Połōnczōne + Łōnczynie… + Rozłōnczōne + Błōnd autoryzacyje + Niy szło znojś serwera + Brak połōnczynio + Błōnd registracyje + Miano zajynte + Zaregistrowano sprownie + Tyn serwer niy spiyro registracyje + Niynoleżny tokyn registracyje + Niy podarziła sie negocjacyjo TLS + Dōmyna niyweryfikowalno + Naruszynie prawideł + Serwer niykōmpatybilny + Błōnd potoku + Błōnd ôtwiyranio potoku + Bez szyfrowanio + OTR + OpenPGP + OMEMO + Wymaż kōnto + Zastow tymczasowo + Publikuj awatar + Udostympnij klucz publiczny OpenPGP + Wymaż klucz publiczny OpenPGP + Na zicher chcesz wymazać klucz publiczny OpenPGP ze swojij propagacyje ôbecności\? +\nTwoje kōntakty niy bydōm już mogły wysyłać ci wiadōmości zaszyfrowanych OpenPGP. + Klucz publiczny OpenPGP ôstoł ôpublikowany. + Włōncz kōnto + Na zicher\? + Wymazanie kōnta wymazuje cołko historyjo godek + Nagrej głos + Adresa XMPP + Zablokuj adresa XMPP + username@example.com + Hasło + To niyma noleżno adresa XMPP + Brak pamiyńci. Ôbroz je za srogi + Chcesz przidać %s do listy kōntaktōw\? + Informacyje ô serwerze + XEP-0313: MAM + XEP-0280: Kopije wiadōmości + XEP-0352: Skaźnik Sztandu Klijynta + XEP-0191: Rozkoz blokowanio + XEP-0237: Wersyjowanie listy + XEP-0198: Sztelōnki potoku + XEP-0215: Wykrywanie Zewnyntrznych Usug + XEP-0163: PEP (Awatary / OMEMO) + XEP-0363: Przesyłanie zbiorōw bez HTTP + XEP-0357: Push + dostympny + niydostympny + Brak informacyje ô kluczu publicznym + prawie widziany + widziany przed minutōm + widziany prze %d minutami + widziany przed godzinōm + widziany przed %d godzinami + widziany wczorej + widziany przed %d dniami + Wiadōmość zaszyfrowano. Zainstaluj OpenKeychain, coby ôdszyfrować. + Znojdziōne nowe wiadōmości zaszyfrowane we OpenPGP + ID klucza OpenPGP + Ôdcisk OMEMO + Ôdcisk v\\OMEMO + Ôdcisk OMEMO tyj wiadōmości + Ôdcisk v\\OMEMO tyj wiadōmości + Insze masziny + Zadufane ôdciski OMEMO + Pobiyranie kluczy… + Skōńczōno + Ôdszyfruj + Zokłodki + Szukej + Wpisz kōntakt + Wymaż kōntakt + Informacyje ô kōntakcie + Zablokuj kōntakt + Ôdblokuj kōntakt + Stwōrz + Ôbier + Kōntakt już istniyje + Prziwstōń + kanal@konferyncyje.prziklod.com/przemianek + kanal@konferyncyje.prziklod.com + Przidej za zokłodka + Wymaż zokłodka + Wymaż kōnferyncyjo + Wymaż kanał + Je żeś zicher, iże chcesz wymazać ta kōnferyncyjo\? +\n +\nPozōr: Ta kōnferyncyjo bydzie doimyntnie wymazano na serwerze. + Na zicher chcesz wymazać tyn kanał publiczny\? +\n +\nPozōr: Tyn kanał bydzie doimyntnie wymazany ze serwera. + Niy szło wymazać kōnferyncyje + Niy szło wymazać kanału + Edytuj tytuł kōnferyncyje + Tymat + Przistympowanie do kōnferyncyje… + Ôpuść izba + Kōntakt przidoł cie do listy kōntaktōw + Tyż przidej + Kōntakt %s przeczytoł dotōnd + Kōntakty %s przeczytały dotōnd + Kōntakty %1$s i %2$d inszych przeczytało dotōnd + Wszyjscy przeczytali dotōnd + Publikuj + Tyknij awatar, coby ôbrać ôbroz z galeryje + Publikowanie… + Serwer ôdkozoł publikacyje + Niy szło skōnwertować ôbrazu + Niy szło spamiyntać ôbrazu we pamiyńci masziny + (abo dugo przitrzim, coby nasztalować wychodny) + Twōj serwer niy umożliwo publikacyje awatarōw + szepce + do %s + Wyślij prywatno wiadōmość do %s + Połōncz + Kōnto już istniyje + Dalij + Połōnczōno ze serwerym + Przeskocz + Zastow powiadōmiynia + Włōncz + Kōnferyncyjo wymogo hasła + Wkludź hasło + Poproś kōntakt ô udostympniynie powiadōmiyń ô ôbecności. +\n +\nTo bydzie używane do skazowanio, jakigo programu używo twōj kōntakt. + Poproś teroz + Ignoruj + Pozōr: Wysyłanie bez powiadōmiyń ô ôbecności ze ôbōch strōn może prziniyś niyôczekowane problymy. +\n +\nIdź do „Informacyji ô kōntakcie” i wejzdrzij do subskrypcyji ôbecności. + Bezpieczyństwo + Przizwōl na poprowianie wiadōmości + Przizwōl swojim kōntaktōm poprowiać wiadōmości + Sztelōnki eksperta + Modyfikuj je pozornie + Ô %s + Godziny cisze + Poczōntek + Kōniec + Włōncz godziny cisze + Powiadōmiynia bydōm wyciszōne we ôbranych godzinach + Inksze + Synchrōnizuj ze zokłodkami + Przistympuj do godek grupowych autōmatycznie, jeźli tak pado zokłodka + Ôdcisk klucza OMEMO bōł skopiowany do skrytki + Ôd tyj grupy mosz wykluczynie + Kōnferyncyjo ino dlo czōnkōw + Ukrōcynie zasobu + Ze tyj kōnferyncyje cie wyciepli + Kōnferyncyjo ôstała zawarto + Już żeś niy je we tyj kōnferyncyji + ze użyciym kōnta %s + trzimane na %s + Wybadowanie %s na hoście HTTP + Brak połōnczynio. Sprōbuj zaś niyskorzij + Wybadej srogość %s + Wybadej srogość %1$s na %2$s + Ôpcyje wiadōmości + Cytata + Wraź za cytata + Skopiyruj ôryginalny URL + Wyślij zaś + URL zbioru + Skopiowano URL do skrytki + Skopiowano adresa XMPP do skrytki + Skopiowano kōmunikat błyndu do skrytki + adresa necowo + Zeskanuj kod + Pokoż kod QR + Pokoż wykoz wykluczyń + Informacyje kōnta + Potwiyrdź + Sprōbuj zaś + Usuga na piyrszym planie + Niy zwolo systymowi na przerwanie połōnczynio + Stwōrz kopijo ibryczno + Kopijo ibryczno bydzie spamiyntano we %s + Tworzynie kopije ibrycznyj + Kopijo ibryczno stworzōno + Kopijo ibryczno spamiyntano we %s + Prziwrocanie ze kopije ibrycznyj + Prziwrocanie ze kopije ibrycznyj gotowe + Niy zapōmnij ô aktywacyji tego kōnta. + Ôbier zbiōr + Ôdbiyranie %1$s (skōńczōne %2$d%%) + Pobier %s + Wymaż %s + zbiōr + Ôtwōrz %s + Wysyłanie (skōńczōne %1$d%%) + Rychtowanie do udostympniynio ôbrozka + Zapropōnowano pobranie zbioru %s + Pociep przesyłanie + niy szło udostympnić zbioru + transmisyjo zbioru pociepniynto + Zbiōr wymazany + Niy szło znojś aplikacyje do ôtwarcia zbioru + Niy szło znojś aplikacyje do ôtwarcia linka + Niy szło znojś aplikacyje do pokozanio kōntaktu + Dynamiczne tagi + Pokazuj etykety pod kōntaktami + Włōncz powiadōmiynia + Niy szło znojś serwera kōnferyncyje + Niy szło stworzić kōnferyncyje + Awatar kōnta + Skopiuj ôdcisk klucza OMEMO do skrytki + Wygyneruj zaś klucz OMEMO + Wymaż masziny + Na zicher chcesz wymazać wszyjske inksze masziny z ôgłoszynio OMEMO\? Jak twoje masziny sie zaś połōnczōm, to sie zaś ôgłoszōm, ale mogōm niy dostać wiadōmości wysłanych bez tyn czas. + Niy ma dostympnych kluczy dlo tego kōntaktu. +\nNiy szło pobrać nowych kluczy ze serwera. Możno je coś niy tak ze serwerym ôd twojigo kōntaktu\? + Brak dostympnych kluczy dlo tego kōntaktu. +\nDej pozōr, czy wzajymnie powiadōmiocie sie ô ôbecności. + Coś poszło źle + Pobiyranie historyje z serwera + Kōniec historyje na serwerze + Aktualizowanie… + Hasło było zmiyniōne! + Niy szło zmiynić hasła + Zmiyń hasło + Teroźne hasło + Nowe hasło + Hasło niy może być prōzne + Aktywuj wszyjske kōnta + Zastow wszyjske kōnta + Użyj + Brak stanowiska + Offline + Wykluczōny + Czōnek + Tryb rozszyrżōny + Prziznej uprawniynia czōnkostwa + Wymaż uprawniynia czōnkostwa + Prziznej uprawniynia administratora + Odbierz uprawniynia administratora + Prziznej uprawniynia posiedziciela + Wymaż uprawniynia posiedziciela + Wyciep z kōnferyncyje + Wyciep z kanału + Niy szło umiynić stanowiska ôd %s + Wyklucz + Wyklucz na kanale + Chcesz wyciepnōńć %s z publicznego kanału. Jedyny spusōb na to, to je wykluczyć ta ôsoba na dycki. + Wyklucz teroz + Niy szło zmiynić funkcyje %s + Kōnfiguracyjo prywatnyj kōnferyncyje + Kōnfiguracyjo publicznego kanału + Prywatne, ino dlo czōnkōw + Niych adresa XMPP bydzie widzialno dlo wszyjskich + Niych kanał bydzie moderowany + Niy bieresz udziału + Sztalōnki kōnferyncyje były zmiyniōne! + Niy idzie zmiynić sztelōnkōw kōnferyncyje + Nigdy + Ryncznie + Ôdłōż + Ôdpowiydz + Ôznocz za przeczytane + Sztelōnki wkludzanio + Enter wysyło + Używej knefla Enter do wysyłanio wiadōmości. Dycki możesz używać Ctrl+Enter, żeby wysłać wiadōmość, nawet jak ta ôpcyjo je zastawiōno. + Pokoż knefel Enter + Umiyń knefel emotikōnōw na Enter + zbiōr audio + zbiōr wideo + ôbroz + wektorowo grafika + Dokumynt PDF + Aplikacyjo Androida + Kōntakt + Avatar bōł sprownie ôpublikowany! + Wysyłanie %s + Propōnowanie %s + Skryj niydostympnych + %s pisze… + %s niy pisze + %s piszōm… + %s niy piszōm + Powiadōmiynia pisanio + Dowej znać kōntaktōm, jak dō nich piszesz + Wyślij lokalizacyjo + Pokoż lokalizacyjo + Niy szło znojś aplikacyje do pokazowanio lokalizacyje + Lokalizacyjo + Kōnwersacyjo zawarto + Ôpuś prywatno kōnferyncyjo + Ôpuś publiczny kanał + Niy dufej certyfikatōm systymowym + Wymogej ryncznego potwiyrdzanio certyfikatōw + Wymaż certyfikaty + Ôbier zadufane certyfikaty do wymazanio + Brak ryncznie zadufanych certyfikatōw + Wymaż certyfikaty + Wymaż zaznaczōne + Pociep + + Wymazany %d certyfikat + Wymazane %d certyfikaty + Wymazane %d certyfikatōw + + Zastōmp knefel wysyłanio gibkōm akcyjōm + Gibko akcyjo + Brak + Ôstatnio używano + Ôbier gibko akcyjo + Przeszukej kōntakty + Przeszukej zokłodki + Wyślij wiadōmość prywatno + %1$s już niy je we kōnferyncyji + Miano ôd używocza + Miano ôd używocza + Niynolezne miano ôd używocza + Pobiyranie niyudane: Niy szło znojś serwera + Pobiyranie niyudane: Niy szło znojś zbioru + Pobiyranie niyudane: Niy szło połōnczyć z hostym + Pobiyranie niyudane: Niy szło spamiyntać zbioru + Pobiyranie niypodarzōne: Niynoleżny zbiōr + Nec TOR je niydostympny + Błōnd połōnczynio + Serwer niy ôdpado dōmynie + Zepsute + Dostympność + Status „W ôddali”, kej ekran je zastawiōny + Ôznaczo twōj zasōb za „W ôddali”, kej ekran je zastawiōny + „Niy szterować” we trybie cichym + Ôznaczo twōj zasōb za „Niy szterować”, kej maszina je w trybie cichym + Traktuj tryb wibracyje jak tryb cichy + Ôznaczo twōj zasōb za „Niy szterować”, kej maszina je w trybie wibracyje + Rozszyrzōne sztelōnki połōnczynio + Pokoż miano ôd hosta i sztelōnki portu przi przidowaniu kōnta + xmpp.prziklod.com + Wloguj certyfikatym + Niy szło ôdczytać certyfikatu + Preferyncyje archiwizacyje + Preferyncyje archiwizacyje po strōnie serwera + Pobiyranie preferyncyji archiwizacyje. Czekej… + Niy idzie pobrać preferyncyji archiwizacyje + Wymogano CAPTCHA + Wkludź tekst z ôbrozka wyżyj + Lyńcuch certyfikatōw niyma zadufany + Adresa XMPP niy pasuje do certyfikatu + Ôdnōw certyfikat + Błōnd pobiyranio klucza OMEMO! + Klucz OMEMO zweryfikowany certyfikatym! + Twoja maszina niy spiyro ôbiyranio certyfikatōw klijynckich! + Połōnczynie + Połōncz bez nec TOR + Tuneluj wszyjske połōnczynia bez nec TOR. Wymogo aplikacyje Orbot + Miano hosta + Port + Adresa serwera abo .onion + To niyma noleżny numer portu + To niyma noleżne miano hosta + %1$d z %2$d kōnt połōnczōnych + + %d wiadōmość + %d wiadōmości + %d wiadōmości + + Zaladuj wiyncyj wiadōmości + Zbiōr dzielōny ze %s + Ôbroz dzielōny ze %s + Ôbrazy dzielōne ze %s + Tekst dzielōny ze %s + Przizwōl %1$s na dostymp do zewnyntrznego składu + Przizwōl %1$s na dostymp do aparatu + Synchrōnizuj z kōntaktami + %1$s potrzebuje twojij zwōle na dopasowanie twojich kōntaktōw XMPP z wykazym kōntaktōw w telefōnie. +\nTo pokoże jejich połne miana i awatary. +\n +\n%1$s ino przeczyto twoje kōntakty i dopasuje je lokalnie, bez wysyłanio na twōj serwer. + Quicksy potrzebuje dostympu do numerōw telefōnōw twojich kōntaktōw, coby zasugerować ci kōntakty, co już używajōm Quicksy. <br><br>Niy trzimiymy kopiji tych numerōw. +\n +\nAby dostać wiyncyj informacyje przeczytej naszo polityka prywatności</a>. <br><br>Teroz pojawi sie prośba ô zwōlo na dostymp do twojich kōntaktōw. + Powiadōm ô wszyskich wiadōmościach + Powiadōmiej ino w przipodku spōmniynio ô mie + Powiadōmiynia zastawiōne + Powiadōmiynia strzimane + Kōmpresyjo ôbrazōw + Podpowiydź: Użyj “Wybier zbiōr” zamiast “Wybier ôbroz”, coby wysłać pojedyncze ôbrozki bez kōmpresyje bez zglyndu na tyn sztalōnek. + Dycki + Ino sroge ôbrozki + Ôptymalizacyje używanio bateryje włōnczōne + Twoja maszina mo włōnczōne agresywne szporowanie bateryje, bez co %1$s może ôdbiyrać wiadōmości z ôpōźniyniym. +\nZastawiynie tych ôptymalizacyji je rekōmyndowane. + Twoja maszina mo włōnczōne agresywne szporowanie bateryje, bez co %1$s może ôdbiyrać wiadōmości z ôpōźniyniym. +\n +\nTeroz pojawi sie prośba ô jejich zastawiynie. + Zastow + Zaznaczōne przestrzyństwo je za sroge + (Brak aktywynych kōnt) + To pole je wymogane + Poprow wiadōmość + Wyślij poprawiōno wiadōmość + Tyn kōntakt już bōł ôd ciebie zweryfikowany. Bez wybranie “Gotowe” ino potwiyrdzosz, że %s noleży do tyj grupowyj godki. + To kōnto było ôd ciebie zastawiōne + Feler bezpieczyństwa: niynoleżny dostymp do zbioru! + Niy szło znojś aplikacyje do udostympniynio URI + Udostympnij URI ze pōmocōm… + Quicksy to modyfikacyjo popularnego klijynta XMPP Conversations, z autōmatycznym wykrywaniym kōntaktōw.<br><br>Zapisujesz sie przi użyciu numeru telefōnu i Quicksy autōmatycznie — podle numerōw telefōnōw we adresowyj ksiōnżce — zasugeruje potyncjalne kōntakty dlo ciebie.<br><br>Bez zapisanie sie zgodzosz sie na naszo <a href=https://quicksy.im/#privacy>polityka prywatności</a>. + Zgodzōm sie, kōntynuuj + Pokludzymy cie bez proces tworzynio kōnta na conversations.im.¹ +\nKej ôbieresz conversations.im za liferanta, to poradzisz kōmunikować sie ze używoczami inkszych liferantōw, jeźli podosz im swoja połno adresa XMPP. + Twoja połno adresa XMPP to: %s + Stwōrz kōnto + Użyj inkszego serwera + Ôbier miano ôd używocza + Regyruj dostympnościōm ryncznie + Sztaluj dostympność we ôknie edytowanio wiadōmości statusu. + Status + Pogodo + Je + Fōrt + Niy ma + Zajynte + Bezpieczne hasło je wygynerowane + Twoja maszina niy przizwolo na zastawiynie ôptymalizacyje bateryje + Registracyjo niy podarziła sie: sprōbuj niyskorzij + Registracyjo niy podarziła sie: hasło za słabe + Ôbier czōnkōw + Tworzynie kōnferyncyje… + Zaproś zaś + Zastow + Krōtki + Postrzedni + Dugi + Roznajmuj użycie + Informuje twoje kōntakty ô tym, kedy używosz Conversations + Prywatność + Tymat + Wybier paleta farbōw + Autōmatycznie + Jasny + Ciymny + Zielōny zadek + Używej zielōnego zadku dlo dostanych wiadōmości + Niy idzie sie połōnczyć z OpenKeychain + Ta maszina juz niy je używano + Kōmputer + Mobilniok + Tablet + Przeglōndarka necu + Kōnsola + Wymogany płat + Dej zwōlo na dostymp do Internetu + Jo + Kōntakt prosi ô udostympniynie statusu + Przizwōl + Brak zwōle na dostymp do %s + Niy szło znojś serwera + Brak ôdpowiedzi ôd zdalnego serwera + Niy szło zaktualizować kōnta + Zgłoś ta adresa XMPP za spamowanie. + Wymaż tożsamości OMEMO + Wygyneruj jeszcze roz klucze OMEMO. Wszyske twoje kōntakty bydōm musiały cie zaś zweryfikować. Użyj tego ino w ôstateczności. + Wymaż zaznaczōne klucze + Potrzebne połōnczynie, coby ôpublikować twōj awatar. + Pokoż kōmunikaty felerōw + Kōmunikat ô felerze + Szporowanie danych je włōnczōne + Twōj systym ôperacyjny blokuje dostymp do internetu %1$s, jak ôn funguje we zadku. Coby dostować powiadōmiynia ô nowych wiadōmościach, trzeba dać %1$s niyôgraniczōny dostymp do internetu, kedy szporowanie danych je włōnczōne. +\n%1$s bydzie durch ôgraniczoł transfer danych, kedy ino to je możliwe. + Twoja maszina niy spiyro zastawianio szporowanio danych dlo %1$s. + Niy szło stworzić tymczasowego zbioru + Maszina je zweryfikowano + Skopiyruj ôdcisk + Wszyske twoje klucze OMEMO sōm zweryfikowane + Kod kryskowy niy zawiyro ôdciskōw dlo tyj godki. + Zaufane ôdciski + Użyj fotoaparatu, coby zeskanować kod kryskowy kōntaktu + Czekej na ściōngniyńcie kluczy + Udostympnij bez kod QR + Udostympnij bez URI XMPP + Udostympnij bez link HTTP + Ôd Razu Ufej Przed Weryfikacyjōm + Autōmatycznie ufej wszyskim nowym maszinōm ôd niyzweryfikowanych kōntaktōw, ale proś ô rynczne potwierdzynie nowych maszin ôd zweryfikowanych kōntaktōw. + Ôd razu zaufane klucze OMEMO, to znaczy mogōm noleżeć do kogoś inkszego abo ftoś może sie podepnōńć. + Niezaufane + Niynoleżny kod kryskowy 2D + Wypucuj cache (używane ôd fotoaparatu) + Wysnoż cache + Wysnoż prywatny skłod + Wysnoż prywatny skłod, kaj sōm trzimane zbiory (mogōm być pobrane zaś z serwera) + Trefiōłch tyn link we zaufanym zdrzōdle + Zaroz zweryfikujesz klucz OMEMO %1$s bez klikniyńcie w link. To je bezpiecznie ino, kej link je ze zaufanego zdrzōdła, kaj ino %2$s mōg go ôpublikować. + Zaroz zweryfikujesz klucze OMEMO swojego kōnta. To je bezpieczne ino jeźli ôtwiyrosz tyn link ze zaufanego zdrzōdła, kaj ino ty możesz ôpublikować tyn link. + Dalij + Zweryfikuj klucze OMEMO + Pokoż niyaktywne + Skryj niyaktywne + Przestōń ufać maszinie + Je żeś zicher, iże chcesz cofnōńć weryfikacyjo tyj masziny\? +\nTa maszina, i wiadōmości, co bydōm z nij przichodzić, bydōm ôznaczane za niyzaufane. + + %d sekunda + %d sekundy + %d sekund + + + %d minuta + %d minuty + %d minut + + + %d godzina + %d godziny + %d godzin + + + %d dziyń + %d dni + %d dni + + + %d tydziyń + %d tydnie + %d tydni + + + %d miesiōnc + %d miesiōnce + %d miesiyncy + + Autōmatyczne wymazowanie wiadōmości + Autōmatycznie wymazuj z tyj masziny wiadōmości starsze aniżeli skōnfigurowany ôkres czasu. + Szyfrowanie wiadōmości + Bez pobiyranio wiadōmości bez lokalny ôkres retyncyje. + Kōmpresowanie filmu + Powiōnzane godki zawarte. + Kōntakt zablokowany. + Powiadōmiynia ôd cudzych ludzi + Powiadōmiej przi wiadōmościach i połōnczyniach ôd cudzych ludzi. + Prziszła widōmość ôd kogoś cudzego + Zablokuj cudzo ôsoba + Zablokuj cołko dōmyna + teroz online + Sprōbuj zaś ôdszyfrować + Feler sesyje + Starszy mechanizm SASL + Serwer wymogo registracyje na strōnie + Ôtwōrz strōna + Niy szło znojś aplikacyje do ôtwarciŏ strōny + Powiadōmiynia heads-up + Pokazuj powiadōmiynia Heads-up + Dzisiej + Wczorej + Potwiyrdź miano ôd hosta ze pōmocōm DNSSEC + Certyfikaty serwera, co posiadajōm noleżne miano ôd hosta, sōm uznowane za zweryfikowane + Certyfikat niy zawiyro adresy XMPP + czyńściowo + Nagrej film + Skopiyruj do skrytki + Wiadōmość skopiyrowano do skrytki + Wiadōmość + Prywatne wiadōmości sōm zastawiōne + Aplikacyje chrōniōne + Coby dostować wiadōmości, kedy ekran je zastawiōny, musisz przidać Conversations do listy chrōniōnych aplikacyji. + Zaakceptować niyznōmy certyfikat\? + Certyfikat ôd serwera niy ma podpisany ôd znōmego Amtu Certyfikacyje. + Zaakceptować niypasujōnce miano ôd serwera\? + Niy idzie potwiyrdzić serwera za “%s. Certyfikat je ważny ino dlo: + Chcesz kōntynuować połōnczynie\? + Informacyje certyfikatu: + Roz + Skaner kodōw QR potrzebuje dostympu do aparatu + Przewiń na spodek + Przewiń na spodek po wysłaniu wiadōmości + Edytuj kōmunikat statusu + Edytuj kōmunikat statusu + Zastow szyfrowanie + %1$s niy mogło wysłać zaszyfrowanyj wiadōmości do %2$s. Możliwe, iże kōntakt używo starego serwera abo klijynta, co niy spiyro OMEMO. + Niy podarziło sie pobranie listy maszin + Niy podarziło sie pobranie kluczy szyfrowanio + Podpowiydź: W niykerych przipodkach może pōmōc wzajymne przidanie sie do listy kōntaktōw. + Na zicher chcesz zastawić szyfrowanie OMEMO dlo tyj kōnwersacyje\? +\nAdministratōr twojigo serwera bydzie mōg czytać twoje wiadōmości, ale może to być jedyny spōsōb, coby kōmunikować sie z ludźmi, co używajōm starych klijyntōw. + Zastow teroz + Cychōnek: + Szyfrowanie OMEMO + OMEMO bydzie dycki używane w godkach 1:1 i prywatnych grupowych godkach. + OMEMO bydzie używane wychodnie przi nowych godkach. + OMEMO bydzie musiało być włōnczōne ryncznie przi nowych godkach. + Stwōrz Skrōt + Srogość czciōnki + Relatywno srogość czciōnki używanyj we aplikacyji. + Włōnczōne wychodnie + Zastawiōne wychodnie + Mało + Strzednio + Srogo + Wiadōmość niy była zaszyfrowano dlo tyj masziny. + Niy szło ôdszyfrować wiadōmości OMEMO. + cofnij + Udostympnianie lokalizacyje je zastawiōne + Zablokuj pozycyjo + Ôdblokuj pozycyjo + Skopiyruj lokalizacyjo + Udostympnij lokalizacyjo + Kerōnki + Udostympniej lokalizacyjo + Pokazuj lokalizacyjo + Udostympnij + Niy idzie zaczōńć nagrowanio + Czekej… + Przizwōl %1$s na dostymp do mikrofōnu + Szukej we widōmościach + GIF + Pokoż kōnwersacyjo + Przidowek Udostympnianio Lokalizacyje + Użyj Przidowka Udostympnianio Lokalizacyje zamiast wbudowanyj karty + Skopiyruj URL + Skopiyruj adresa XMPP + Udostympnianie zbiorōw bez HTTP S3 + Bezpostrzednie wyszukowanie + Na ekranie “Zacznij kōnwersacyjo” ôtwōrz tastatura i wraź kursōr w polu wyszukowanio + Awatar kōnwersacyje + Serwer niy spiyro awatarōw kōnwersacyje + Ino posiedziciel może zmiynić awatar kōnwersacyje + Miano ôd kōntaktu + Pseudōnim + Miano + Niy trza podować miana + Miano ôd kōnferyncyje + Ta kōnferyncyjo ôstała wymazano + Niy idzie zaczōńć nagrowanio + Usuga na piyrszym planie + Ta kategoryjo powiadōmiyń je używano, coby pokazować stałe powiadōmiynie, co ôznaczo, iże %1$s funguje. + Wiadōmość Statusu + Problymy z połōnczyniym + Ta kategoryjo powiadōmiyń je używano, coby pokazować stałe powiadōmiynie, co ôznaczo, iże Conversations mo problymy z połōnczyniym. + Wiadōmości + Połōnczynia + Wiadōmości + Połōnczynia, co przichodzōm + Połōnczynia, co wychodzōm + Ciche wiadōmości + Ta kategoryjo powiadōmiyń je używano coby pokazować powiadōmiynia, co niy powodujōm żodnych klangōw. Bez tyn przikłod w czasie aktywności na inkszyj maszinie (ôkres karyncyje). + Niypodarzōne wysyłki + Sztalōnki powiadōmiyń wiadōmości + Sztalōnki powiadōmiyń dlo połōnczyń, co przichodzōm + Ważność, Klang, Wibracyjo + Kōmpresyjo wideo + Pokoż media + Uczestnicy + Przeglōndarka mediōw + Zbiōr pōminiynty skirz naruszynio bezpiyczyństwa. + Jakość wideo + Niższo jakość gwarantuje myńszo srogość + Postrzednio (360p) + Wysoko (720p) + pociepniynte + Już tworzisz nowo wiadōmość. + Funkcyjo niyzaimplymyntowano + Niynoleżny kod kraju + Ôbier kroj + numer telefōnu + Zweryfikuj swōj numer telefōnu + Quicksy wyśle SMS (mogōm być naliczane płaty) coby zweryfikować numer telefōnu. Wpisz kod ôd kraju i numer telefōnu: + Zweryfikujymy numer telefōnu

%s

Zgodzo sie wszysko, abo chcesz zmiynić numer\?
+ %s to niy ma noleżny numer telefōnu. + Wkludź swōj numer telefōnu. + Przeszukej kraje + Zweryfikuj %s + Wysłali my SMS do %s. + Wysłali my dalszy SMS z 6 cyfrowym kodym. + Wkludź 6-cyfrowy kod PIN niżyj. + Wyślij SMS zaś + Wyślij SMS zaś (%s) + Czekej (%s) + nazod + Autōmatycznie bōł wrażōny prowdopodobny PIN ze skrytki. + Wkludź 6-cyfrowy PIN. + Na zicher chcesz przerwać procedura registracyje\? + Ja + Niy + Weryfikowanie… + Żōndanie SMS… + Wkludzōny PIN je niynoleżny. + Wysłany ôd nos PIN straciōł ważność. + Niyznōmy feler necu. + Niyznōmo ôdpowiydź serwera. + Niy idzie sie połōnczyć z serwerym. + Niy idzie dostać bezpiecznego połōnczynio. + Niy szło znojś serwera. + Coś poszło źle przi przetworzaniu twojigo żōndanio. + Niynoleżny wert używocza + Tymczasowo niydostympne. Sprōbuj niyskorzij. + Brak połōnczynio z necym. + Sprōbuj zaś za %s + Limit pytań spotrzebowany + Za moc prōb + Używosz zastarzałyj wersyje aplikacyje. + Aktualizuj + Twōj numer telefōnu je aktualnie wlogowany na inkszyj maszinie. + Wkludź swoje miano, coby ludzie, co cie majōm we kōntaktach, wiedzieli, fto żeś je. + Twoje miano + Wkludź swoje miano + Użyj knefla edycyje coby nasztalować swoje miano. + Ôdciepżōndanie + Zainstaluj Orbot + Puść Orbot + Żodno marketowo aplikacyjo niy je zainstalowano. + Tyn kanał sprawi, iże twoja adresa XMPP bydzie publiczno + e-book + Ôryginalne (niyskōmpresowane) + Ôtwōrz ze pōmocōm… + Profilowy ôbrozek Conversations + Ôbier kōnto + Prziwrōć kopijo ibryczno + Prziwrōć + Wkludź swoje hasło do kōnta %s coby prziwrōcić kopijo ibryczno. + Niy używej kopije ibrycznyj, coby klōnować (puszczać rōwnolygle) instalacyjo. Prziwrocanie kopije je przeznaczōne ino do migracyje abo kedy maszina była stracōno. + Niy idzie prziwrōcić kopije ibrycznyj. + Niy idzie ôdszyfrować kopije ibrycznyj. Je hasło noleżne\? + Kopijo i Prziwrocanie + Wkludź adresa XMPP + Nowo grupowo godka + Prziwstōń do publicznego kanału + Nowo prywatno grupowo godka + Nowy publiczny kanał + Miano ôd kanału + Adresa XMPP + Podej miano ôd kanału + Podej adresa XMPP + To je adresa XMPP. Podej miano. + Tworzynie kanału publicznego… + Tyn kanał już istniyje + Przistympujesz do istniyjōncego kanału + Niy szło spamiyntać kōnfiguracyje ôd kanału + Przizwōl wszyskim na zmiana tymatu + Przizwōl wszyskim na zaproszanie inkszych + Kożdy może zmiyniać tymat. + Posiedziciele mogōm zmiyniać tymat. + Administratorzi mogōm zmiyniać tymat. + Posiedziciele mogōm zaproszać inkszych. + Kożdy może zaproszać inkszych. + Adresy XMPP widzialne dlo administratorōw. + Adresy XMPP widzialne dlo wszyskich. + Tyn publiczny kanał niy mo uczestnikōw. Zaproś swoje kōntakty abo użyj udostympnianio, coby ôpublikować adresa XMPP. + Ta prywatno grupowo godka niy mo uczestnikōw. + Regyruj uprawniyniami + Wyszukej uczestnikōw + Zbiōr je za srogi + Przidej + Ôdkryj kanały + Wyszukej kanał + Możliwe naruszynie prywatności! + Ôdkrywanie kanałōw używo usugi trzecij fiyrmy <a href=https://search.jabber.network>search.jabber.network</a>. <br><br>Użycie tyj funkcyje prześle dō nij twoja adresa IP jak tyż kryteria wyszukowanio. Wejzdrzij na <a href=https://search.jabber.network/privacy>Polityka Prywatności</a>, coby dostać wiyncyj informacyji. + Już mōm kōnto + Przidej kōnto, co juz istniyje + Zaregistruj nowe kōnto + To wyglōndo jak miano ôd dōmyny + Przidej tak by tak + To wyglōndo jak adresa ôd kanału + Udostympnij zbiory kopiji ibrycznych + Kopijo ibryczno Conversations + Zdarzynie + Ôtwōrz kopijo ibryczno + Wybrany zbiōr to niy ma zbiōr kopije ibrycznyj Conversations + To kōnto już je nasztalowane + Podej hasło dlo tego kōnta + Niy idzie wykōnać tyj akcyje + Przistōmp do publicznego kanału… + Aplikacyjo, co udostympnio, niy dała zwōle na dostymp do tego zbioru. + Grupowe godki i kanały + jabber.network + Serwer lokalny + Wiynkszość używoczōw powinna ôbrać “jabber.network” dlo lepszych dorad ze cołkigo ekosystymu XMPP. + Metoda ôdkrywanio kanałōw + Kopijo ibryczno + Ô aplikacyji + Włōncz kōnto + Zazwōń + Przichodzi połōnczynie + Przichodzi połōnczynie wideo + Łōnczynie + Połōnczōny + Łōnczynie zaś + Akceptowanie połōnczynio + Kōńczynie połōnczynio + Ôdbier + Ôdciep + Wyszukowanie maszin + Zwōniynie + Zajynte + Niy idzie wykōnać połōnczynio + Połōnczynie serwane + Połōnczynie pociepniynte + Feler aplikacyje + Problym weryfikacyje + Rozłōncz + Połōnczynie wychodzi + Połōnczynie wideo wychodzi + Łōnczynie zaś + Łōnczynie zaś + Zastow Tor coby zwōnić + Połōnczynie przichodzōnce + Połōnczynie przichodzōnce · %s + Niyôdebrane · %s + Połōnczynie wychodzōnce + Połōnczynie wychodzōnce · %s + Niyôdebrane połōnczynie + Połōnczynie audio + Połōnczynie wideo + Pōmoc + Przejdź do kōnwersacyje + Twōj mikrofōn je niydostympny + Możesz mieć ino jedno połōnczynie w jednyj chwili. + Wrōć do trwajōncego połōnczynio + Niy idzie zmiynić fotoaparatu + Przidej do ôblubiōnych + Wymaż ze ôblubiōnych + Śledzynie GPX + Niy szło skorygować wiadōmości + Wszyske kōnwersacyje + Ta kōnwersacyjo + Twōj awatar + Awatar ôd %s + Zaszyfrowane bez OMEMO + Zaszyfrowane bez OpenPGP + Niyzaszyfrowane + Zawrzij + Nagrej głosowo wiadōmość + Grej źwiynk + Spauzuj źwiynk + Przidej kōntakt, stwōrz abo przistōmp do grupowyj godki, abo ôdkrywej kanały + + Pokoż %1$d uczestnika + Pokoż %1$d uczestnikōw + Pokoż %1$d uczestnikōw + + + Wiadōmość niy mogła być dolifrowano + Wielaś wiadōmości niy mogło być dolifrowanych + Wielaś wiadōmości niy mogło być dolifrowanych + + Niypodarzōne lifrowania + Wiyncyj ôpcyjōw + Żodno aplikacyjo niyznojdziōno + Zaproś do Conversations + Niy idzie przetworzić zaproszynio + Serwer niy spiyro gynerowanio zaproszyń + Żodne aktywne kōnta niy spiyrajōm tyj funkcyje + Tworzynie kopije ibrycznyj je puszczōne. Dostaniesz powiadōmiynie, jak bydzie gotowo. + Niy idzie włōnczyć wideo. + Dokumynt ze samym tekstym + Registracyjo kōnt niy je spiyrano + Żodno adresa XMPP niyznojdziōno +
diff --git a/src/main/res/values-tr-rTR/strings.xml b/src/main/res/values-tr-rTR/strings.xml index 266255aed..283fee4d3 100644 --- a/src/main/res/values-tr-rTR/strings.xml +++ b/src/main/res/values-tr-rTR/strings.xml @@ -976,5 +976,4 @@ Düz metin dosyası Hesap kayıtları desteklenmemektedir. Herhangi bir XMPP adresi bulunamadı - - + diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index 6c11f159d..4878bfc42 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -963,5 +963,4 @@ 纯文本文档 不支持注册账户 未找到 XMPP 地址 - - + diff --git a/src/quicksy/res/values-szl/strings.xml b/src/quicksy/res/values-szl/strings.xml new file mode 100644 index 000000000..d3f435559 --- /dev/null +++ b/src/quicksy/res/values-szl/strings.xml @@ -0,0 +1,12 @@ + + + Czas, jak dugo Quicksy je cichy po ôboczyniu aktywności na inkszyj maszinie + Jak wysyłosz sztreki sztapla, to pōmogosz przi budowaniu Quicksy + Informuj wszyske twoje kōntakty ô tym, kedy używosz Quicksy + Żeby durch dostować powiadōmiynia, nawet jak ekran je zgaszōny, musisz dodać Quicksy do listy chrōniōnych aplikacyji. + Profilowy ôbrozek Quicksy + Quicksy niy ma dostympne we twojim kraju. + Niy idzie zweryfikować tożsamości ôd serwera. + Niyznōmy feler bezpieczyństwa. + Przekroczynie czasu przi łōnczyniu ze serwerym. + From 467e34e2feb6a13c33ea24d9a1e9345689931a8c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 14 Jun 2022 08:52:11 +0200 Subject: [PATCH 111/394] bump various libraries --- build.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 2e1870b2f..66f433fe1 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.2.0' + classpath 'com.android.tools.build:gradle:7.2.1' } } @@ -33,7 +33,7 @@ configurations { dependencies { implementation 'androidx.viewpager:viewpager:1.0.0' - playstoreImplementation('com.google.firebase:firebase-messaging:23.0.3') { + playstoreImplementation('com.google.firebase:firebase-messaging:23.0.5') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' @@ -42,7 +42,7 @@ dependencies { quicksyPlaystoreImplementation 'com.google.android.gms:play-services-auth-api-phone:18.0.1' implementation 'org.sufficientlysecure:openpgp-api:10.0' implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0' - implementation 'androidx.appcompat:appcompat:1.4.1' + implementation 'androidx.appcompat:appcompat:1.4.2' implementation 'androidx.exifinterface:exifinterface:1.3.3' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' @@ -74,7 +74,7 @@ dependencies { implementation "com.squareup.okhttp3:okhttp:4.9.3" implementation 'com.google.guava:guava:30.1.1-android' - quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.44' + quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.49' implementation fileTree(include: ['libwebrtc-m99.aar'], dir: 'libs') } From 17b9ca9dec1796f9b9f598b3a93cec690a6f5ab5 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 22 Jun 2022 07:18:07 +0200 Subject: [PATCH 112/394] =?UTF-8?q?use=20item=20id=20'current'=20for=20nic?= =?UTF-8?q?k=20as=20fallback=20as=20per=20XEP-0060=20=C2=A712.20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/eu/siacs/conversations/generator/IqGenerator.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 198a2e71a..2776aa4f0 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -131,6 +131,7 @@ public IqPacket retrieveBookmarks() { public IqPacket publishNick(String nick) { final Element item = new Element("item"); + item.setAttribute("id", "current"); item.addChild("nick", Namespace.NICK).setContent(nick); return publish(Namespace.NICK, item); } From 42bd8e6d619207b46b0ecf9262020379d78a85ee Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 22 Jun 2022 07:56:44 +0200 Subject: [PATCH 113/394] minor code clean up --- .../java/eu/siacs/conversations/ui/ConversationsActivity.java | 4 ++-- .../conversations/xmpp/jingle/JingleConnectionManager.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java index 308e139fc..6d0a03f8b 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java @@ -145,7 +145,7 @@ void onBackendConnected() { return; } xmppConnectionService.getNotificationService().setIsInForeground(true); - Intent intent = pendingViewIntent.pop(); + final Intent intent = pendingViewIntent.pop(); if (intent != null) { if (processViewIntent(intent)) { if (binding.secondaryFragment != null) { @@ -159,7 +159,7 @@ void onBackendConnected() { notifyFragmentOfBackendConnected(id); } - ActivityResult activityResult = postponedActivityResult.pop(); + final ActivityResult activityResult = postponedActivityResult.pop(); if (activityResult != null) { handleActivityResult(activityResult); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 533974a0f..0f9694915 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -569,9 +569,9 @@ public Optional getOngoingRtpConnection(final Contact contact } } synchronized (this.rtpSessionProposals) { - for (Map.Entry entry : + for (final Map.Entry entry : this.rtpSessionProposals.entrySet()) { - RtpSessionProposal proposal = entry.getKey(); + final RtpSessionProposal proposal = entry.getKey(); if (proposal.account == contact.getAccount() && contact.getJid().asBareJid().equals(proposal.with)) { final DeviceDiscoveryState preexistingState = entry.getValue(); From d9fa535f00a7c8daeed8dc8e0b6bd93650f23fab Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 22 Jun 2022 08:02:59 +0200 Subject: [PATCH 114/394] pulled translations from transifex --- src/main/res/values-de/strings.xml | 16 +++++++++------- src/main/res/values-es/strings.xml | 4 +++- src/main/res/values-gl/strings.xml | 4 +++- src/main/res/values-pt-rBR/strings.xml | 4 +++- src/main/res/values-ro-rRO/strings.xml | 4 +++- src/main/res/values-zh-rCN/strings.xml | 4 +++- 6 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index b54309ed2..44632eca0 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -401,8 +401,8 @@ XMPP-Adressen für alle sichtbar machen Channel wird moderiert Du bist kein Mitglied - Gruppenchatoptionen wurden modifiziert! - Gruppenchatoptionen konnten nicht modifiziert werden + Gruppenchatoptionen wurden geändert! + Gruppenchatoptionen konnten nicht geändert werden Niemals Bis auf Weiteres Schlummern @@ -440,7 +440,7 @@ Zertifizierungsstellen nicht vertrauen Alle Zertifikate müssen manuell bestätigt werden Zertifikate löschen - Als vertrauenswürdig bestätigte Zertifikate löschen + Manuell bestätigte Zertifikate löschen Keine manuell bestätigten Zertifikate Zertifikate löschen Auswahl löschen @@ -481,13 +481,13 @@ Hostname- und Port-Optionen bei Kontoeinrichtung anzeigen xmpp.domain.de Mit Zertifikat anmelden - Zertifikat konnte nicht ausgewertet werden + Zertifikat konnte nicht verarbeitet werden Archivierungseinstellungen Archivierungseinstellungen des Servers Archivierungseinstellungen werden abgerufen. Bitte warten… Archivierungseinstellungen konnten nicht abgerufen werden CAPTCHA erforderlich - Gib den Text von obigem Bild ein + Gib den Text aus dem Bild oben ein Nicht vertrauenswürdige Zertifikatskette XMPP-Adresse stimmt nicht dem Zertifikat überein Zertifikat erneuern @@ -968,7 +968,7 @@ Weitere Optionen Keine Anwendung gefunden Einladung zu Conversations - Einladung kann nicht gelesen werden + Einladung kann nicht verarbeitet werden Server unterstützt keine Generierung von Einladungen Keine aktiven Konten unterstützen diese Funktion Die Sicherung wurde gestartet. Du bekommst eine Benachrichtigung, sobald sie fertig ist. @@ -976,4 +976,6 @@ Textdokument Kontoregistrierungen werden nicht unterstützt Keine XMPP-Adresse gefunden - + Temporärer Authentifizierungsfehler + + diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index d2c5b8afd..202d70fc7 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -989,4 +989,6 @@ Documento de texto plano Los registros de cuenta no están soportados Dirección XMPP no encontrada - + Fallo temporal de autenticación + + diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 76b0f937a..338ff69b8 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -976,4 +976,6 @@ Documento de texto plano Non está permitido o rexistro de novas contas Non se atopa un enderezo XMPP - + Fallo temporal da autenticación + + diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index 9bcf1aaea..57fe94c67 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -989,4 +989,6 @@ Documento em texto puro O registro de contas não está ativo Não foi encontrado nenhum endereço XMPP - + Falha temporária na autenticação + + diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index a57f9d0eb..c0c15cb4a 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -989,4 +989,6 @@ Document text Nu este posibilă înregistrarea unui cont Nu a fost găsită o adresă XMPP - + Eroare temporară de autentificare + + diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index 4878bfc42..be4aa4fac 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -963,4 +963,6 @@ 纯文本文档 不支持注册账户 未找到 XMPP 地址 - + 临时认证失败 + + From 84e08933f91844a58c7d85c319ecf401cb43889e Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 27 Jun 2022 13:34:26 -0500 Subject: [PATCH 115/394] A Quicky user can be a stranger At some point a refactor changed this check from checking that the quicksy domain itself is talking to you, to checking that anyone using quicksy is talking to you, which breaks the notifications from strangers setting for quicksy users. --- src/main/java/eu/siacs/conversations/entities/Conversation.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index aeba1f14c..4a825fbb3 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -1076,7 +1076,7 @@ public boolean isWithStranger() { && !contact.isOwnServer() && !contact.showInContactList() && !contact.isSelf() - && !JidHelper.isQuicksyDomain(contact.getJid()) + && !(contact.getJid().isDomainJid() && JidHelper.isQuicksyDomain(contact.getJid())) && sentMessagesCount() == 0; } From 206b09919bb71115097d072f3f0caa6c7f3d85b7 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 1 Jul 2022 13:07:34 +0200 Subject: [PATCH 116/394] bump dependencies --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 66f433fe1..ad5a88b9c 100644 --- a/build.gradle +++ b/build.gradle @@ -33,7 +33,7 @@ configurations { dependencies { implementation 'androidx.viewpager:viewpager:1.0.0' - playstoreImplementation('com.google.firebase:firebase-messaging:23.0.5') { + playstoreImplementation('com.google.firebase:firebase-messaging:23.0.6') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' From 65daeff1125a6d475dabb45f9a993dfee9fc41fd Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 1 Jul 2022 15:53:24 +0200 Subject: [PATCH 117/394] pulled translations from transifex --- src/main/res/values-de/strings.xml | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 44632eca0..8372ff2dc 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -178,7 +178,7 @@ OMEMO Konto löschen Vorübergehend abschalten - Avatar veröffentlichen + Profilbild veröffentlichen Öffentlichen OpenPGP-Schlüssel veröffentlichen Öffentlichen OpenPGP-Schlüssel verwerfen Bist du sicher, dass du deinen öffentlichen OpenPGP-Schlüssel aus deiner Anwesenheitsmitteilung entfernen möchtest?\nDeine Kontakte können dir dann keine OpenPGP-verschlüsselten Nachrichten senden. @@ -250,7 +250,7 @@ Channel konnte nicht gelöscht werden Gruppenchatthema bearbeiten Thema - Gruppenchat wird beigetreten… + Gruppenchat beigetreten… Verlassen Kontakt hat dich zur Kontaktliste hinzugefügt Auch hinzufügen @@ -259,13 +259,13 @@ %1$s +%2$d andere haben bis zu diesem Punkt gelesen Alle haben bis zu diesem Punkt gelesen Veröffentlichen - Avatar antippen, um ein Bild aus der Galerie auszuwählen + Profilbild antippen, um ein Bild aus der Galerie auszuwählen Veröffentliche… Der Server hat die Veröffentlichung des Avatars abgelehnt. Bild konnte nicht konvertiert werden - Avatar kann nicht gespeichert werden + Profilbild kann nicht gespeichert werden (Oder klicke lange, um den Standard wiederherzustellen) - Dein Server unterstützt die Veröffentlichung von Avataren nicht + Dein Server unterstützt die Veröffentlichung von Profilbildern nicht private Nachricht: an %s Private Nachricht an %s senden @@ -356,7 +356,7 @@ Benachrichtigungen aktivieren Keinen Gruppenchatserver gefunden Gruppenchat konnte nicht erstellt werden - Konto-Avatar + Konto-Profilbild OMEMO-Fingerabdruck in Zwischenablage kopieren OMEMO-Schlüssel erneuern Geräte entfernen @@ -420,7 +420,7 @@ PDF-Dokument Android App Kontakt - Avatar wurde veröffentlicht! + Profilbild wurde veröffentlicht! %s wird gesendet %s wird angeboten Offline verstecken @@ -477,7 +477,7 @@ Als Beschäftigt anzeigen, wenn sich das Gerät im lautlosen Modus befindet Vibration als Lautlos behandeln Als Beschäftigt anzeigen, wenn das Gerät auf Vibration eingestellt ist - Erweiterte Verbindungsoptionen + Erweiterte Verbindungseinstellungen Hostname- und Port-Optionen bei Kontoeinrichtung anzeigen xmpp.domain.de Mit Zertifikat anmelden @@ -515,7 +515,7 @@ %1$s den Zugriff auf den externen Speicher gewähren %1$s den Zugriff auf die Kamera gewähren Mit Kontakten synchronisieren - %1$s möchte die Erlaubnis, auf deine Kontakte zuzugreifen, um sie mit deiner XMPP-Kontaktliste abzugleichen.\nDadurch werden die vollständigen Namen und Avatare deiner Kontakte angezeigt.\n\n%1$s liest nur dein Adressbuch und gleicht es lokal ab, ohne dass etwas auf deinen Server hochgeladen wird. + %1$s möchte die Erlaubnis, auf deine Kontakte zuzugreifen, um sie mit deiner XMPP-Kontaktliste abzugleichen.\nDadurch werden die vollständigen Namen und Profilbilder deiner Kontakte angezeigt.\n\n%1$s liest nur dein Adressbuch und gleicht es lokal ab, ohne dass etwas auf deinen Server hochgeladen wird.
Wir werden keine Kopie dieser Telefonnummern speichern.\n\nFür weitere Informationen lies unsere Datenschutzerklärung.

Du wirst nun gefragt, ob du den Zugriff auf deine Kontakte erlauben möchtest.]]>
Bei allen Nachrichten benachrichtigen Nur benachrichtigen, wenn ich erwähnt werde @@ -595,7 +595,7 @@ OMEMO-Identitäten zurücksetzen Erzeuge neue OMEMO-Schlüssel. Alle deine Kontakte müssen sie erneut verifizieren. Verwende dies nur als letztes Mittel. Ausgewählte Schlüssel löschen - Du musst verbunden sein, um deinen Avatar zu veröffentlichen. + Du musst verbunden sein, um deinen Profilbild zu veröffentlichen. Zeige Fehlermeldung Fehlermeldung Datensparmodus aktiv @@ -745,9 +745,9 @@ HTTP-Dateifreigabe für S3 Direkte Suche Beim Dialog \'Unterhaltung beginnen\' Tastatur öffnen und den Cursor im Suchfeld platzieren - Gruppenchat-Avatar - Host unterstützt keine Gruppenchat-Avatare - Nur der Eigentümer kann den Gruppenchat-Avatar ändern + Gruppenchat-Profilbild + Host unterstützt keine Gruppenchat-Profilbilder + Nur der Eigentümer kann das Gruppenchat-Profilbild ändern Kontaktname Nickname Name @@ -946,8 +946,8 @@ Nachricht konnte nicht korrigiert werden Alle Unterhaltungen Diese Unterhaltung - Dein Avatar - Avatar für %s + Dein Profilbild + Profilbild für %s Verschlüsselt mit OMEMO Verschlüsselt mit OpenPGP Nicht verschlüsselt From 73c7d76bd6f729ac851747f545eca0efe80c3983 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 1 Jul 2022 15:53:36 +0200 Subject: [PATCH 118/394] add local only flag to foreground service --- .../services/NotificationService.java | 764 ++++++++++++------ 1 file changed, 529 insertions(+), 235 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index ca4499300..abe6e9e11 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -79,7 +79,8 @@ public class NotificationService { - private static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE = Executors.newSingleThreadScheduledExecutor(); + private static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE = + Executors.newSingleThreadScheduledExecutor(); public static final Object CATCHUP_LOCK = new Object(); @@ -137,7 +138,8 @@ private static boolean isImageMessage(Message message) { @RequiresApi(api = Build.VERSION_CODES.O) void initializeChannels() { final Context c = mXmppConnectionService; - final NotificationManager notificationManager = c.getSystemService(NotificationManager.class); + final NotificationManager notificationManager = + c.getSystemService(NotificationManager.class); if (notificationManager == null) { return; } @@ -145,41 +147,60 @@ void initializeChannels() { notificationManager.deleteNotificationChannel("export"); notificationManager.deleteNotificationChannel("incoming_calls"); - notificationManager.createNotificationChannelGroup(new NotificationChannelGroup("status", c.getString(R.string.notification_group_status_information))); - notificationManager.createNotificationChannelGroup(new NotificationChannelGroup("chats", c.getString(R.string.notification_group_messages))); - notificationManager.createNotificationChannelGroup(new NotificationChannelGroup("calls", c.getString(R.string.notification_group_calls))); - final NotificationChannel foregroundServiceChannel = new NotificationChannel("foreground", - c.getString(R.string.foreground_service_channel_name), - NotificationManager.IMPORTANCE_MIN); - foregroundServiceChannel.setDescription(c.getString(R.string.foreground_service_channel_description, c.getString(R.string.app_name))); + notificationManager.createNotificationChannelGroup( + new NotificationChannelGroup( + "status", c.getString(R.string.notification_group_status_information))); + notificationManager.createNotificationChannelGroup( + new NotificationChannelGroup( + "chats", c.getString(R.string.notification_group_messages))); + notificationManager.createNotificationChannelGroup( + new NotificationChannelGroup( + "calls", c.getString(R.string.notification_group_calls))); + final NotificationChannel foregroundServiceChannel = + new NotificationChannel( + "foreground", + c.getString(R.string.foreground_service_channel_name), + NotificationManager.IMPORTANCE_MIN); + foregroundServiceChannel.setDescription( + c.getString( + R.string.foreground_service_channel_description, + c.getString(R.string.app_name))); foregroundServiceChannel.setShowBadge(false); foregroundServiceChannel.setGroup("status"); notificationManager.createNotificationChannel(foregroundServiceChannel); - final NotificationChannel errorChannel = new NotificationChannel("error", - c.getString(R.string.error_channel_name), - NotificationManager.IMPORTANCE_LOW); + final NotificationChannel errorChannel = + new NotificationChannel( + "error", + c.getString(R.string.error_channel_name), + NotificationManager.IMPORTANCE_LOW); errorChannel.setDescription(c.getString(R.string.error_channel_description)); errorChannel.setShowBadge(false); errorChannel.setGroup("status"); notificationManager.createNotificationChannel(errorChannel); - final NotificationChannel videoCompressionChannel = new NotificationChannel("compression", - c.getString(R.string.video_compression_channel_name), - NotificationManager.IMPORTANCE_LOW); + final NotificationChannel videoCompressionChannel = + new NotificationChannel( + "compression", + c.getString(R.string.video_compression_channel_name), + NotificationManager.IMPORTANCE_LOW); videoCompressionChannel.setShowBadge(false); videoCompressionChannel.setGroup("status"); notificationManager.createNotificationChannel(videoCompressionChannel); - final NotificationChannel exportChannel = new NotificationChannel("backup", - c.getString(R.string.backup_channel_name), - NotificationManager.IMPORTANCE_LOW); + final NotificationChannel exportChannel = + new NotificationChannel( + "backup", + c.getString(R.string.backup_channel_name), + NotificationManager.IMPORTANCE_LOW); exportChannel.setShowBadge(false); exportChannel.setGroup("status"); notificationManager.createNotificationChannel(exportChannel); - final NotificationChannel incomingCallsChannel = new NotificationChannel(INCOMING_CALLS_NOTIFICATION_CHANNEL, - c.getString(R.string.incoming_calls_channel_name), - NotificationManager.IMPORTANCE_HIGH); + final NotificationChannel incomingCallsChannel = + new NotificationChannel( + INCOMING_CALLS_NOTIFICATION_CHANNEL, + c.getString(R.string.incoming_calls_channel_name), + NotificationManager.IMPORTANCE_HIGH); incomingCallsChannel.setSound(null, null); incomingCallsChannel.setShowBadge(false); incomingCallsChannel.setLightColor(LED_COLOR); @@ -189,22 +210,27 @@ void initializeChannels() { incomingCallsChannel.enableVibration(false); notificationManager.createNotificationChannel(incomingCallsChannel); - final NotificationChannel ongoingCallsChannel = new NotificationChannel("ongoing_calls", - c.getString(R.string.ongoing_calls_channel_name), - NotificationManager.IMPORTANCE_LOW); + final NotificationChannel ongoingCallsChannel = + new NotificationChannel( + "ongoing_calls", + c.getString(R.string.ongoing_calls_channel_name), + NotificationManager.IMPORTANCE_LOW); ongoingCallsChannel.setShowBadge(false); ongoingCallsChannel.setGroup("calls"); notificationManager.createNotificationChannel(ongoingCallsChannel); - - final NotificationChannel messagesChannel = new NotificationChannel("messages", - c.getString(R.string.messages_channel_name), - NotificationManager.IMPORTANCE_HIGH); + final NotificationChannel messagesChannel = + new NotificationChannel( + "messages", + c.getString(R.string.messages_channel_name), + NotificationManager.IMPORTANCE_HIGH); messagesChannel.setShowBadge(true); - messagesChannel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), new AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT) - .build()); + messagesChannel.setSound( + RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), + new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT) + .build()); messagesChannel.setLightColor(LED_COLOR); final int dat = 70; final long[] pattern = {0, 3 * dat, dat, dat}; @@ -213,19 +239,24 @@ void initializeChannels() { messagesChannel.enableLights(true); messagesChannel.setGroup("chats"); notificationManager.createNotificationChannel(messagesChannel); - final NotificationChannel silentMessagesChannel = new NotificationChannel("silent_messages", - c.getString(R.string.silent_messages_channel_name), - NotificationManager.IMPORTANCE_LOW); - silentMessagesChannel.setDescription(c.getString(R.string.silent_messages_channel_description)); + final NotificationChannel silentMessagesChannel = + new NotificationChannel( + "silent_messages", + c.getString(R.string.silent_messages_channel_name), + NotificationManager.IMPORTANCE_LOW); + silentMessagesChannel.setDescription( + c.getString(R.string.silent_messages_channel_description)); silentMessagesChannel.setShowBadge(true); silentMessagesChannel.setLightColor(LED_COLOR); silentMessagesChannel.enableLights(true); silentMessagesChannel.setGroup("chats"); notificationManager.createNotificationChannel(silentMessagesChannel); - final NotificationChannel quietHoursChannel = new NotificationChannel("quiet_hours", - c.getString(R.string.title_pref_quiet_hours), - NotificationManager.IMPORTANCE_LOW); + final NotificationChannel quietHoursChannel = + new NotificationChannel( + "quiet_hours", + c.getString(R.string.title_pref_quiet_hours), + NotificationManager.IMPORTANCE_LOW); quietHoursChannel.setShowBadge(true); quietHoursChannel.setLightColor(LED_COLOR); quietHoursChannel.enableLights(true); @@ -235,14 +266,18 @@ void initializeChannels() { notificationManager.createNotificationChannel(quietHoursChannel); - final NotificationChannel deliveryFailedChannel = new NotificationChannel("delivery_failed", - c.getString(R.string.delivery_failed_channel_name), - NotificationManager.IMPORTANCE_DEFAULT); + final NotificationChannel deliveryFailedChannel = + new NotificationChannel( + "delivery_failed", + c.getString(R.string.delivery_failed_channel_name), + NotificationManager.IMPORTANCE_DEFAULT); deliveryFailedChannel.setShowBadge(false); - deliveryFailedChannel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), new AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT) - .build()); + deliveryFailedChannel.setSound( + RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), + new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT) + .build()); deliveryFailedChannel.setGroup("chats"); notificationManager.createNotificationChannel(deliveryFailedChannel); } @@ -256,16 +291,23 @@ private boolean notify(final Message message) { } public boolean notificationsFromStrangers() { - return mXmppConnectionService.getBooleanPreference("notifications_from_strangers", R.bool.notifications_from_strangers); + return mXmppConnectionService.getBooleanPreference( + "notifications_from_strangers", R.bool.notifications_from_strangers); } private boolean isQuietHours() { - if (!mXmppConnectionService.getBooleanPreference("enable_quiet_hours", R.bool.enable_quiet_hours)) { + if (!mXmppConnectionService.getBooleanPreference( + "enable_quiet_hours", R.bool.enable_quiet_hours)) { return false; } - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService); - final long startTime = TimePreference.minutesToTimestamp(preferences.getLong("quiet_hours_start", TimePreference.DEFAULT_VALUE)); - final long endTime = TimePreference.minutesToTimestamp(preferences.getLong("quiet_hours_end", TimePreference.DEFAULT_VALUE)); + final SharedPreferences preferences = + PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService); + final long startTime = + TimePreference.minutesToTimestamp( + preferences.getLong("quiet_hours_start", TimePreference.DEFAULT_VALUE)); + final long endTime = + TimePreference.minutesToTimestamp( + preferences.getLong("quiet_hours_end", TimePreference.DEFAULT_VALUE)); final long nowTime = Calendar.getInstance().getTimeInMillis(); if (endTime < startTime) { @@ -278,7 +320,8 @@ private boolean isQuietHours() { public void pushFromBacklog(final Message message) { if (notify(message)) { synchronized (notifications) { - getBacklogMessageCounter((Conversation) message.getConversation()).incrementAndGet(); + getBacklogMessageCounter((Conversation) message.getConversation()) + .incrementAndGet(); pushToStack(message); } } @@ -329,7 +372,9 @@ private List getBacklogConversations(Account account) { private int getBacklogMessageCount(Account account) { int count = 0; - for (Iterator> it = mBacklogMessageCounter.entrySet().iterator(); it.hasNext(); ) { + for (Iterator> it = + mBacklogMessageCounter.entrySet().iterator(); + it.hasNext(); ) { Map.Entry entry = it.next(); if (entry.getKey().getAccount() == account) { count += entry.getValue().get(); @@ -357,7 +402,8 @@ private void pushToStack(final Message message) { public void push(final Message message) { synchronized (CATCHUP_LOCK) { - final XmppConnection connection = message.getConversation().getAccount().getXmppConnection(); + final XmppConnection connection = + message.getConversation().getAccount().getXmppConnection(); if (connection != null && connection.isWaitingForSmCatchup()) { connection.incrementSmCatchupMessageCounter(); pushFromBacklog(message); @@ -370,25 +416,43 @@ public void push(final Message message) { public void pushFailedDelivery(final Message message) { final Conversation conversation = (Conversation) message.getConversation(); final boolean isScreenLocked = !mXmppConnectionService.isScreenLocked(); - if (this.mIsInForeground && isScreenLocked && this.mOpenConversation == message.getConversation()) { - Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": suppressing failed delivery notification because conversation is open"); + if (this.mIsInForeground + && isScreenLocked + && this.mOpenConversation == message.getConversation()) { + Log.d( + Config.LOGTAG, + message.getConversation().getAccount().getJid().asBareJid() + + ": suppressing failed delivery notification because conversation is open"); return; } final PendingIntent pendingIntent = createContentIntent(conversation); - final int notificationId = generateRequestCode(conversation, 0) + DELIVERY_FAILED_NOTIFICATION_ID; + final int notificationId = + generateRequestCode(conversation, 0) + DELIVERY_FAILED_NOTIFICATION_ID; final int failedDeliveries = conversation.countFailedDeliveries(); final Notification notification = new Builder(mXmppConnectionService, "delivery_failed") .setContentTitle(conversation.getName()) .setAutoCancel(true) .setSmallIcon(R.drawable.ic_error_white_24dp) - .setContentText(mXmppConnectionService.getResources().getQuantityText(R.plurals.some_messages_could_not_be_delivered, failedDeliveries)) + .setContentText( + mXmppConnectionService + .getResources() + .getQuantityText( + R.plurals.some_messages_could_not_be_delivered, + failedDeliveries)) .setGroup("delivery_failed") - .setContentIntent(pendingIntent).build(); + .setContentIntent(pendingIntent) + .build(); final Notification summaryNotification = new Builder(mXmppConnectionService, "delivery_failed") - .setContentTitle(mXmppConnectionService.getString(R.string.failed_deliveries)) - .setContentText(mXmppConnectionService.getResources().getQuantityText(R.plurals.some_messages_could_not_be_delivered, 1024)) + .setContentTitle( + mXmppConnectionService.getString(R.string.failed_deliveries)) + .setContentText( + mXmppConnectionService + .getResources() + .getQuantityText( + R.plurals.some_messages_could_not_be_delivered, + 1024)) .setSmallIcon(R.drawable.ic_error_white_24dp) .setGroup("delivery_failed") .setGroupSummary(true) @@ -398,32 +462,38 @@ public void pushFailedDelivery(final Message message) { notify(DELIVERY_FAILED_NOTIFICATION_ID, summaryNotification); } - public synchronized void startRinging(final AbstractJingleConnection.Id id, final Set media) { + public synchronized void startRinging( + final AbstractJingleConnection.Id id, final Set media) { showIncomingCallNotification(id, media); - final NotificationManager notificationManager = (NotificationManager) mXmppConnectionService.getSystemService(Context.NOTIFICATION_SERVICE); + final NotificationManager notificationManager = + (NotificationManager) + mXmppConnectionService.getSystemService(Context.NOTIFICATION_SERVICE); final int currentInterruptionFilter; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && notificationManager != null) { currentInterruptionFilter = notificationManager.getCurrentInterruptionFilter(); } else { - currentInterruptionFilter = 1; //INTERRUPTION_FILTER_ALL + currentInterruptionFilter = 1; // INTERRUPTION_FILTER_ALL } if (currentInterruptionFilter != 1) { - Log.d(Config.LOGTAG, "do not ring or vibrate because interruption filter has been set to " + currentInterruptionFilter); + Log.d( + Config.LOGTAG, + "do not ring or vibrate because interruption filter has been set to " + + currentInterruptionFilter); return; } final ScheduledFuture currentVibrationFuture = this.vibrationFuture; - this.vibrationFuture = SCHEDULED_EXECUTOR_SERVICE.scheduleAtFixedRate( - new VibrationRunnable(), - 0, - 3, - TimeUnit.SECONDS - ); + this.vibrationFuture = + SCHEDULED_EXECUTOR_SERVICE.scheduleAtFixedRate( + new VibrationRunnable(), 0, 3, TimeUnit.SECONDS); if (currentVibrationFuture != null) { currentVibrationFuture.cancel(true); } - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService); + final SharedPreferences preferences = + PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService); final Resources resources = mXmppConnectionService.getResources(); - final String ringtonePreference = preferences.getString("call_ringtone", resources.getString(R.string.incoming_call_ringtone)); + final String ringtonePreference = + preferences.getString( + "call_ringtone", resources.getString(R.string.incoming_call_ringtone)); if (Strings.isNullOrEmpty(ringtonePreference)) { Log.d(Config.LOGTAG, "ringtone has been set to none"); return; @@ -440,26 +510,34 @@ public synchronized void startRinging(final AbstractJingleConnection.Id id, fina this.currentlyPlayingRingtone.play(); } - private void showIncomingCallNotification(final AbstractJingleConnection.Id id, final Set media) { - final Intent fullScreenIntent = new Intent(mXmppConnectionService, RtpSessionActivity.class); - fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, id.account.getJid().asBareJid().toEscapedString()); + private void showIncomingCallNotification( + final AbstractJingleConnection.Id id, final Set media) { + final Intent fullScreenIntent = + new Intent(mXmppConnectionService, RtpSessionActivity.class); + fullScreenIntent.putExtra( + RtpSessionActivity.EXTRA_ACCOUNT, + id.account.getJid().asBareJid().toEscapedString()); fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString()); fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId); fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - final NotificationCompat.Builder builder = new NotificationCompat.Builder(mXmppConnectionService, INCOMING_CALLS_NOTIFICATION_CHANNEL); + final NotificationCompat.Builder builder = + new NotificationCompat.Builder( + mXmppConnectionService, INCOMING_CALLS_NOTIFICATION_CHANNEL); if (media.contains(Media.VIDEO)) { builder.setSmallIcon(R.drawable.ic_videocam_white_24dp); - builder.setContentTitle(mXmppConnectionService.getString(R.string.rtp_state_incoming_video_call)); + builder.setContentTitle( + mXmppConnectionService.getString(R.string.rtp_state_incoming_video_call)); } else { builder.setSmallIcon(R.drawable.ic_call_white_24dp); - builder.setContentTitle(mXmppConnectionService.getString(R.string.rtp_state_incoming_call)); + builder.setContentTitle( + mXmppConnectionService.getString(R.string.rtp_state_incoming_call)); } final Contact contact = id.getContact(); - builder.setLargeIcon(mXmppConnectionService.getAvatarService().get( - contact, - AvatarService.getSystemUiAvatarSize(mXmppConnectionService)) - ); + builder.setLargeIcon( + mXmppConnectionService + .getAvatarService() + .get(contact, AvatarService.getSystemUiAvatarSize(mXmppConnectionService))); final Uri systemAccount = contact.getSystemAccount(); if (systemAccount != null) { builder.addPerson(systemAccount.toString()); @@ -470,38 +548,49 @@ private void showIncomingCallNotification(final AbstractJingleConnection.Id id, builder.setCategory(NotificationCompat.CATEGORY_CALL); PendingIntent pendingIntent = createPendingRtpSession(id, Intent.ACTION_VIEW, 101); builder.setFullScreenIntent(pendingIntent, true); - builder.setContentIntent(pendingIntent); //old androids need this? + builder.setContentIntent(pendingIntent); // old androids need this? builder.setOngoing(true); - builder.addAction(new NotificationCompat.Action.Builder( - R.drawable.ic_call_end_white_48dp, - mXmppConnectionService.getString(R.string.dismiss_call), - createCallAction(id.sessionId, XmppConnectionService.ACTION_DISMISS_CALL, 102)) - .build()); - builder.addAction(new NotificationCompat.Action.Builder( - R.drawable.ic_call_white_24dp, - mXmppConnectionService.getString(R.string.answer_call), - createPendingRtpSession(id, RtpSessionActivity.ACTION_ACCEPT_CALL, 103)) - .build()); + builder.addAction( + new NotificationCompat.Action.Builder( + R.drawable.ic_call_end_white_48dp, + mXmppConnectionService.getString(R.string.dismiss_call), + createCallAction( + id.sessionId, + XmppConnectionService.ACTION_DISMISS_CALL, + 102)) + .build()); + builder.addAction( + new NotificationCompat.Action.Builder( + R.drawable.ic_call_white_24dp, + mXmppConnectionService.getString(R.string.answer_call), + createPendingRtpSession( + id, RtpSessionActivity.ACTION_ACCEPT_CALL, 103)) + .build()); modifyIncomingCall(builder); final Notification notification = builder.build(); notification.flags = notification.flags | Notification.FLAG_INSISTENT; notify(INCOMING_CALL_NOTIFICATION_ID, notification); } - public Notification getOngoingCallNotification(final XmppConnectionService.OngoingCall ongoingCall) { + public Notification getOngoingCallNotification( + final XmppConnectionService.OngoingCall ongoingCall) { final AbstractJingleConnection.Id id = ongoingCall.id; - final NotificationCompat.Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "ongoing_calls"); + final NotificationCompat.Builder builder = + new NotificationCompat.Builder(mXmppConnectionService, "ongoing_calls"); if (ongoingCall.media.contains(Media.VIDEO)) { builder.setSmallIcon(R.drawable.ic_videocam_white_24dp); if (ongoingCall.reconnecting) { - builder.setContentTitle(mXmppConnectionService.getString(R.string.reconnecting_video_call)); + builder.setContentTitle( + mXmppConnectionService.getString(R.string.reconnecting_video_call)); } else { - builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_video_call)); + builder.setContentTitle( + mXmppConnectionService.getString(R.string.ongoing_video_call)); } } else { builder.setSmallIcon(R.drawable.ic_call_white_24dp); if (ongoingCall.reconnecting) { - builder.setContentTitle(mXmppConnectionService.getString(R.string.reconnecting_call)); + builder.setContentTitle( + mXmppConnectionService.getString(R.string.reconnecting_call)); } else { builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_call)); } @@ -512,21 +601,31 @@ public Notification getOngoingCallNotification(final XmppConnectionService.Ongoi builder.setCategory(NotificationCompat.CATEGORY_CALL); builder.setContentIntent(createPendingRtpSession(id, Intent.ACTION_VIEW, 101)); builder.setOngoing(true); - builder.addAction(new NotificationCompat.Action.Builder( - R.drawable.ic_call_end_white_48dp, - mXmppConnectionService.getString(R.string.hang_up), - createCallAction(id.sessionId, XmppConnectionService.ACTION_END_CALL, 104)) - .build()); + builder.addAction( + new NotificationCompat.Action.Builder( + R.drawable.ic_call_end_white_48dp, + mXmppConnectionService.getString(R.string.hang_up), + createCallAction( + id.sessionId, XmppConnectionService.ACTION_END_CALL, 104)) + .build()); return builder.build(); } - private PendingIntent createPendingRtpSession(final AbstractJingleConnection.Id id, final String action, final int requestCode) { - final Intent fullScreenIntent = new Intent(mXmppConnectionService, RtpSessionActivity.class); + private PendingIntent createPendingRtpSession( + final AbstractJingleConnection.Id id, final String action, final int requestCode) { + final Intent fullScreenIntent = + new Intent(mXmppConnectionService, RtpSessionActivity.class); fullScreenIntent.setAction(action); - fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, id.account.getJid().asBareJid().toEscapedString()); + fullScreenIntent.putExtra( + RtpSessionActivity.EXTRA_ACCOUNT, + id.account.getJid().asBareJid().toEscapedString()); fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString()); fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId); - return PendingIntent.getActivity(mXmppConnectionService, requestCode, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT); + return PendingIntent.getActivity( + mXmppConnectionService, + requestCode, + fullScreenIntent, + PendingIntent.FLAG_UPDATE_CURRENT); } public void cancelIncomingCallNotification() { @@ -552,7 +651,8 @@ public boolean stopSoundAndVibration() { } public static void cancelIncomingCallNotification(final Context context) { - final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); + final NotificationManagerCompat notificationManager = + NotificationManagerCompat.from(context); try { notificationManager.cancel(INCOMING_CALL_NOTIFICATION_ID); } catch (RuntimeException e) { @@ -563,21 +663,30 @@ public static void cancelIncomingCallNotification(final Context context) { private void pushNow(final Message message) { mXmppConnectionService.updateUnreadCountBadge(); if (!notify(message)) { - Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": suppressing notification because turned off"); + Log.d( + Config.LOGTAG, + message.getConversation().getAccount().getJid().asBareJid() + + ": suppressing notification because turned off"); return; } final boolean isScreenLocked = mXmppConnectionService.isScreenLocked(); - if (this.mIsInForeground && !isScreenLocked && this.mOpenConversation == message.getConversation()) { - Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": suppressing notification because conversation is open"); + if (this.mIsInForeground + && !isScreenLocked + && this.mOpenConversation == message.getConversation()) { + Log.d( + Config.LOGTAG, + message.getConversation().getAccount().getJid().asBareJid() + + ": suppressing notification because conversation is open"); return; } synchronized (notifications) { pushToStack(message); final Conversational conversation = message.getConversation(); final Account account = conversation.getAccount(); - final boolean doNotify = (!(this.mIsInForeground && this.mOpenConversation == null) || isScreenLocked) - && !account.inGracePeriod() - && !this.inMiniGracePeriod(account); + final boolean doNotify = + (!(this.mIsInForeground && this.mOpenConversation == null) || isScreenLocked) + && !account.inGracePeriod() + && !this.inMiniGracePeriod(account); updateNotification(doNotify, Collections.singletonList(conversation.getUuid())); } } @@ -638,13 +747,19 @@ private void updateNotification(final boolean notify, final List convers updateNotification(notify, conversations, false); } - private void updateNotification(final boolean notify, final List conversations, final boolean summaryOnly) { - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService); + private void updateNotification( + final boolean notify, final List conversations, final boolean summaryOnly) { + final SharedPreferences preferences = + PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService); final boolean quiteHours = isQuietHours(); - final boolean notifyOnlyOneChild = notify && conversations != null && conversations.size() == 1; //if this check is changed to > 0 catchup messages will create one notification per conversation - + final boolean notifyOnlyOneChild = + notify + && conversations != null + && conversations.size() + == 1; // if this check is changed to > 0 catchup messages will + // create one notification per conversation if (notifications.size() == 0) { cancel(NOTIFICATION_ID); @@ -654,7 +769,9 @@ private void updateNotification(final boolean notify, final List convers } final Builder mBuilder; if (notifications.size() == 1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - mBuilder = buildSingleConversations(notifications.values().iterator().next(), notify, quiteHours); + mBuilder = + buildSingleConversations( + notifications.values().iterator().next(), notify, quiteHours); modifyForSoundVibrationAndLight(mBuilder, notify, quiteHours, preferences); notify(NOTIFICATION_ID, mBuilder.build()); } else { @@ -666,12 +783,16 @@ private void updateNotification(final boolean notify, final List convers if (!summaryOnly) { for (Map.Entry> entry : notifications.entrySet()) { String uuid = entry.getKey(); - final boolean notifyThis = notifyOnlyOneChild ? conversations.contains(uuid) : notify; - Builder singleBuilder = buildSingleConversations(entry.getValue(), notifyThis, quiteHours); + final boolean notifyThis = + notifyOnlyOneChild ? conversations.contains(uuid) : notify; + Builder singleBuilder = + buildSingleConversations(entry.getValue(), notifyThis, quiteHours); if (!notifyOnlyOneChild) { - singleBuilder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY); + singleBuilder.setGroupAlertBehavior( + NotificationCompat.GROUP_ALERT_SUMMARY); } - modifyForSoundVibrationAndLight(singleBuilder, notifyThis, quiteHours, preferences); + modifyForSoundVibrationAndLight( + singleBuilder, notifyThis, quiteHours, preferences); singleBuilder.setGroup(CONVERSATIONS_GROUP); setNotificationColor(singleBuilder); notify(entry.getKey(), NOTIFICATION_ID, singleBuilder.build()); @@ -682,19 +803,28 @@ private void updateNotification(final boolean notify, final List convers } } - private void modifyForSoundVibrationAndLight(Builder mBuilder, boolean notify, boolean quietHours, SharedPreferences preferences) { + private void modifyForSoundVibrationAndLight( + Builder mBuilder, boolean notify, boolean quietHours, SharedPreferences preferences) { final Resources resources = mXmppConnectionService.getResources(); - final String ringtone = preferences.getString("notification_ringtone", resources.getString(R.string.notification_ringtone)); - final boolean vibrate = preferences.getBoolean("vibrate_on_notification", resources.getBoolean(R.bool.vibrate_on_notification)); + final String ringtone = + preferences.getString( + "notification_ringtone", + resources.getString(R.string.notification_ringtone)); + final boolean vibrate = + preferences.getBoolean( + "vibrate_on_notification", + resources.getBoolean(R.bool.vibrate_on_notification)); final boolean led = preferences.getBoolean("led", resources.getBoolean(R.bool.led)); - final boolean headsup = preferences.getBoolean("notification_headsup", resources.getBoolean(R.bool.headsup_notifications)); + final boolean headsup = + preferences.getBoolean( + "notification_headsup", resources.getBoolean(R.bool.headsup_notifications)); if (notify && !quietHours) { if (vibrate) { final int dat = 70; final long[] pattern = {0, 3 * dat, dat, dat}; mBuilder.setVibrate(pattern); } else { - mBuilder.setVibrate(new long[]{0}); + mBuilder.setVibrate(new long[] {0}); } Uri uri = Uri.parse(ringtone); try { @@ -708,7 +838,12 @@ private void modifyForSoundVibrationAndLight(Builder mBuilder, boolean notify, b if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { mBuilder.setCategory(Notification.CATEGORY_MESSAGE); } - mBuilder.setPriority(notify ? (headsup ? NotificationCompat.PRIORITY_HIGH : NotificationCompat.PRIORITY_DEFAULT) : NotificationCompat.PRIORITY_LOW); + mBuilder.setPriority( + notify + ? (headsup + ? NotificationCompat.PRIORITY_HIGH + : NotificationCompat.PRIORITY_DEFAULT) + : NotificationCompat.PRIORITY_LOW); setNotificationColor(mBuilder); mBuilder.setDefaults(0); if (led) { @@ -731,9 +866,18 @@ private Uri fixRingtoneUri(Uri uri) { } private Builder buildMultipleConversation(final boolean notify, final boolean quietHours) { - final Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService, quietHours ? "quiet_hours" : (notify ? "messages" : "silent_messages")); + final Builder mBuilder = + new NotificationCompat.Builder( + mXmppConnectionService, + quietHours ? "quiet_hours" : (notify ? "messages" : "silent_messages")); final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle(); - style.setBigContentTitle(mXmppConnectionService.getResources().getQuantityString(R.plurals.x_unread_conversations, notifications.size(), notifications.size())); + style.setBigContentTitle( + mXmppConnectionService + .getResources() + .getQuantityString( + R.plurals.x_unread_conversations, + notifications.size(), + notifications.size())); final StringBuilder names = new StringBuilder(); Conversation conversation = null; for (final ArrayList messages : notifications.values()) { @@ -743,11 +887,24 @@ private Builder buildMultipleConversation(final boolean notify, final boolean qu SpannableString styledString; if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) { int count = messages.size(); - styledString = new SpannableString(name + ": " + mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages, count, count)); + styledString = + new SpannableString( + name + + ": " + + mXmppConnectionService + .getResources() + .getQuantityString( + R.plurals.x_messages, count, count)); styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0); style.addLine(styledString); } else { - styledString = new SpannableString(name + ": " + UIHelper.getMessagePreview(mXmppConnectionService, messages.get(0)).first); + styledString = + new SpannableString( + name + + ": " + + UIHelper.getMessagePreview( + mXmppConnectionService, messages.get(0)) + .first); styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0); style.addLine(styledString); } @@ -758,7 +915,13 @@ private Builder buildMultipleConversation(final boolean notify, final boolean qu if (names.length() >= 2) { names.delete(names.length() - 2, names.length()); } - final String contentTitle = mXmppConnectionService.getResources().getQuantityString(R.plurals.x_unread_conversations, notifications.size(), notifications.size()); + final String contentTitle = + mXmppConnectionService + .getResources() + .getQuantityString( + R.plurals.x_unread_conversations, + notifications.size(), + notifications.size()); mBuilder.setContentTitle(contentTitle); mBuilder.setTicker(contentTitle); mBuilder.setContentText(names.toString()); @@ -773,46 +936,72 @@ private Builder buildMultipleConversation(final boolean notify, final boolean qu return mBuilder; } - private Builder buildSingleConversations(final ArrayList messages, final boolean notify, final boolean quietHours) { - final Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService, quietHours ? "quiet_hours" : (notify ? "messages" : "silent_messages")); + private Builder buildSingleConversations( + final ArrayList messages, final boolean notify, final boolean quietHours) { + final Builder mBuilder = + new NotificationCompat.Builder( + mXmppConnectionService, + quietHours ? "quiet_hours" : (notify ? "messages" : "silent_messages")); if (messages.size() >= 1) { final Conversation conversation = (Conversation) messages.get(0).getConversation(); - mBuilder.setLargeIcon(mXmppConnectionService.getAvatarService() - .get(conversation, AvatarService.getSystemUiAvatarSize(mXmppConnectionService))); + mBuilder.setLargeIcon( + mXmppConnectionService + .getAvatarService() + .get( + conversation, + AvatarService.getSystemUiAvatarSize(mXmppConnectionService))); mBuilder.setContentTitle(conversation.getName()); if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) { int count = messages.size(); - mBuilder.setContentText(mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages, count, count)); + mBuilder.setContentText( + mXmppConnectionService + .getResources() + .getQuantityString(R.plurals.x_messages, count, count)); } else { Message message; - //TODO starting with Android 9 we might want to put images in MessageStyle - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P && (message = getImage(messages)) != null) { + // TODO starting with Android 9 we might want to put images in MessageStyle + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P + && (message = getImage(messages)) != null) { modifyForImage(mBuilder, message, messages); } else { modifyForTextOnly(mBuilder, messages); } - RemoteInput remoteInput = new RemoteInput.Builder("text_reply").setLabel(UIHelper.getMessageHint(mXmppConnectionService, conversation)).build(); + RemoteInput remoteInput = + new RemoteInput.Builder("text_reply") + .setLabel( + UIHelper.getMessageHint( + mXmppConnectionService, conversation)) + .build(); PendingIntent markAsReadPendingIntent = createReadPendingIntent(conversation); - NotificationCompat.Action markReadAction = new NotificationCompat.Action.Builder( - R.drawable.ic_drafts_white_24dp, - mXmppConnectionService.getString(R.string.mark_as_read), - markAsReadPendingIntent) - .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ) - .setShowsUserInterface(false) - .build(); + NotificationCompat.Action markReadAction = + new NotificationCompat.Action.Builder( + R.drawable.ic_drafts_white_24dp, + mXmppConnectionService.getString(R.string.mark_as_read), + markAsReadPendingIntent) + .setSemanticAction( + NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ) + .setShowsUserInterface(false) + .build(); final String replyLabel = mXmppConnectionService.getString(R.string.reply); final String lastMessageUuid = Iterables.getLast(messages).getUuid(); - final NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder( - R.drawable.ic_send_text_offline, - replyLabel, - createReplyIntent(conversation, lastMessageUuid, false)) - .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY) - .setShowsUserInterface(false) - .addRemoteInput(remoteInput).build(); - final NotificationCompat.Action wearReplyAction = new NotificationCompat.Action.Builder(R.drawable.ic_wear_reply, - replyLabel, - createReplyIntent(conversation, lastMessageUuid, true)).addRemoteInput(remoteInput).build(); - mBuilder.extend(new NotificationCompat.WearableExtender().addAction(wearReplyAction)); + final NotificationCompat.Action replyAction = + new NotificationCompat.Action.Builder( + R.drawable.ic_send_text_offline, + replyLabel, + createReplyIntent(conversation, lastMessageUuid, false)) + .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY) + .setShowsUserInterface(false) + .addRemoteInput(remoteInput) + .build(); + final NotificationCompat.Action wearReplyAction = + new NotificationCompat.Action.Builder( + R.drawable.ic_wear_reply, + replyLabel, + createReplyIntent(conversation, lastMessageUuid, true)) + .addRemoteInput(remoteInput) + .build(); + mBuilder.extend( + new NotificationCompat.WearableExtender().addAction(wearReplyAction)); int addedActionsCount = 1; mBuilder.addAction(markReadAction); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { @@ -823,23 +1012,31 @@ private Builder buildSingleConversations(final ArrayList messages, fina if (displaySnoozeAction(messages)) { String label = mXmppConnectionService.getString(R.string.snooze); PendingIntent pendingSnoozeIntent = createSnoozeIntent(conversation); - NotificationCompat.Action snoozeAction = new NotificationCompat.Action.Builder( - R.drawable.ic_notifications_paused_white_24dp, - label, - pendingSnoozeIntent).build(); + NotificationCompat.Action snoozeAction = + new NotificationCompat.Action.Builder( + R.drawable.ic_notifications_paused_white_24dp, + label, + pendingSnoozeIntent) + .build(); mBuilder.addAction(snoozeAction); ++addedActionsCount; } if (addedActionsCount < 3) { final Message firstLocationMessage = getFirstLocationMessage(messages); if (firstLocationMessage != null) { - final PendingIntent pendingShowLocationIntent = createShowLocationIntent(firstLocationMessage); + final PendingIntent pendingShowLocationIntent = + createShowLocationIntent(firstLocationMessage); if (pendingShowLocationIntent != null) { - final String label = mXmppConnectionService.getResources().getString(R.string.show_location); - NotificationCompat.Action locationAction = new NotificationCompat.Action.Builder( - R.drawable.ic_room_white_24dp, - label, - pendingShowLocationIntent).build(); + final String label = + mXmppConnectionService + .getResources() + .getString(R.string.show_location); + NotificationCompat.Action locationAction = + new NotificationCompat.Action.Builder( + R.drawable.ic_room_white_24dp, + label, + pendingShowLocationIntent) + .build(); mBuilder.addAction(locationAction); ++addedActionsCount; } @@ -848,12 +1045,22 @@ private Builder buildSingleConversations(final ArrayList messages, fina if (addedActionsCount < 3) { Message firstDownloadableMessage = getFirstDownloadableMessage(messages); if (firstDownloadableMessage != null) { - String label = mXmppConnectionService.getResources().getString(R.string.download_x_file, UIHelper.getFileDescriptionString(mXmppConnectionService, firstDownloadableMessage)); - PendingIntent pendingDownloadIntent = createDownloadIntent(firstDownloadableMessage); - NotificationCompat.Action downloadAction = new NotificationCompat.Action.Builder( - R.drawable.ic_file_download_white_24dp, - label, - pendingDownloadIntent).build(); + String label = + mXmppConnectionService + .getResources() + .getString( + R.string.download_x_file, + UIHelper.getFileDescriptionString( + mXmppConnectionService, + firstDownloadableMessage)); + PendingIntent pendingDownloadIntent = + createDownloadIntent(firstDownloadableMessage); + NotificationCompat.Action downloadAction = + new NotificationCompat.Action.Builder( + R.drawable.ic_file_download_white_24dp, + label, + pendingDownloadIntent) + .build(); mBuilder.addAction(downloadAction); ++addedActionsCount; } @@ -874,13 +1081,16 @@ private Builder buildSingleConversations(final ArrayList messages, fina return mBuilder; } - private void modifyForImage(final Builder builder, final Message message, final ArrayList messages) { + private void modifyForImage( + final Builder builder, final Message message, final ArrayList messages) { try { - final Bitmap bitmap = mXmppConnectionService.getFileBackend().getThumbnail(message, getPixel(288), false); + final Bitmap bitmap = + mXmppConnectionService + .getFileBackend() + .getThumbnail(message, getPixel(288), false); final ArrayList tmp = new ArrayList<>(); for (final Message msg : messages) { - if (msg.getType() == Message.TYPE_TEXT - && msg.getTransferable() == null) { + if (msg.getType() == Message.TYPE_TEXT && msg.getTransferable() == null) { tmp.add(msg); } } @@ -892,7 +1102,8 @@ private void modifyForImage(final Builder builder, final Message message, final builder.setContentText(text); builder.setTicker(text); } else { - final String description = UIHelper.getFileDescriptionString(mXmppConnectionService, message); + final String description = + UIHelper.getFileDescriptionString(mXmppConnectionService, message); builder.setContentText(description); builder.setTicker(description); } @@ -915,7 +1126,15 @@ private Person getPerson(Message message) { builder.setName(UIHelper.getMessageDisplayName(message)); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - builder.setIcon(IconCompat.createWithBitmap(mXmppConnectionService.getAvatarService().get(message, AvatarService.getSystemUiAvatarSize(mXmppConnectionService), false))); + builder.setIcon( + IconCompat.createWithBitmap( + mXmppConnectionService + .getAvatarService() + .get( + message, + AvatarService.getSystemUiAvatarSize( + mXmppConnectionService), + false))); } return builder.build(); } @@ -923,35 +1142,60 @@ private Person getPerson(Message message) { private void modifyForTextOnly(final Builder builder, final ArrayList messages) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { final Conversation conversation = (Conversation) messages.get(0).getConversation(); - final Person.Builder meBuilder = new Person.Builder().setName(mXmppConnectionService.getString(R.string.me)); + final Person.Builder meBuilder = + new Person.Builder().setName(mXmppConnectionService.getString(R.string.me)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - meBuilder.setIcon(IconCompat.createWithBitmap(mXmppConnectionService.getAvatarService().get(conversation.getAccount(), AvatarService.getSystemUiAvatarSize(mXmppConnectionService)))); + meBuilder.setIcon( + IconCompat.createWithBitmap( + mXmppConnectionService + .getAvatarService() + .get( + conversation.getAccount(), + AvatarService.getSystemUiAvatarSize( + mXmppConnectionService)))); } final Person me = meBuilder.build(); - NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle(me); + NotificationCompat.MessagingStyle messagingStyle = + new NotificationCompat.MessagingStyle(me); final boolean multiple = conversation.getMode() == Conversation.MODE_MULTI; if (multiple) { messagingStyle.setConversationTitle(conversation.getName()); } for (Message message : messages) { - final Person sender = message.getStatus() == Message.STATUS_RECEIVED ? getPerson(message) : null; + final Person sender = + message.getStatus() == Message.STATUS_RECEIVED ? getPerson(message) : null; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isImageMessage(message)) { - final Uri dataUri = FileBackend.getMediaUri(mXmppConnectionService, mXmppConnectionService.getFileBackend().getFile(message)); - NotificationCompat.MessagingStyle.Message imageMessage = new NotificationCompat.MessagingStyle.Message(UIHelper.getMessagePreview(mXmppConnectionService, message).first, message.getTimeSent(), sender); + final Uri dataUri = + FileBackend.getMediaUri( + mXmppConnectionService, + mXmppConnectionService.getFileBackend().getFile(message)); + NotificationCompat.MessagingStyle.Message imageMessage = + new NotificationCompat.MessagingStyle.Message( + UIHelper.getMessagePreview(mXmppConnectionService, message) + .first, + message.getTimeSent(), + sender); if (dataUri != null) { imageMessage.setData(message.getMimeType(), dataUri); } messagingStyle.addMessage(imageMessage); } else { - messagingStyle.addMessage(UIHelper.getMessagePreview(mXmppConnectionService, message).first, message.getTimeSent(), sender); + messagingStyle.addMessage( + UIHelper.getMessagePreview(mXmppConnectionService, message).first, + message.getTimeSent(), + sender); } } messagingStyle.setGroupConversation(multiple); builder.setStyle(messagingStyle); } else { if (messages.get(0).getConversation().getMode() == Conversation.MODE_SINGLE) { - builder.setStyle(new NotificationCompat.BigTextStyle().bigText(getMergedBodies(messages))); - final CharSequence preview = UIHelper.getMessagePreview(mXmppConnectionService, messages.get(messages.size() - 1)).first; + builder.setStyle( + new NotificationCompat.BigTextStyle().bigText(getMergedBodies(messages))); + final CharSequence preview = + UIHelper.getMessagePreview( + mXmppConnectionService, messages.get(messages.size() - 1)) + .first; builder.setContentText(preview); builder.setTicker(preview); builder.setNumber(messages.size()); @@ -973,7 +1217,10 @@ private void modifyForTextOnly(final Builder builder, final ArrayList m builder.setContentText(styledString); builder.setTicker(styledString); } else { - final String text = mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages, count, count); + final String text = + mXmppConnectionService + .getResources() + .getQuantityString(R.plurals.x_messages, count, count); builder.setContentText(text); builder.setTicker(text); } @@ -996,7 +1243,8 @@ private Message getImage(final Iterable messages) { private Message getFirstDownloadableMessage(final Iterable messages) { for (final Message message : messages) { - if (message.getTransferable() != null || (message.getType() == Message.TYPE_TEXT && message.treatAsDownloadable())) { + if (message.getTransferable() != null + || (message.getType() == Message.TYPE_TEXT && message.treatAsDownloadable())) { return message; } } @@ -1024,27 +1272,37 @@ private CharSequence getMergedBodies(final ArrayList messages) { } private PendingIntent createShowLocationIntent(final Message message) { - Iterable intents = GeoHelper.createGeoIntentsFromMessage(mXmppConnectionService, message); - for (Intent intent : intents) { + Iterable intents = + GeoHelper.createGeoIntentsFromMessage(mXmppConnectionService, message); + for (final Intent intent : intents) { if (intent.resolveActivity(mXmppConnectionService.getPackageManager()) != null) { - return PendingIntent.getActivity(mXmppConnectionService, generateRequestCode(message.getConversation(), 18), intent, PendingIntent.FLAG_UPDATE_CURRENT); + return PendingIntent.getActivity( + mXmppConnectionService, + generateRequestCode(message.getConversation(), 18), + intent, + PendingIntent.FLAG_UPDATE_CURRENT); } } return null; } - private PendingIntent createContentIntent(final String conversationUuid, final String downloadMessageUuid) { - final Intent viewConversationIntent = new Intent(mXmppConnectionService, ConversationsActivity.class); + private PendingIntent createContentIntent( + final String conversationUuid, final String downloadMessageUuid) { + final Intent viewConversationIntent = + new Intent(mXmppConnectionService, ConversationsActivity.class); viewConversationIntent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION); viewConversationIntent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversationUuid); if (downloadMessageUuid != null) { - viewConversationIntent.putExtra(ConversationsActivity.EXTRA_DOWNLOAD_UUID, downloadMessageUuid); - return PendingIntent.getActivity(mXmppConnectionService, + viewConversationIntent.putExtra( + ConversationsActivity.EXTRA_DOWNLOAD_UUID, downloadMessageUuid); + return PendingIntent.getActivity( + mXmppConnectionService, generateRequestCode(conversationUuid, 8), viewConversationIntent, PendingIntent.FLAG_UPDATE_CURRENT); } else { - return PendingIntent.getActivity(mXmppConnectionService, + return PendingIntent.getActivity( + mXmppConnectionService, generateRequestCode(conversationUuid, 10), viewConversationIntent, PendingIntent.FLAG_UPDATE_CURRENT); @@ -1052,7 +1310,8 @@ private PendingIntent createContentIntent(final String conversationUuid, final S } private int generateRequestCode(String uuid, int actionId) { - return (actionId * NOTIFICATION_ID_MULTIPLIER) + (uuid.hashCode() % NOTIFICATION_ID_MULTIPLIER); + return (actionId * NOTIFICATION_ID_MULTIPLIER) + + (uuid.hashCode() % NOTIFICATION_ID_MULTIPLIER); } private int generateRequestCode(Conversational conversation, int actionId) { @@ -1072,19 +1331,24 @@ private PendingIntent createDeleteIntent(Conversation conversation) { intent.setAction(XmppConnectionService.ACTION_CLEAR_NOTIFICATION); if (conversation != null) { intent.putExtra("uuid", conversation.getUuid()); - return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 20), intent, 0); + return PendingIntent.getService( + mXmppConnectionService, generateRequestCode(conversation, 20), intent, 0); } return PendingIntent.getService(mXmppConnectionService, 0, intent, 0); } - private PendingIntent createReplyIntent(final Conversation conversation, final String lastMessageUuid, final boolean dismissAfterReply) { + private PendingIntent createReplyIntent( + final Conversation conversation, + final String lastMessageUuid, + final boolean dismissAfterReply) { final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); intent.setAction(XmppConnectionService.ACTION_REPLY_TO_CONVERSATION); intent.putExtra("uuid", conversation.getUuid()); intent.putExtra("dismiss_notification", dismissAfterReply); intent.putExtra("last_message_uuid", lastMessageUuid); final int id = generateRequestCode(conversation, dismissAfterReply ? 12 : 14); - return PendingIntent.getService(mXmppConnectionService, id, intent, PendingIntent.FLAG_UPDATE_CURRENT); + return PendingIntent.getService( + mXmppConnectionService, id, intent, PendingIntent.FLAG_UPDATE_CURRENT); } private PendingIntent createReadPendingIntent(Conversation conversation) { @@ -1092,7 +1356,11 @@ private PendingIntent createReadPendingIntent(Conversation conversation) { intent.setAction(XmppConnectionService.ACTION_MARK_AS_READ); intent.putExtra("uuid", conversation.getUuid()); intent.setPackage(mXmppConnectionService.getPackageName()); - return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 16), intent, PendingIntent.FLAG_UPDATE_CURRENT); + return PendingIntent.getService( + mXmppConnectionService, + generateRequestCode(conversation, 16), + intent, + PendingIntent.FLAG_UPDATE_CURRENT); } private PendingIntent createCallAction(String sessionId, final String action, int requestCode) { @@ -1100,7 +1368,8 @@ private PendingIntent createCallAction(String sessionId, final String action, in intent.setAction(action); intent.setPackage(mXmppConnectionService.getPackageName()); intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, sessionId); - return PendingIntent.getService(mXmppConnectionService, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT); + return PendingIntent.getService( + mXmppConnectionService, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT); } private PendingIntent createSnoozeIntent(Conversation conversation) { @@ -1108,7 +1377,11 @@ private PendingIntent createSnoozeIntent(Conversation conversation) { intent.setAction(XmppConnectionService.ACTION_SNOOZE); intent.putExtra("uuid", conversation.getUuid()); intent.setPackage(mXmppConnectionService.getPackageName()); - return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 22), intent, PendingIntent.FLAG_UPDATE_CURRENT); + return PendingIntent.getService( + mXmppConnectionService, + generateRequestCode(conversation, 22), + intent, + PendingIntent.FLAG_UPDATE_CURRENT); } private PendingIntent createTryAgainIntent() { @@ -1147,8 +1420,7 @@ public void setIsInForeground(final boolean foreground) { } private int getPixel(final int dp) { - final DisplayMetrics metrics = mXmppConnectionService.getResources() - .getDisplayMetrics(); + final DisplayMetrics metrics = mXmppConnectionService.getResources().getDisplayMetrics(); return ((int) (dp * metrics.density)); } @@ -1157,8 +1429,10 @@ private void markLastNotification() { } private boolean inMiniGracePeriod(final Account account) { - final int miniGrace = account.getStatus() == Account.State.ONLINE ? Config.MINI_GRACE_PERIOD - : Config.MINI_GRACE_PERIOD * 2; + final int miniGrace = + account.getStatus() == Account.State.ONLINE + ? Config.MINI_GRACE_PERIOD + : Config.MINI_GRACE_PERIOD * 2; return SystemClock.elapsedRealtime() < (this.mLastNotification + miniGrace); } @@ -1178,26 +1452,34 @@ Notification createForegroundNotification() { } } } - mBuilder.setContentText(mXmppConnectionService.getString(R.string.connected_accounts, connected, enabled)); + mBuilder.setContentText( + mXmppConnectionService.getString(R.string.connected_accounts, connected, enabled)); final PendingIntent openIntent = createOpenConversationsIntent(); if (openIntent != null) { mBuilder.setContentIntent(openIntent); } - mBuilder.setWhen(0); - mBuilder.setPriority(Notification.PRIORITY_MIN); - mBuilder.setSmallIcon(connected > 0 ? R.drawable.ic_link_white_24dp : R.drawable.ic_link_off_white_24dp); + mBuilder.setWhen(0) + .setPriority(Notification.PRIORITY_MIN) + .setSmallIcon( + connected > 0 + ? R.drawable.ic_link_white_24dp + : R.drawable.ic_link_off_white_24dp) + .setLocalOnly(true); if (Compatibility.runsTwentySix()) { mBuilder.setChannelId("foreground"); } - return mBuilder.build(); } private PendingIntent createOpenConversationsIntent() { try { - return PendingIntent.getActivity(mXmppConnectionService, 0, new Intent(mXmppConnectionService, ConversationsActivity.class), 0); + return PendingIntent.getActivity( + mXmppConnectionService, + 0, + new Intent(mXmppConnectionService, ConversationsActivity.class), + 0); } catch (RuntimeException e) { return null; } @@ -1212,7 +1494,10 @@ void updateErrorNotification() { final List errors = new ArrayList<>(); boolean torNotAvailable = false; for (final Account account : mXmppConnectionService.getAccounts()) { - if (account.hasErrorStatus() && account.showErrorNotification() && (showAllErrors || account.getLastErrorStatus() == Account.State.UNAUTHORIZED)) { + if (account.hasErrorStatus() + && account.showErrorNotification() + && (showAllErrors + || account.getLastErrorStatus() == Account.State.UNAUTHORIZED)) { errors.add(account); torNotAvailable |= account.getStatus() == Account.State.TOR_NOT_AVAILABLE; } @@ -1225,29 +1510,31 @@ void updateErrorNotification() { cancel(ERROR_NOTIFICATION_ID); return; } else if (errors.size() == 1) { - mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.problem_connecting_to_account)); + mBuilder.setContentTitle( + mXmppConnectionService.getString(R.string.problem_connecting_to_account)); mBuilder.setContentText(errors.get(0).getJid().asBareJid().toEscapedString()); } else { - mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.problem_connecting_to_accounts)); + mBuilder.setContentTitle( + mXmppConnectionService.getString(R.string.problem_connecting_to_accounts)); mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_fix)); } - mBuilder.addAction(R.drawable.ic_autorenew_white_24dp, + mBuilder.addAction( + R.drawable.ic_autorenew_white_24dp, mXmppConnectionService.getString(R.string.try_again), - createTryAgainIntent() - ); + createTryAgainIntent()); if (torNotAvailable) { if (TorServiceUtils.isOrbotInstalled(mXmppConnectionService)) { mBuilder.addAction( R.drawable.ic_play_circle_filled_white_48dp, mXmppConnectionService.getString(R.string.start_orbot), - PendingIntent.getActivity(mXmppConnectionService, 147, TorServiceUtils.LAUNCH_INTENT, 0) - ); + PendingIntent.getActivity( + mXmppConnectionService, 147, TorServiceUtils.LAUNCH_INTENT, 0)); } else { mBuilder.addAction( R.drawable.ic_file_download_white_24dp, mXmppConnectionService.getString(R.string.install_orbot), - PendingIntent.getActivity(mXmppConnectionService, 146, TorServiceUtils.INSTALL_INTENT, 0) - ); + PendingIntent.getActivity( + mXmppConnectionService, 146, TorServiceUtils.INSTALL_INTENT, 0)); } } mBuilder.setDeleteIntent(createDismissErrorIntent()); @@ -1269,7 +1556,9 @@ void updateErrorNotification() { intent.putExtra("jid", errors.get(0).getJid().asBareJid().toEscapedString()); intent.putExtra(EditAccountActivity.EXTRA_OPENED_FROM_NOTIFICATION, true); } - mBuilder.setContentIntent(PendingIntent.getActivity(mXmppConnectionService, 145, intent, PendingIntent.FLAG_UPDATE_CURRENT)); + mBuilder.setContentIntent( + PendingIntent.getActivity( + mXmppConnectionService, 145, intent, PendingIntent.FLAG_UPDATE_CURRENT)); if (Compatibility.runsTwentySix()) { mBuilder.setChannelId("error"); } @@ -1291,7 +1580,8 @@ void updateFileAddingNotification(int current, Message message) { } private void notify(String tag, int id, Notification notification) { - final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService); + final NotificationManagerCompat notificationManager = + NotificationManagerCompat.from(mXmppConnectionService); try { notificationManager.notify(tag, id, notification); } catch (RuntimeException e) { @@ -1300,7 +1590,8 @@ private void notify(String tag, int id, Notification notification) { } public void notify(int id, Notification notification) { - final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService); + final NotificationManagerCompat notificationManager = + NotificationManagerCompat.from(mXmppConnectionService); try { notificationManager.notify(id, notification); } catch (RuntimeException e) { @@ -1309,7 +1600,8 @@ public void notify(int id, Notification notification) { } public void cancel(int id) { - final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService); + final NotificationManagerCompat notificationManager = + NotificationManagerCompat.from(mXmppConnectionService); try { notificationManager.cancel(id); } catch (RuntimeException e) { @@ -1318,7 +1610,8 @@ public void cancel(int id) { } private void cancel(String tag, int id) { - final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService); + final NotificationManagerCompat notificationManager = + NotificationManagerCompat.from(mXmppConnectionService); try { notificationManager.cancel(tag, id); } catch (RuntimeException e) { @@ -1330,7 +1623,8 @@ private class VibrationRunnable implements Runnable { @Override public void run() { - final Vibrator vibrator = (Vibrator) mXmppConnectionService.getSystemService(Context.VIBRATOR_SERVICE); + final Vibrator vibrator = + (Vibrator) mXmppConnectionService.getSystemService(Context.VIBRATOR_SERVICE); vibrator.vibrate(CALL_PATTERN, -1); } } From d8fd59394cad303245c781ba6e6ef716c2284737 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 1 Jul 2022 15:54:21 +0200 Subject: [PATCH 119/394] fix array out of bounds. fixes #4334 --- .../util/EditMessageActionModeCallback.java | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/util/EditMessageActionModeCallback.java b/src/main/java/eu/siacs/conversations/ui/util/EditMessageActionModeCallback.java index fca8ebbbc..a859e14f8 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/EditMessageActionModeCallback.java +++ b/src/main/java/eu/siacs/conversations/ui/util/EditMessageActionModeCallback.java @@ -48,17 +48,28 @@ public class EditMessageActionModeCallback implements ActionMode.Callback { public EditMessageActionModeCallback(EditMessage editMessage) { this.editMessage = editMessage; - this.clipboardManager = (ClipboardManager) editMessage.getContext().getSystemService(Context.CLIPBOARD_SERVICE); + this.clipboardManager = + (ClipboardManager) + editMessage.getContext().getSystemService(Context.CLIPBOARD_SERVICE); } @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { + public boolean onCreateActionMode(final ActionMode mode, final Menu menu) { final MenuInflater inflater = mode.getMenuInflater(); inflater.inflate(R.menu.edit_message_actions, menu); final MenuItem pasteAsQuote = menu.findItem(R.id.paste_as_quote); final ClipData primaryClip = clipboardManager.getPrimaryClip(); - if (primaryClip != null && primaryClip.getItemCount() >= 0) { - pasteAsQuote.setVisible(primaryClip.getDescription().getMimeType(0).startsWith("text/") && !TextUtils.isEmpty(primaryClip.getItemAt(0).getText())); + if (primaryClip != null && primaryClip.getItemCount() > 0) { + final String mimeType; + try { + mimeType = primaryClip.getDescription().getMimeType(0); + } catch (final Exception e) { + pasteAsQuote.setVisible(false); + return true; + } + pasteAsQuote.setVisible( + mimeType.startsWith("text/") + && !TextUtils.isEmpty(primaryClip.getItemAt(0).getText())); } else { pasteAsQuote.setVisible(false); } @@ -71,10 +82,10 @@ public boolean onPrepareActionMode(ActionMode mode, Menu menu) { } @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + public boolean onActionItemClicked(final ActionMode mode, final MenuItem item) { if (item.getItemId() == R.id.paste_as_quote) { final ClipData primaryClip = clipboardManager.getPrimaryClip(); - if (primaryClip != null && primaryClip.getItemCount() >= 1) { + if (primaryClip != null && primaryClip.getItemCount() > 0) { editMessage.insertAsQuote(primaryClip.getItemAt(0).getText().toString()); return true; } @@ -83,7 +94,5 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { } @Override - public void onDestroyActionMode(ActionMode mode) { - - } + public void onDestroyActionMode(ActionMode mode) {} } From b97e2deaa29dad374c936ca46c2ca0dbb210192c Mon Sep 17 00:00:00 2001 From: Licaon_Kter Date: Wed, 17 Feb 2021 08:45:57 +0000 Subject: [PATCH 120/394] Show battery dialogue always --- .../conversations/ui/ConversationsActivity.java | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java index 6d0a03f8b..c31c3464b 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java @@ -221,8 +221,7 @@ private void setNeverAskForBatteryOptimizationsAgain() { } private void openBatteryOptimizationDialogIfNeeded() { - if (hasAccountWithoutPush() - && isOptimizingBattery() + if (isOptimizingBattery() && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M && getPreferences().getBoolean(getBatteryOptimizationPreferenceKey(), true)) { AlertDialog.Builder builder = new AlertDialog.Builder(this); @@ -245,15 +244,6 @@ && getPreferences().getBoolean(getBatteryOptimizationPreferenceKey(), true)) { } } - private boolean hasAccountWithoutPush() { - for (Account account : xmppConnectionService.getAccounts()) { - if (account.getStatus() == Account.State.ONLINE && !xmppConnectionService.getPushManagementService().available(account)) { - return true; - } - } - return false; - } - private void notifyFragmentOfBackendConnected(@IdRes int id) { final Fragment fragment = getFragmentManager().findFragmentById(id); if (fragment instanceof OnBackendConnected) { From 49851057116eb257ef2a2c02089ffc2a507846ad Mon Sep 17 00:00:00 2001 From: Licaon_Kter Date: Wed, 17 Feb 2021 08:50:14 +0000 Subject: [PATCH 121/394] Here too ...but why was that function created elsewhere if here you just compare this? --- .../java/eu/siacs/conversations/ui/EditAccountActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index e9c0bce39..fc21f6296 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -1045,7 +1045,7 @@ private void updateAccountInformation(boolean init) { if (this.mAccount.isOnlineAndConnected() && !this.mFetchingAvatar) { Features features = this.mAccount.getXmppConnection().getFeatures(); this.binding.stats.setVisibility(View.VISIBLE); - boolean showBatteryWarning = !xmppConnectionService.getPushManagementService().available(mAccount) && isOptimizingBattery(); + boolean showBatteryWarning = isOptimizingBattery(); boolean showDataSaverWarning = isAffectedByDataSaver(); showOsOptimizationWarning(showBatteryWarning, showDataSaverWarning); this.binding.sessionEst.setText(UIHelper.readableTimeDifferenceFull(this, this.mAccount.getXmppConnection() From 2364d7c46d39e04f81ed83ae873e56d9457fa625 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 9 Jul 2022 10:56:24 +0200 Subject: [PATCH 122/394] pulled translations from transifex --- src/main/res/values-it/strings.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index a665e79c7..ffc63ac41 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -989,4 +989,6 @@ Documento di testo Le registrazioni di profili non sono supportate Nessun indirizzo XMPP trovato - + Errore di autenticazione temporaneo + + From e455ed4f1ae10ebb82d88d98102c7a6673c43a6f Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 9 Jul 2022 14:45:59 +0200 Subject: [PATCH 123/394] fix orbot detection --- src/main/AndroidManifest.xml | 12 ++++---- .../conversations/utils/TorServiceUtils.java | 28 +++++++++++-------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index a3dd9822a..458c8a7f4 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -50,27 +50,29 @@ android:required="false" /> - + + + - + - + diff --git a/src/main/java/eu/siacs/conversations/utils/TorServiceUtils.java b/src/main/java/eu/siacs/conversations/utils/TorServiceUtils.java index 9fc9f5082..4f0e6fc53 100644 --- a/src/main/java/eu/siacs/conversations/utils/TorServiceUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/TorServiceUtils.java @@ -6,41 +6,47 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; +import android.widget.Toast; import eu.siacs.conversations.R; import me.drakeet.support.toast.ToastCompat; public class TorServiceUtils { - private final static String URI_ORBOT = "org.torproject.android"; + private static final String URI_ORBOT = "org.torproject.android"; private static final Uri ORBOT_PLAYSTORE_URI = Uri.parse("market://details?id=" + URI_ORBOT); - private final static String ACTION_START_TOR = "org.torproject.android.START_TOR"; + private static final String ACTION_START_TOR = "org.torproject.android.START_TOR"; public static final Intent INSTALL_INTENT = new Intent(Intent.ACTION_VIEW, ORBOT_PLAYSTORE_URI); public static final Intent LAUNCH_INTENT = new Intent(ACTION_START_TOR); - public final static String ACTION_STATUS = "org.torproject.android.intent.action.STATUS"; - public final static String EXTRA_STATUS = "org.torproject.android.intent.extra.STATUS"; + public static final String ACTION_STATUS = "org.torproject.android.intent.action.STATUS"; + public static final String EXTRA_STATUS = "org.torproject.android.intent.extra.STATUS"; - public static boolean isOrbotInstalled(Context context) { + public static boolean isOrbotInstalled(final Context context) { try { context.getPackageManager().getPackageInfo(URI_ORBOT, PackageManager.GET_ACTIVITIES); return true; - } catch (PackageManager.NameNotFoundException e) { + } catch (final PackageManager.NameNotFoundException e) { return false; } } - public static void downloadOrbot(Activity activity, int requestCode) { try { activity.startActivityForResult(INSTALL_INTENT, requestCode); - } catch (ActivityNotFoundException e) { - ToastCompat.makeText(activity, R.string.no_market_app_installed, ToastCompat.LENGTH_SHORT).show(); + } catch (final ActivityNotFoundException e) { + ToastCompat.makeText( + activity, R.string.no_market_app_installed, ToastCompat.LENGTH_SHORT) + .show(); } } - public static void startOrbot(Activity activity, int requestCode) { - activity.startActivityForResult(LAUNCH_INTENT, requestCode); + public static void startOrbot(final Activity activity, final int requestCode) { + try { + activity.startActivityForResult(LAUNCH_INTENT, requestCode); + } catch (final ActivityNotFoundException e) { + Toast.makeText(activity, R.string.install_orbot, Toast.LENGTH_LONG).show(); + } } } From abfe1f1dbd85d9b8e88b3b4a7e86e0bcf92c0465 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 9 Jul 2022 14:46:18 +0200 Subject: [PATCH 124/394] do not show toast when activity is gone. fixes #4335 --- .../eu/siacs/conversations/ui/ConversationFragment.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 1b334e5d4..2eb602257 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -710,8 +710,12 @@ public void success(Message message) { } @Override - public void error(final int error, Message message) { + public void error(final int error, final Message message) { hidePrepareFileToast(prepareFileToast); + final ConversationsActivity activity = ConversationFragment.this.activity; + if (activity == null) { + return; + } activity.runOnUiThread(() -> activity.replaceToast(getString(error))); } }); From 7d92ac365ddae026fc03cf66dbd2d1e96909f078 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 10 Jul 2022 10:46:49 +0200 Subject: [PATCH 125/394] version bump to 2.10.7 --- CHANGELOG.md | 6 ++++++ build.gradle | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 304fac6d9..4e748a2cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +### Version 2.10.7 + +* always ask for battery optimizations opt-out +* set local only flag on 'x connected accounts' notifications +* Minor bug fixes + ### Version 2.10.6 * Minor bug fixes diff --git a/build.gradle b/build.gradle index ad5a88b9c..b48ffb58e 100644 --- a/build.gradle +++ b/build.gradle @@ -91,8 +91,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 30 - versionCode 42032 - versionName "2.10.6" + versionCode 42033 + versionName "2.10.7" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId From dd30951dfb968afea503872a98bcde946945e418 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 10 Jul 2022 13:00:34 +0200 Subject: [PATCH 126/394] every device is 21+ now --- .../eu/siacs/conversations/persistance/FileBackend.java | 8 ++++---- .../java/eu/siacs/conversations/ui/util/Attachment.java | 2 +- .../java/eu/siacs/conversations/utils/Compatibility.java | 4 ---- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 821899eb7..faeca6308 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -437,7 +437,7 @@ public Bitmap getPreviewForUri(Attachment attachment, int size, boolean cacheOnl return bitmap; } final String mime = attachment.getMime(); - if ("application/pdf".equals(mime) && Compatibility.runsTwentyOne()) { + if ("application/pdf".equals(mime)) { bitmap = cropCenterSquarePdf(attachment.getUri(), size); drawOverlay( bitmap, @@ -961,7 +961,7 @@ public Bitmap getThumbnail(Message message, int size, boolean cacheOnly) throws } DownloadableFile file = getFile(message); final String mime = file.getMimeType(); - if ("application/pdf".equals(mime) && Compatibility.runsTwentyOne()) { + if ("application/pdf".equals(mime)) { thumbnail = getPdfDocumentPreview(file, size); } else if (mime.startsWith("video/")) { thumbnail = getVideoPreview(file, size); @@ -1507,12 +1507,12 @@ public void updateFileParams(Message message, String url) { body.append(url); } body.append('|').append(file.getSize()); - if (image || video || (pdf && Compatibility.runsTwentyOne())) { + if (image || video || pdf) { try { final Dimensions dimensions; if (video) { dimensions = getVideoDimensions(file); - } else if (pdf && Compatibility.runsTwentyOne()) { + } else if (pdf) { dimensions = getPdfDocumentDimensions(file); } else { dimensions = getImageDimensions(file); diff --git a/src/main/java/eu/siacs/conversations/ui/util/Attachment.java b/src/main/java/eu/siacs/conversations/ui/util/Attachment.java index b539c70ef..f994955d0 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/Attachment.java +++ b/src/main/java/eu/siacs/conversations/ui/util/Attachment.java @@ -179,7 +179,7 @@ public boolean renderThumbnail() { private static boolean renderFileThumbnail(final String mime) { return mime.startsWith("video/") || isImage(mime) - || (Compatibility.runsTwentyOne() && "application/pdf".equals(mime)); + || "application/pdf".equals(mime); } public Uri getUri() { diff --git a/src/main/java/eu/siacs/conversations/utils/Compatibility.java b/src/main/java/eu/siacs/conversations/utils/Compatibility.java index f181853c6..aadeaabf5 100644 --- a/src/main/java/eu/siacs/conversations/utils/Compatibility.java +++ b/src/main/java/eu/siacs/conversations/utils/Compatibility.java @@ -43,10 +43,6 @@ public static boolean hasStoragePermission(Context context) { return Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission(context, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; } - public static boolean runsTwentyOne() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; - } - private static boolean runsTwentyFour() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N; } From 8027b3be248a175c68573c65824319a6747ff89a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 22 Jul 2022 10:39:18 +0200 Subject: [PATCH 127/394] parse pep events only from bare jid --- .../eu/siacs/conversations/parser/MessageParser.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index f45b1e89b..86799bd11 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -196,8 +196,8 @@ private Invite extractInvite(Element message) { } private void parseEvent(final Element event, final Jid from, final Account account) { - Element items = event.findChild("items"); - String node = items == null ? null : items.getAttribute("node"); + final Element items = event.findChild("items"); + final String node = items == null ? null : items.getAttribute("node"); if ("urn:xmpp:avatar:metadata".equals(node)) { Avatar avatar = Avatar.parseMetadata(items); if (avatar != null) { @@ -1000,7 +1000,7 @@ public void onMessagePacketReceived(Account account, MessagePacket original) { } final Element event = original.findChild("event", "http://jabber.org/protocol/pubsub#event"); - if (event != null && InvalidJid.hasValidFrom(original)) { + if (event != null && InvalidJid.hasValidFrom(original) && original.getFrom().isBareJid()) { if (event.hasChild("items")) { parseEvent(event, original.getFrom(), account); } else if (event.hasChild("delete")) { @@ -1012,6 +1012,9 @@ public void onMessagePacketReceived(Account account, MessagePacket original) { final String nick = packet.findChildContent("nick", Namespace.NICK); if (nick != null && InvalidJid.hasValidFrom(original)) { + if (mXmppConnectionService.isMuc(account, from)) { + return; + } final Contact contact = account.getRoster().getContact(from); if (contact.setPresenceName(nick)) { mXmppConnectionService.syncRoster(account); From 78c3b1f527ce56768754b0ab1555498614b71c46 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 22 Jul 2022 16:54:18 +0200 Subject: [PATCH 128/394] pulled translations from transifex --- src/main/res/values-ja/strings.xml | 4 +- src/main/res/values-pl/strings.xml | 4 +- src/main/res/values-sv/strings.xml | 172 +++++++++++++++++++++++++++++ 3 files changed, 178 insertions(+), 2 deletions(-) diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml index a207bb71c..7fb4f6ae3 100644 --- a/src/main/res/values-ja/strings.xml +++ b/src/main/res/values-ja/strings.xml @@ -957,4 +957,6 @@ プレーンテキスト文書 アカウント登録はサポートされていません XMPPアドレスがみつかりません - + 一時的な認証失敗 + + diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 4b49a6501..30f28a9ad 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -1003,4 +1003,6 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Dokument zwykłego tekstu Rejestracja kont nie jest wspierana Nie znaleziono adresu XMPP - + Tymczasowy błąd uwierzytelniania + + diff --git a/src/main/res/values-sv/strings.xml b/src/main/res/values-sv/strings.xml index d73433c36..14e91099b 100644 --- a/src/main/res/values-sv/strings.xml +++ b/src/main/res/values-sv/strings.xml @@ -201,6 +201,7 @@ XEP-0191: Blocking Command XEP-0237: Roster Versioning XEP-0198: Stream Management + XEP-0215: External Service Discovery XEP-0163: PEP (Avatarbilder / OMEMO) XEP-0363: Ladda upp via HTTP XEP-0357: Push @@ -464,6 +465,7 @@ Nerladdning gick fel: Filen hittades inte Nerladdningen gick fel: Kunder inte ansluta till server Nerladdning gick fel: Kunde inte skriva fil + Nedladdning misslyckades: Ogiltig fil Tor-nätverk ej tillgängligt Bind-fel Den här servern ansvarar inte för den här domänen @@ -537,11 +539,15 @@ Säkerhetsfel: Ogiltig filåtkomst! Ingen applikation hittades för att dela URI Dela URI med... +
Du registrerar dig med ditt telefonnummer och Quicksy kommer automatiskt – baserat på telefonnumren i din adressbok – att föreslå möjliga kontakter till dig.

Genom att registrera dig godkänner du vår integritetspolicy.]]>
Acceptera och gå vidare + En guide har skapats för kontoskapande på conversations.im.¹\nNär du väljer conversations.im som leverantör kommer du att kunna kommunicera med användare av andra leverantörer genom att ge dem din fullständiga XMPP-adress. Din fullständiga XMPP-adress kommer att vara: %s Skapa konto Använd min egen leverantör Välj användarnamn + Hantera tillgänglighet manuellt + Ställ in din tillgänglighet när du redigerar ditt statusmeddelande. Statusmeddelande Tillgänglig Online @@ -559,6 +565,8 @@ Kort Medium Lång + Gör användandet offentligt + Låter dina kontakter veta när du använder Conversations Privatliv Tema Välj färgschema @@ -591,6 +599,8 @@ Visa felmeddelande Felmeddelande Databesparing + Ditt operativsystem begränsar åtkomsten till Internet i bakgrunden för %1$s. För att få aviseringar om nya meddelanden bör du tillåta obegränsad åtkomst för %1$s, när databesparing är på.\n %1$s kommer fortfarande att anstränga sig för att spara data när det är möjligt. + Din enhet stöder inte inaktivering av databesparing för %1$s. Det gick inte att skapa en tillfällig fil Denna enhet har verifierats Kopiera fingeravtryck @@ -613,10 +623,13 @@ Rensa privat lagring där filer lagras (De kan om-laddas från servern) Jag följde denna länk från en trovärdig källa Du håller på att verifiera OMEMO-nyckeln för %1$s efter att du följt en länk. Detta är endast säkert om du följde länken från en trovärdig källa där endast %2$s kan ha publiserat denna länk. + Du är på väg att verifiera OMEMO-nycklarna för ditt eget konto. Detta är bara säkert om du följde den här länken från en pålitlig källa där bara du kunde ha publicerat den här länken. + Fortsätt Verifiera OMEMO-nycklar Visa inaktiva Dölj inaktiva Lita ej på enhet + Är du säker på att du vill ta bort verifieringen av den här enheten?\nDen här enheten och meddelanden från den kommer att markeras som \"Ej betrodd\". %d sekund %d sekunder @@ -649,12 +662,15 @@ Korresponderande konversationer är stängda. Kontakt blockerad. Notifieringar från främlingar + Meddela för meddelanden och samtal från främlingar. Mottagna meddelanden från främlingar Blockera främling Blockera hel domän online just nu Försök dekryptera igen Sessionsfel + Nedgraderad SASL-mekanism + Servern kräver registrering via webbplatsen Öppna webbsida Ingen applikation hittades för att kunna öppna webbsidan Se upp-notifikationer @@ -662,46 +678,110 @@ Idag Igår Bekräfta värdnamn med DNSSEC + Servercertifikat som innehåller det validerade värdnamnet anses vara verifierade Certifikatet innehåller ej en XMPP-adress delvis Spela in video Kopiera till urklipp Meddelande kopierat till urklipp Meddelande + Privata meddelanden är inaktiverade + Skyddade applikationer + För att fortsätta ta emot aviseringar, även när skärmen är avstängd, måste du lägga till Conversations i listan över skyddade applikationer. Godkänn okänt certifikat? Servercertifikatet är inte signerat av en känd certifikatutfärdare. + Acceptera servernamn som inte matchar? + Servern kunde inte autentisera som \"%s\". Certifikatet är endast giltigt för: Vill du ansluta ändå? Certifikatdetaljer: + En gång + QR-läsaren behöver åtkomst till kameran + Bläddra till botten + Bläddra ner efter att du har skickat ett meddelande + Redigera Statusmeddelande + Redigera statusmeddelande + Inaktivera kryptering + %1$s kan inte skicka krypterade meddelanden till %2$s. Detta kan bero på att din kontakt använder en föråldrad server eller klient som inte kan hantera OMEMO. + Det gick inte att hämta enhetslistan + Det gick inte att hämta krypteringsnycklar + Tips: I vissa fall kan detta åtgärdas genom att lägga till varandra i era respektive kontaktlistor. + Är du säker på att du vill inaktivera OMEMO-kryptering för den här konversationen?\nDetta gör att din serveradministratör kan läsa dina meddelanden, men det kan också vara det enda sättet att kommunicera med människor som använder äldre klienter. + Inaktivera nu Utkast: + OMEMO-kryptering + OMEMO kommer alltid att användas för privata konversationer och privata gruppchattar. + OMEMO kommer att användas som standard för nya konversationer. + OMEMO måste manuellt aktiveras för varje ny konversation. Skapa genväg + Textstorlek + Den relativa teckenstorleken som används i appen. + På som standard + Av som standard Liten Mellan Stor + Meddelandet är inte krypterat för den här enheten. + Misslyckades med att dekryptera OMEMO-meddelandet. + ångra + Platsdelning är inaktiverat + Lås position + Lås upp position Kopiera plats Dela plats + Hänvisningar Dela plats Visa plats Dela + Det gick inte att starta inspelningen Var god dröj... + Ge %1$s tillgång till mikrofonen Söka i meddelanden GIF + Visa konversation + Dela plats-tillägget + Kopiera webbadress Kopiera XMPP-adress + HTTP-fildelning för S3 + Direktsök + Gruppkonversationens visningsbild + Värden stöder inte visningsbilder för gruppkonversationer + Endast ägaren kan ändra visningsbilden för gruppkonversationen + Kontaktnamn Smeknamn Namn Att ange ett namn är valfritt Gruppchattens namn + Kunde inte att spara inspelningen + Förgrundsservice + Statusinformation + Anslutningsproblem + Meddelanden + Samtal + Meddelanden + Inkommande samtal + Pågående samtal + Tysta meddelanden + Misslyckade leveranser + Videokompression + Visa media Deltagare + Mediautforskare + Videokvalitet Mellan (360p) Hög (720p) + avbruten + Du håller redan på att skriva ett meddelande. Välj ett land telefonnummer Bekräfta ditt telefonnummer + tillbaka Ja Nej Bekräftar... Okänt nätverksfel. För många försök Du använder en föråldrad version av denna app. + Uppdatera Ditt namn Skriv in ditt namn Avslå begäran @@ -709,38 +789,130 @@ Starta Orbot e-bok Öppna med... + Konversationens profilbild Välj konto Återställa säkerhetskopiering Återställa Ange ditt lösenord till kontot %s för att återställa säkerhetskopian. Det gick inte att återställa säkerhetskopian. + Säkerhetskopia & Återställ + Ange XMPP-adress Skapa gruppchatt + Anslut till publik gruppkonversation Skapa sluten gruppchatt + Skapa publik gruppkonversation Kanalnamn XMPP-adress Vänligen ange ett namn på kanalen Ange en XMPP-adress Detta är en XMPP-adress. Ange ett namn. + Skapar publik gruppkonversation... Denna kanal finns redan Du har gått med i en befintlig kanal + Det gick inte att spara kanalkonfigurationen + Tillåt vem som helst att ändra ämnet + Tillåt vem som helst att bjuda in andra + Vem som helst kan ändra ämnet. + Ägaren kan ändra ämnet. + Administratörer kan ändra ämnet. + Ägare kan bjuda in andra. + Vem som helst kan bjuda in andra. XMPP-adresser är synliga för administratörer. XMPP-adresser är synliga för alla. + Den här publika gruppkonversationen har inga deltagare. Bjud in dina kontakter eller använd \'dela-knappen\' för att dela XMPP-adressen. Denna slutna gruppchatt har inga deltagare. Hantera rättigheter + Sök efter deltagare För stor fil Bifoga Upptäck kanaler + Sök efter gruppkonversationer + Möjlig integritetskränkning! Jag har redan ett konto Lägg till befintligt konto Skapa nytt konto Detta verkar vara ett domännamn Lägg till ändå Detta ser ut som en kanaladress + Dela säkerhetskopior + Säkerhetskopior för Conversations + Händelse + Öppna säkerhetskopia Filen du valde är inte en säkerhetskopia till Conversations + Det här kontot har redan konfigurerats + Var god ange lösenordet för det här kontot + Det gick inte att utföra den här åtgärden + Anslut till publik gruppkonversation... + Delnings-appen gav inte behörighet till att komma åt den här filen. + + jabber.network + Lokal server + De flesta användare bör välja \"jabber.network\" för bättre förslag från hela det offentliga XMPP-ekosystemet. + Metod för kanalupptäckt + Säkerhetskopiering Om Aktivera ett konto + Ring + Inkommande samtal + Inkommande videosamtal + Ansluter + Ansluten + Återansluter + Accepterar samtal + Avslutar samtal + Svara + Avvisa + Upptäcker enheter + Ringer Upptagen + Kunde inte koppla samtal + Anslutning bröts + Återkallat samtal + Appmisslyckande + Verifikationsproblem + Lägg på + Pågående samtal + Pågående videosamtal + Återansluter samtalet + Återansluter videosamtalet + Inaktivera Tor för att ringa samtal + Inkommande samtal + Inkommande samtal · %s + Missat samtal · %s + Utgående samtal + Pågående samtal · %s + Missat samtal + Röstsamtal + Videosamtal + Hjälp + Växla till konversation + Din mikrofon är inte tillgänglig + Du kan bara ha ett samtal åt gången. + Återgå till pågående samtal + Kunde inte växla kamera Fäst flik till toppen Ta bort flik från toppen + GPX-spår + Kunde inte korrigera meddelandet + Alla konversationer + Den här konversationen + Din visningsbild + Visningsbild för %s + Krypterad med OMEMO + Krypterad med OpenPGP + Inte krypterad + Avsluta + Spela in ett röstmeddelande + Spela upp ljud + Pausa ljud + Lägg till kontakt, skapa eller gå med i gruppchatt eller upptäck kanaler + + Visa %1$d deltagare + Visa %1$d deltagare + + Misslyckade leveranser Fler alternativ + Ingen applikation hittades + Bjud in till Conversations + Ingen XMPP-adress hittades From b6ce914f6209fc3a2195d75283fee5234c0d1c69 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 22 Jul 2022 20:30:47 +0200 Subject: [PATCH 129/394] version bump to 2.10.8 --- CHANGELOG.md | 4 ++++ build.gradle | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e748a2cf..90365d20e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### Version 2.10.8 + +* Fix wrong avatar being shown for group chats + ### Version 2.10.7 * always ask for battery optimizations opt-out diff --git a/build.gradle b/build.gradle index b48ffb58e..900cead2d 100644 --- a/build.gradle +++ b/build.gradle @@ -91,8 +91,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 30 - versionCode 42033 - versionName "2.10.7" + versionCode 42034 + versionName "2.10.8" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId From 62a379862e3ccbcaa4631960da4dedeaa9e0517d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 1 Aug 2022 10:14:49 +0200 Subject: [PATCH 130/394] jingle rtp: improve logging and error reporting --- .../eu/siacs/conversations/parser/AbstractParser.java | 2 +- .../java/eu/siacs/conversations/parser/MessageParser.java | 3 ++- .../xmpp/jingle/JingleConnectionManager.java | 4 ++-- .../conversations/xmpp/jingle/JingleRtpConnection.java | 8 ++++---- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java index 000fa036d..f4b01b7d3 100644 --- a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java +++ b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java @@ -126,7 +126,7 @@ public static MucOptions.User parseItem(Conversation conference, Element item, J return user; } - public static String extractErrorMessage(Element packet) { + public static String extractErrorMessage(final Element packet) { final Element error = packet.findChild("error"); if (error != null && error.getChildren().size() > 0) { final List errorNames = orderedElementNames(error.getChildren()); diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 86799bd11..5c66451ce 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -327,7 +327,8 @@ private boolean handleErrorMessage(final Account account, final MessagePacket pa } if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROCEED_ID_PREFIX)) { final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROCEED_ID_PREFIX.length()); - mXmppConnectionService.getJingleConnectionManager().failProceed(account, from, sessionId); + final String message = extractErrorMessage(packet); + mXmppConnectionService.getJingleConnectionManager().failProceed(account, from, sessionId, message); return true; } mXmppConnectionService.markMessage(account, diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 0f9694915..416877236 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -908,12 +908,12 @@ public void endRtpSession(final String sessionId) { } } - public void failProceed(Account account, final Jid with, String sessionId) { + public void failProceed(Account account, final Jid with, final String sessionId, final String message) { final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with, sessionId); final AbstractJingleConnection existingJingleConnection = connections.get(id); if (existingJingleConnection instanceof JingleRtpConnection) { - ((JingleRtpConnection) existingJingleConnection).deliverFailedProceed(); + ((JingleRtpConnection) existingJingleConnection).deliverFailedProceed(message); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index fd918fa9b..353851c37 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -797,7 +797,7 @@ private synchronized void sendSessionAccept( } catch (final WebRTCWrapper.InitializationException e) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to initialize WebRTC"); webRTCWrapper.close(); - sendSessionTerminate(Reason.FAILED_APPLICATION); + sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage()); return; } final org.webrtc.SessionDescription sdp = @@ -928,10 +928,10 @@ synchronized void deliveryMessage( } } - void deliverFailedProceed() { + void deliverFailedProceed(final String message) { Log.d( Config.LOGTAG, - id.account.getJid().asBareJid() + ": receive message error for proceed message"); + id.account.getJid().asBareJid() + ": receive message error for proceed message ("+Strings.nullToEmpty(message)+")"); if (transition(State.TERMINATED_CONNECTIVITY_ERROR)) { webRTCWrapper.close(); Log.d( @@ -1270,7 +1270,7 @@ private void failureToInitiateSession(final Throwable throwable, final State tar webRTCWrapper.close(); final Reason reason = Reason.ofThrowable(throwable); if (isInState(targetState)) { - sendSessionTerminate(reason); + sendSessionTerminate(reason, throwable.getMessage()); } else { sendRetract(reason); } From 67f021426bc94699c3ce3b066a61c3a3babe40a1 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 4 Aug 2022 11:31:58 +0200 Subject: [PATCH 131/394] remove null bytes from strings before creating sql statements in backup --- .../conversations/services/ExportBackupService.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/ExportBackupService.java b/src/main/java/eu/siacs/conversations/services/ExportBackupService.java index f89434897..6cbb26ad1 100644 --- a/src/main/java/eu/siacs/conversations/services/ExportBackupService.java +++ b/src/main/java/eu/siacs/conversations/services/ExportBackupService.java @@ -15,6 +15,7 @@ import androidx.core.app.NotificationCompat; +import com.google.common.base.CharMatcher; import com.google.common.base.Strings; import java.io.DataOutputStream; @@ -114,7 +115,7 @@ private static void accountExport(final SQLiteDatabase db, final String uuid, fi } builder.append(intValue); } else { - DatabaseUtils.appendEscapedSQLString(builder, value); + appendEscapedSQLString(builder, value); } } builder.append(")"); @@ -127,6 +128,10 @@ private static void accountExport(final SQLiteDatabase db, final String uuid, fi writer.append(builder.toString()); } + private static void appendEscapedSQLString(final StringBuilder sb, final String sqlString) { + DatabaseUtils.appendEscapedSQLString(sb, CharMatcher.is('\u0000').removeFrom(sqlString)); + } + private static void simpleExport(SQLiteDatabase db, String table, String column, String uuid, PrintWriter writer) { final Cursor cursor = db.query(table, null, column + "=?", new String[]{uuid}, null, null, null); while (cursor != null && cursor.moveToNext()) { @@ -201,7 +206,7 @@ private static void appendValues(final Cursor cursor, final StringBuilder builde } else if (value.matches("[0-9]+")) { builder.append(value); } else { - DatabaseUtils.appendEscapedSQLString(builder, value); + appendEscapedSQLString(builder, value); } } builder.append(")"); From 1f3743122fbd1f1188d6830955d103fc7a78edf2 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 4 Aug 2022 11:32:48 +0200 Subject: [PATCH 132/394] upgrade okhttp --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 900cead2d..ddd1f46a1 100644 --- a/build.gradle +++ b/build.gradle @@ -71,7 +71,7 @@ dependencies { implementation "com.squareup.retrofit2:retrofit:2.9.0" implementation "com.squareup.retrofit2:converter-gson:2.9.0" - implementation "com.squareup.okhttp3:okhttp:4.9.3" + implementation "com.squareup.okhttp3:okhttp:4.10.0" implementation 'com.google.guava:guava:30.1.1-android' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.49' From 353c4f118d4ffbb93309a00b893535549e4607fe Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 5 Aug 2022 10:45:44 +0200 Subject: [PATCH 133/394] use threemas webrtc build (trial) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ddd1f46a1..5409afedc 100644 --- a/build.gradle +++ b/build.gradle @@ -75,7 +75,7 @@ dependencies { implementation 'com.google.guava:guava:30.1.1-android' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.49' - implementation fileTree(include: ['libwebrtc-m99.aar'], dir: 'libs') + implementation 'ch.threema:webrtc-android:100.0.0' } ext { From d41020ccf3781fbfac45f8bac2249d9ac6ec9aed Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 5 Aug 2022 10:46:12 +0200 Subject: [PATCH 134/394] ignore race condition after reject from notification fixes #4351 fixes #4261 --- .../xmpp/jingle/JingleConnectionManager.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 416877236..b39673fa5 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -892,7 +892,15 @@ public void rejectRtpSession(final String sessionId) { for (final AbstractJingleConnection connection : this.connections.values()) { if (connection.getId().sessionId.equals(sessionId)) { if (connection instanceof JingleRtpConnection) { - ((JingleRtpConnection) connection).rejectCall(); + try { + ((JingleRtpConnection) connection).rejectCall(); + return; + } catch (final IllegalStateException e) { + Log.w( + Config.LOGTAG, + "race condition on rejecting call from notification", + e); + } } } } From 50ba165746972117e6fcb3a4554ef3456b3fbf4d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 10 Jul 2022 12:34:02 +0200 Subject: [PATCH 135/394] bump targetSdk to 32 --- build.gradle | 4 +- src/conversations/AndroidManifest.xml | 3 +- src/main/AndroidManifest.xml | 16 +++- .../services/NotificationService.java | 96 ++++++++++++++----- .../services/XmppConnectionService.java | 17 +++- .../conversations/utils/Compatibility.java | 68 ++++++++----- 6 files changed, 142 insertions(+), 62 deletions(-) diff --git a/build.gradle b/build.gradle index 5409afedc..9e1366ce3 100644 --- a/build.gradle +++ b/build.gradle @@ -86,11 +86,11 @@ ext { android { namespace 'eu.siacs.conversations' - compileSdkVersion 31 + compileSdkVersion 32 defaultConfig { minSdkVersion 21 - targetSdkVersion 30 + targetSdkVersion 32 versionCode 42034 versionName "2.10.8" archivesBaseName += "-$versionName" diff --git a/src/conversations/AndroidManifest.xml b/src/conversations/AndroidManifest.xml index bf2297949..d573b32d3 100644 --- a/src/conversations/AndroidManifest.xml +++ b/src/conversations/AndroidManifest.xml @@ -26,7 +26,8 @@ + android:launchMode="singleTask" + android:exported="true"> diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 458c8a7f4..2a48da7a0 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -83,7 +83,9 @@ - + @@ -107,6 +109,7 @@ android:label="@string/title_activity_show_location" /> @@ -127,6 +130,7 @@ android:windowSoftInputMode="stateAlwaysHidden" /> @@ -166,6 +170,7 @@ @@ -174,6 +179,7 @@ @@ -192,6 +198,7 @@ @@ -225,6 +232,7 @@ android:label="@string/group_chat_avatar" /> @@ -261,10 +269,6 @@ - - - - @@ -302,6 +307,7 @@ diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index abe6e9e11..acac26cc0 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.services; +import static eu.siacs.conversations.utils.Compatibility.s; + import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationChannelGroup; @@ -625,7 +627,9 @@ private PendingIntent createPendingRtpSession( mXmppConnectionService, requestCode, fullScreenIntent, - PendingIntent.FLAG_UPDATE_CURRENT); + s() + ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } public void cancelIncomingCallNotification() { @@ -759,7 +763,7 @@ private void updateNotification( && conversations != null && conversations.size() == 1; // if this check is changed to > 0 catchup messages will - // create one notification per conversation + // create one notification per conversation if (notifications.size() == 0) { cancel(NOTIFICATION_ID); @@ -835,9 +839,7 @@ private void modifyForSoundVibrationAndLight( } else { mBuilder.setLocalOnly(true); } - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - mBuilder.setCategory(Notification.CATEGORY_MESSAGE); - } + mBuilder.setCategory(Notification.CATEGORY_MESSAGE); mBuilder.setPriority( notify ? (headsup @@ -1280,7 +1282,9 @@ private PendingIntent createShowLocationIntent(final Message message) { mXmppConnectionService, generateRequestCode(message.getConversation(), 18), intent, - PendingIntent.FLAG_UPDATE_CURRENT); + s() + ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } } return null; @@ -1299,13 +1303,17 @@ private PendingIntent createContentIntent( mXmppConnectionService, generateRequestCode(conversationUuid, 8), viewConversationIntent, - PendingIntent.FLAG_UPDATE_CURRENT); + s() + ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } else { return PendingIntent.getActivity( mXmppConnectionService, generateRequestCode(conversationUuid, 10), viewConversationIntent, - PendingIntent.FLAG_UPDATE_CURRENT); + s() + ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } } @@ -1332,9 +1340,20 @@ private PendingIntent createDeleteIntent(Conversation conversation) { if (conversation != null) { intent.putExtra("uuid", conversation.getUuid()); return PendingIntent.getService( - mXmppConnectionService, generateRequestCode(conversation, 20), intent, 0); + mXmppConnectionService, + generateRequestCode(conversation, 20), + intent, + s() + ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } - return PendingIntent.getService(mXmppConnectionService, 0, intent, 0); + return PendingIntent.getService( + mXmppConnectionService, + 0, + intent, + s() + ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } private PendingIntent createReplyIntent( @@ -1348,7 +1367,12 @@ private PendingIntent createReplyIntent( intent.putExtra("last_message_uuid", lastMessageUuid); final int id = generateRequestCode(conversation, dismissAfterReply ? 12 : 14); return PendingIntent.getService( - mXmppConnectionService, id, intent, PendingIntent.FLAG_UPDATE_CURRENT); + mXmppConnectionService, + id, + intent, + s() + ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } private PendingIntent createReadPendingIntent(Conversation conversation) { @@ -1360,7 +1384,9 @@ private PendingIntent createReadPendingIntent(Conversation conversation) { mXmppConnectionService, generateRequestCode(conversation, 16), intent, - PendingIntent.FLAG_UPDATE_CURRENT); + s() + ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } private PendingIntent createCallAction(String sessionId, final String action, int requestCode) { @@ -1369,7 +1395,12 @@ private PendingIntent createCallAction(String sessionId, final String action, in intent.setPackage(mXmppConnectionService.getPackageName()); intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, sessionId); return PendingIntent.getService( - mXmppConnectionService, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT); + mXmppConnectionService, + requestCode, + intent, + s() + ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } private PendingIntent createSnoozeIntent(Conversation conversation) { @@ -1381,19 +1412,33 @@ private PendingIntent createSnoozeIntent(Conversation conversation) { mXmppConnectionService, generateRequestCode(conversation, 22), intent, - PendingIntent.FLAG_UPDATE_CURRENT); + s() + ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } private PendingIntent createTryAgainIntent() { final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); intent.setAction(XmppConnectionService.ACTION_TRY_AGAIN); - return PendingIntent.getService(mXmppConnectionService, 45, intent, 0); + return PendingIntent.getService( + mXmppConnectionService, + 45, + intent, + s() + ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } private PendingIntent createDismissErrorIntent() { final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); intent.setAction(XmppConnectionService.ACTION_DISMISS_ERROR_NOTIFICATIONS); - return PendingIntent.getService(mXmppConnectionService, 69, intent, 0); + return PendingIntent.getService( + mXmppConnectionService, + 69, + intent, + s() + ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } private boolean wasHighlightedOrPrivate(final Message message) { @@ -1538,15 +1583,9 @@ void updateErrorNotification() { } } mBuilder.setDeleteIntent(createDismissErrorIntent()); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - mBuilder.setVisibility(Notification.VISIBILITY_PRIVATE); - mBuilder.setSmallIcon(R.drawable.ic_warning_white_24dp); - } else { - mBuilder.setSmallIcon(R.drawable.ic_stat_alert_warning); - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { - mBuilder.setLocalOnly(true); - } + mBuilder.setVisibility(Notification.VISIBILITY_PRIVATE); + mBuilder.setSmallIcon(R.drawable.ic_warning_white_24dp); + mBuilder.setLocalOnly(true); mBuilder.setPriority(Notification.PRIORITY_LOW); final Intent intent; if (AccountUtils.MANAGE_ACCOUNT_ACTIVITY != null) { @@ -1558,7 +1597,12 @@ void updateErrorNotification() { } mBuilder.setContentIntent( PendingIntent.getActivity( - mXmppConnectionService, 145, intent, PendingIntent.FLAG_UPDATE_CURRENT)); + mXmppConnectionService, + 145, + intent, + s() + ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT)); if (Compatibility.runsTwentySix()) { mBuilder.setChannelId("error"); } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 7965a4e31..a6823e670 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -36,6 +36,7 @@ import android.provider.ContactsContract; import android.security.KeyChain; import android.telephony.PhoneStateListener; +import android.telephony.TelephonyCallback; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.DisplayMetrics; @@ -1203,9 +1204,10 @@ public void onError(Exception e) { private void setupPhoneStateListener() { final TelephonyManager telephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); - if (telephonyManager != null) { - telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); + if (telephonyManager == null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + return; } + telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); } public boolean isPhoneInCall() { @@ -1402,7 +1404,16 @@ public void scheduleWakeUpCall(int seconds, int requestCode) { final Intent intent = new Intent(this, EventReceiver.class); intent.setAction("ping"); try { - PendingIntent pendingIntent = PendingIntent.getBroadcast(this, requestCode, intent, 0); + final PendingIntent pendingIntent; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + pendingIntent = + PendingIntent.getBroadcast( + this, requestCode, intent, PendingIntent.FLAG_IMMUTABLE); + } else { + pendingIntent = + PendingIntent.getBroadcast( + this, requestCode, intent, 0); + } alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingIntent); } catch (RuntimeException e) { Log.e(Config.LOGTAG, "unable to schedule alarm for ping", e); diff --git a/src/main/java/eu/siacs/conversations/utils/Compatibility.java b/src/main/java/eu/siacs/conversations/utils/Compatibility.java index aadeaabf5..21004e26a 100644 --- a/src/main/java/eu/siacs/conversations/utils/Compatibility.java +++ b/src/main/java/eu/siacs/conversations/utils/Compatibility.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.utils; +import static eu.siacs.conversations.services.EventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE; + import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; @@ -24,23 +26,26 @@ import eu.siacs.conversations.ui.SettingsActivity; import eu.siacs.conversations.ui.SettingsFragment; -import static eu.siacs.conversations.services.EventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE; - public class Compatibility { - private static final List UNUSED_SETTINGS_POST_TWENTYSIX = Arrays.asList( - "led", - "notification_ringtone", - "notification_headsup", - "vibrate_on_notification" - ); - private static final List UNUESD_SETTINGS_PRE_TWENTYSIX = Collections.singletonList( - "message_notification_settings" - ); - + private static final List UNUSED_SETTINGS_POST_TWENTYSIX = + Arrays.asList( + "led", + "notification_ringtone", + "notification_headsup", + "vibrate_on_notification"); + private static final List UNUESD_SETTINGS_PRE_TWENTYSIX = + Collections.singletonList("message_notification_settings"); public static boolean hasStoragePermission(Context context) { - return Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission(context, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; + return Build.VERSION.SDK_INT < Build.VERSION_CODES.M + || ContextCompat.checkSelfPermission( + context, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) + == PackageManager.PERMISSION_GRANTED; + } + + public static boolean s() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S; } private static boolean runsTwentyFour() { @@ -66,20 +71,22 @@ private static SharedPreferences getPreferences(final Context context) { private static boolean targetsTwentySix(Context context) { try { final PackageManager packageManager = context.getPackageManager(); - final ApplicationInfo applicationInfo = packageManager.getApplicationInfo(context.getPackageName(), 0); + final ApplicationInfo applicationInfo = + packageManager.getApplicationInfo(context.getPackageName(), 0); return applicationInfo == null || applicationInfo.targetSdkVersion >= 26; } catch (PackageManager.NameNotFoundException | RuntimeException e) { - return true; //when in doubt… + return true; // when in doubt… } } private static boolean targetsTwentyFour(Context context) { try { final PackageManager packageManager = context.getPackageManager(); - final ApplicationInfo applicationInfo = packageManager.getApplicationInfo(context.getPackageName(), 0); + final ApplicationInfo applicationInfo = + packageManager.getApplicationInfo(context.getPackageName(), 0); return applicationInfo == null || applicationInfo.targetSdkVersion >= 24; } catch (PackageManager.NameNotFoundException | RuntimeException e) { - return true; //when in doubt… + return true; // when in doubt… } } @@ -92,14 +99,23 @@ public static boolean runsAndTargetsTwentyFour(Context context) { } public static boolean keepForegroundService(Context context) { - return runsAndTargetsTwentySix(context) || getBooleanPreference(context, SettingsActivity.KEEP_FOREGROUND_SERVICE, R.bool.enable_foreground_service); + return runsAndTargetsTwentySix(context) + || getBooleanPreference( + context, + SettingsActivity.KEEP_FOREGROUND_SERVICE, + R.bool.enable_foreground_service); } public static void removeUnusedPreferences(SettingsFragment settingsFragment) { - List categories = Arrays.asList( - (PreferenceCategory) settingsFragment.findPreference("notification_category"), - (PreferenceCategory) settingsFragment.findPreference("advanced")); - for (String key : (runsTwentySix() ? UNUSED_SETTINGS_POST_TWENTYSIX : UNUESD_SETTINGS_PRE_TWENTYSIX)) { + List categories = + Arrays.asList( + (PreferenceCategory) + settingsFragment.findPreference("notification_category"), + (PreferenceCategory) settingsFragment.findPreference("advanced")); + for (String key : + (runsTwentySix() + ? UNUSED_SETTINGS_POST_TWENTYSIX + : UNUESD_SETTINGS_PRE_TWENTYSIX)) { Preference preference = settingsFragment.findPreference(key); if (preference != null) { for (PreferenceCategory category : categories) { @@ -111,7 +127,8 @@ public static void removeUnusedPreferences(SettingsFragment settingsFragment) { } if (Compatibility.runsTwentySix()) { if (targetsTwentySix(settingsFragment.getContext())) { - Preference preference = settingsFragment.findPreference(SettingsActivity.KEEP_FOREGROUND_SERVICE); + Preference preference = + settingsFragment.findPreference(SettingsActivity.KEEP_FOREGROUND_SERVICE); if (preference != null) { for (PreferenceCategory category : categories) { if (category != null) { @@ -132,11 +149,12 @@ public static void startService(Context context, Intent intent) { context.startService(intent); } } catch (RuntimeException e) { - Log.d(Config.LOGTAG, context.getClass().getSimpleName() + " was unable to start service"); + Log.d( + Config.LOGTAG, + context.getClass().getSimpleName() + " was unable to start service"); } } - @SuppressLint("UnsupportedChromeOsCameraSystemFeature") public static boolean hasFeatureCamera(final Context context) { final PackageManager packageManager = context.getPackageManager(); From 52ff6f446ce2e7f304b3489df1cc4fbea0cf2f10 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 15 Jul 2022 09:31:43 +0200 Subject: [PATCH 136/394] add permission checks to appRTCBluetoothManager --- src/main/AndroidManifest.xml | 1 + .../services/AppRTCBluetoothManager.java | 317 ++++++++++-------- 2 files changed, 172 insertions(+), 146 deletions(-) diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 2a48da7a0..8cb375870 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ + diff --git a/src/main/java/eu/siacs/conversations/services/AppRTCBluetoothManager.java b/src/main/java/eu/siacs/conversations/services/AppRTCBluetoothManager.java index 862cdf0c7..484072605 100644 --- a/src/main/java/eu/siacs/conversations/services/AppRTCBluetoothManager.java +++ b/src/main/java/eu/siacs/conversations/services/AppRTCBluetoothManager.java @@ -9,6 +9,7 @@ */ package eu.siacs.conversations.services; +import android.Manifest; import android.annotation.SuppressLint; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; @@ -20,25 +21,25 @@ import android.content.IntentFilter; import android.content.pm.PackageManager; import android.media.AudioManager; +import android.os.Build; import android.os.Handler; import android.os.Looper; -import android.os.Process; import android.util.Log; import androidx.annotation.Nullable; +import androidx.core.app.ActivityCompat; + +import com.google.common.collect.ImmutableList; import org.webrtc.ThreadUtils; +import java.util.Collections; import java.util.List; -import java.util.Set; import eu.siacs.conversations.Config; import eu.siacs.conversations.utils.AppRTCUtils; -/** - * AppRTCProximitySensor manages functions related to Bluetoth devices in the - * AppRTC demo. - */ +/** AppRTCProximitySensor manages functions related to Bluetoth devices in the AppRTC demo. */ public class AppRTCBluetoothManager { // Timeout interval for starting or stopping audio to a Bluetooth SCO device. private static final int BLUETOOTH_SCO_TIMEOUT_MS = 4000; @@ -46,28 +47,26 @@ public class AppRTCBluetoothManager { private static final int MAX_SCO_CONNECTION_ATTEMPTS = 2; private final Context apprtcContext; private final AppRTCAudioManager apprtcAudioManager; - @Nullable - private final AudioManager audioManager; + @Nullable private final AudioManager audioManager; private final Handler handler; private final BluetoothProfile.ServiceListener bluetoothServiceListener; private final BroadcastReceiver bluetoothHeadsetReceiver; int scoConnectionAttempts; private State bluetoothState; - @Nullable - private BluetoothAdapter bluetoothAdapter; - @Nullable - private BluetoothHeadset bluetoothHeadset; - @Nullable - private BluetoothDevice bluetoothDevice; + @Nullable private BluetoothAdapter bluetoothAdapter; + @Nullable private BluetoothHeadset bluetoothHeadset; + @Nullable private BluetoothDevice bluetoothDevice; // Runs when the Bluetooth timeout expires. We use that timeout after calling // startScoAudio() or stopScoAudio() because we're not guaranteed to get a // callback after those calls. - private final Runnable bluetoothTimeoutRunnable = new Runnable() { - @Override - public void run() { - bluetoothTimeout(); - } - }; + private final Runnable bluetoothTimeoutRunnable = + new Runnable() { + @Override + public void run() { + bluetoothTimeout(); + } + }; + protected AppRTCBluetoothManager(Context context, AppRTCAudioManager audioManager) { Log.d(Config.LOGTAG, "ctor"); ThreadUtils.checkIsOnMainThread(); @@ -80,42 +79,29 @@ protected AppRTCBluetoothManager(Context context, AppRTCAudioManager audioManage handler = new Handler(Looper.getMainLooper()); } - /** - * Construction. - */ + /** Construction. */ static AppRTCBluetoothManager create(Context context, AppRTCAudioManager audioManager) { Log.d(Config.LOGTAG, "create" + AppRTCUtils.getThreadInfo()); return new AppRTCBluetoothManager(context, audioManager); } - /** - * Returns the internal state. - */ + /** Returns the internal state. */ public State getState() { ThreadUtils.checkIsOnMainThread(); return bluetoothState; } /** - * Activates components required to detect Bluetooth devices and to enable - * BT SCO (audio is routed via BT SCO) for the headset profile. The end - * state will be HEADSET_UNAVAILABLE but a state machine has started which - * will start a state change sequence where the final outcome depends on - * if/when the BT headset is enabled. - * Example of state change sequence when start() is called while BT device - * is connected and enabled: - * UNINITIALIZED --> HEADSET_UNAVAILABLE --> HEADSET_AVAILABLE --> - * SCO_CONNECTING --> SCO_CONNECTED <==> audio is now routed via BT SCO. - * Note that the AppRTCAudioManager is also involved in driving this state - * change. + * Activates components required to detect Bluetooth devices and to enable BT SCO (audio is + * routed via BT SCO) for the headset profile. The end state will be HEADSET_UNAVAILABLE but a + * state machine has started which will start a state change sequence where the final outcome + * depends on if/when the BT headset is enabled. Example of state change sequence when start() + * is called while BT device is connected and enabled: UNINITIALIZED --> HEADSET_UNAVAILABLE --> + * HEADSET_AVAILABLE --> SCO_CONNECTING --> SCO_CONNECTED <==> audio is now routed via BT SCO. + * Note that the AppRTCAudioManager is also involved in driving this state change. */ public void start() { ThreadUtils.checkIsOnMainThread(); - Log.d(Config.LOGTAG, "start"); - if (!hasPermission(apprtcContext, android.Manifest.permission.BLUETOOTH)) { - Log.w(Config.LOGTAG, "Process (pid=" + Process.myPid() + ") lacks BLUETOOTH permission"); - return; - } if (bluetoothState != State.UNINITIALIZED) { Log.w(Config.LOGTAG, "Invalid BT state"); return; @@ -130,11 +116,10 @@ public void start() { return; } // Ensure that the device supports use of BT SCO audio for off call use cases. - if (!audioManager.isBluetoothScoAvailableOffCall()) { + if (this.audioManager == null || !audioManager.isBluetoothScoAvailableOffCall()) { Log.e(Config.LOGTAG, "Bluetooth SCO audio is not available off call"); return; } - logBluetoothAdapterInfo(bluetoothAdapter); // Establish a connection to the HEADSET profile (includes both Bluetooth Headset and // Hands-Free) proxy object and install a listener. if (!getBluetoothProfileProxy( @@ -149,16 +134,20 @@ public void start() { // Register receiver for change in audio connection state of the Headset profile. bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED); registerReceiver(bluetoothHeadsetReceiver, bluetoothHeadsetFilter); - Log.d(Config.LOGTAG, "HEADSET profile state: " - + stateToString(bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET))); + if (hasBluetoothConnectPermission()) { + Log.d( + Config.LOGTAG, + "HEADSET profile state: " + + stateToString( + bluetoothAdapter.getProfileConnectionState( + BluetoothProfile.HEADSET))); + } Log.d(Config.LOGTAG, "Bluetooth proxy for headset profile has started"); bluetoothState = State.HEADSET_UNAVAILABLE; Log.d(Config.LOGTAG, "start done: BT state=" + bluetoothState); } - /** - * Stops and closes all components related to Bluetooth audio. - */ + /** Stops and closes all components related to Bluetooth audio. */ public void stop() { ThreadUtils.checkIsOnMainThread(); Log.d(Config.LOGTAG, "stop: BT state=" + bluetoothState); @@ -184,23 +173,29 @@ public void stop() { } /** - * Starts Bluetooth SCO connection with remote device. - * Note that the phone application always has the priority on the usage of the SCO connection - * for telephony. If this method is called while the phone is in call it will be ignored. - * Similarly, if a call is received or sent while an application is using the SCO connection, - * the connection will be lost for the application and NOT returned automatically when the call - * ends. Also note that: up to and including API version JELLY_BEAN_MR1, this method initiates a - * virtual voice call to the Bluetooth headset. After API version JELLY_BEAN_MR2 only a raw SCO - * audio connection is established. + * Starts Bluetooth SCO connection with remote device. Note that the phone application always + * has the priority on the usage of the SCO connection for telephony. If this method is called + * while the phone is in call it will be ignored. Similarly, if a call is received or sent while + * an application is using the SCO connection, the connection will be lost for the application + * and NOT returned automatically when the call ends. Also note that: up to and including API + * version JELLY_BEAN_MR1, this method initiates a virtual voice call to the Bluetooth headset. + * After API version JELLY_BEAN_MR2 only a raw SCO audio connection is established. * TODO(henrika): should we add support for virtual voice call to BT headset also for JBMR2 and * higher. It might be required to initiates a virtual voice call since many devices do not * accept SCO audio without a "call". */ public boolean startScoAudio() { ThreadUtils.checkIsOnMainThread(); - Log.d(Config.LOGTAG, "startSco: BT state=" + bluetoothState + ", " - + "attempts: " + scoConnectionAttempts + ", " - + "SCO is on: " + isScoOn()); + Log.d( + Config.LOGTAG, + "startSco: BT state=" + + bluetoothState + + ", " + + "attempts: " + + scoConnectionAttempts + + ", " + + "SCO is on: " + + isScoOn()); if (scoConnectionAttempts >= MAX_SCO_CONNECTION_ATTEMPTS) { Log.e(Config.LOGTAG, "BT SCO connection fails - no more attempts"); return false; @@ -213,24 +208,29 @@ public boolean startScoAudio() { Log.d(Config.LOGTAG, "Starting Bluetooth SCO and waits for ACTION_AUDIO_STATE_CHANGED..."); // The SCO connection establishment can take several seconds, hence we cannot rely on the // connection to be available when the method returns but instead register to receive the - // intent ACTION_SCO_AUDIO_STATE_UPDATED and wait for the state to be SCO_AUDIO_STATE_CONNECTED. + // intent ACTION_SCO_AUDIO_STATE_UPDATED and wait for the state to be + // SCO_AUDIO_STATE_CONNECTED. bluetoothState = State.SCO_CONNECTING; audioManager.startBluetoothSco(); audioManager.setBluetoothScoOn(true); scoConnectionAttempts++; startTimer(); - Log.d(Config.LOGTAG, "startScoAudio done: BT state=" + bluetoothState + ", " - + "SCO is on: " + isScoOn()); + Log.d( + Config.LOGTAG, + "startScoAudio done: BT state=" + + bluetoothState + + ", " + + "SCO is on: " + + isScoOn()); return true; } - /** - * Stops Bluetooth SCO connection with remote device. - */ + /** Stops Bluetooth SCO connection with remote device. */ public void stopScoAudio() { ThreadUtils.checkIsOnMainThread(); - Log.d(Config.LOGTAG, "stopScoAudio: BT state=" + bluetoothState + ", " - + "SCO is on: " + isScoOn()); + Log.d( + Config.LOGTAG, + "stopScoAudio: BT state=" + bluetoothState + ", " + "SCO is on: " + isScoOn()); if (bluetoothState != State.SCO_CONNECTING && bluetoothState != State.SCO_CONNECTED) { return; } @@ -238,17 +238,18 @@ public void stopScoAudio() { audioManager.stopBluetoothSco(); audioManager.setBluetoothScoOn(false); bluetoothState = State.SCO_DISCONNECTING; - Log.d(Config.LOGTAG, "stopScoAudio done: BT state=" + bluetoothState + ", " - + "SCO is on: " + isScoOn()); + Log.d( + Config.LOGTAG, + "stopScoAudio done: BT state=" + bluetoothState + ", " + "SCO is on: " + isScoOn()); } /** - * Use the BluetoothHeadset proxy object (controls the Bluetooth Headset - * Service via IPC) to update the list of connected devices for the HEADSET - * profile. The internal state will change to HEADSET_UNAVAILABLE or to - * HEADSET_AVAILABLE and |bluetoothDevice| will be mapped to the connected - * device if available. + * Use the BluetoothHeadset proxy object (controls the Bluetooth Headset Service via IPC) to + * update the list of connected devices for the HEADSET profile. The internal state will change + * to HEADSET_UNAVAILABLE or to HEADSET_AVAILABLE and |bluetoothDevice| will be mapped to the + * connected device if available. */ + @SuppressLint("MissingPermission") public void updateDevice() { if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) { return; @@ -257,7 +258,12 @@ public void updateDevice() { // Get connected devices for the headset profile. Returns the set of // devices which are in state STATE_CONNECTED. The BluetoothDevice class // is just a thin wrapper for a Bluetooth hardware address. - List devices = bluetoothHeadset.getConnectedDevices(); + final List devices; + if (hasBluetoothConnectPermission()) { + devices = bluetoothHeadset.getConnectedDevices(); + } else { + devices = ImmutableList.of(); + } if (devices.isEmpty()) { bluetoothDevice = null; bluetoothState = State.HEADSET_UNAVAILABLE; @@ -266,17 +272,21 @@ public void updateDevice() { // Always use first device in list. Android only supports one device. bluetoothDevice = devices.get(0); bluetoothState = State.HEADSET_AVAILABLE; - Log.d(Config.LOGTAG, "Connected bluetooth headset: " - + "name=" + bluetoothDevice.getName() + ", " - + "state=" + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice)) - + ", SCO audio=" + bluetoothHeadset.isAudioConnected(bluetoothDevice)); + Log.d( + Config.LOGTAG, + "Connected bluetooth headset: " + + "name=" + + bluetoothDevice.getName() + + ", " + + "state=" + + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice)) + + ", SCO audio=" + + bluetoothHeadset.isAudioConnected(bluetoothDevice)); } Log.d(Config.LOGTAG, "updateDevice done: BT state=" + bluetoothState); } - /** - * Stubs for test mocks. - */ + /** Stubs for test mocks. */ @Nullable protected AudioManager getAudioManager(Context context) { return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); @@ -295,52 +305,31 @@ protected boolean getBluetoothProfileProxy( return bluetoothAdapter.getProfileProxy(context, listener, profile); } - protected boolean hasPermission(Context context, String permission) { - return apprtcContext.checkPermission(permission, Process.myPid(), Process.myUid()) - == PackageManager.PERMISSION_GRANTED; - } - - /** - * Logs the state of the local Bluetooth adapter. - */ - @SuppressLint("HardwareIds") - protected void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) { - Log.d(Config.LOGTAG, "BluetoothAdapter: " - + "enabled=" + localAdapter.isEnabled() + ", " - + "state=" + stateToString(localAdapter.getState()) + ", " - + "name=" + localAdapter.getName() + ", " - + "address=" + localAdapter.getAddress()); - // Log the set of BluetoothDevice objects that are bonded (paired) to the local adapter. - Set pairedDevices = localAdapter.getBondedDevices(); - if (!pairedDevices.isEmpty()) { - Log.d(Config.LOGTAG, "paired devices:"); - for (BluetoothDevice device : pairedDevices) { - Log.d(Config.LOGTAG, " name=" + device.getName() + ", address=" + device.getAddress()); - } + protected boolean hasBluetoothConnectPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + return ActivityCompat.checkSelfPermission( + apprtcContext, Manifest.permission.BLUETOOTH_CONNECT) + == PackageManager.PERMISSION_GRANTED; + } else { + return true; } } - /** - * Ensures that the audio manager updates its list of available audio devices. - */ + /** Ensures that the audio manager updates its list of available audio devices. */ private void updateAudioDeviceState() { ThreadUtils.checkIsOnMainThread(); Log.d(Config.LOGTAG, "updateAudioDeviceState"); apprtcAudioManager.updateAudioDeviceState(); } - /** - * Starts timer which times out after BLUETOOTH_SCO_TIMEOUT_MS milliseconds. - */ + /** Starts timer which times out after BLUETOOTH_SCO_TIMEOUT_MS milliseconds. */ private void startTimer() { ThreadUtils.checkIsOnMainThread(); Log.d(Config.LOGTAG, "startTimer"); handler.postDelayed(bluetoothTimeoutRunnable, BLUETOOTH_SCO_TIMEOUT_MS); } - /** - * Cancels any outstanding timer tasks. - */ + /** Cancels any outstanding timer tasks. */ private void cancelTimer() { ThreadUtils.checkIsOnMainThread(); Log.d(Config.LOGTAG, "cancelTimer"); @@ -348,23 +337,36 @@ private void cancelTimer() { } /** - * Called when start of the BT SCO channel takes too long time. Usually - * happens when the BT device has been turned on during an ongoing call. + * Called when start of the BT SCO channel takes too long time. Usually happens when the BT + * device has been turned on during an ongoing call. */ + @SuppressLint("MissingPermission") private void bluetoothTimeout() { ThreadUtils.checkIsOnMainThread(); if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) { return; } - Log.d(Config.LOGTAG, "bluetoothTimeout: BT state=" + bluetoothState + ", " - + "attempts: " + scoConnectionAttempts + ", " - + "SCO is on: " + isScoOn()); + Log.d( + Config.LOGTAG, + "bluetoothTimeout: BT state=" + + bluetoothState + + ", " + + "attempts: " + + scoConnectionAttempts + + ", " + + "SCO is on: " + + isScoOn()); if (bluetoothState != State.SCO_CONNECTING) { return; } // Bluetooth SCO should be connecting; check the latest result. boolean scoConnected = false; - List devices = bluetoothHeadset.getConnectedDevices(); + final List devices; + if (hasBluetoothConnectPermission()) { + devices = bluetoothHeadset.getConnectedDevices(); + } else { + devices = Collections.emptyList(); + } if (devices.size() > 0) { bluetoothDevice = devices.get(0); if (bluetoothHeadset.isAudioConnected(bluetoothDevice)) { @@ -387,16 +389,12 @@ private void bluetoothTimeout() { Log.d(Config.LOGTAG, "bluetoothTimeout done: BT state=" + bluetoothState); } - /** - * Checks whether audio uses Bluetooth SCO. - */ + /** Checks whether audio uses Bluetooth SCO. */ private boolean isScoOn() { return audioManager.isBluetoothScoOn(); } - /** - * Converts BluetoothAdapter states into local string representations. - */ + /** Converts BluetoothAdapter states into local string representations. */ private String stateToString(int state) { switch (state) { case BluetoothAdapter.STATE_DISCONNECTED: @@ -412,11 +410,13 @@ private String stateToString(int state) { case BluetoothAdapter.STATE_ON: return "ON"; case BluetoothAdapter.STATE_TURNING_OFF: - // Indicates the local Bluetooth adapter is turning off. Local clients should immediately + // Indicates the local Bluetooth adapter is turning off. Local clients should + // immediately // attempt graceful disconnection of any remote links. return "TURNING_OFF"; case BluetoothAdapter.STATE_TURNING_ON: - // Indicates the local Bluetooth adapter is turning on. However local clients should wait + // Indicates the local Bluetooth adapter is turning on. However local clients should + // wait // for STATE_ON before attempting to use the adapter. return "TURNING_ON"; default: @@ -457,7 +457,9 @@ public void onServiceConnected(int profile, BluetoothProfile proxy) { if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) { return; } - Log.d(Config.LOGTAG, "BluetoothServiceListener.onServiceConnected: BT state=" + bluetoothState); + Log.d( + Config.LOGTAG, + "BluetoothServiceListener.onServiceConnected: BT state=" + bluetoothState); // Android only supports one connected Bluetooth Headset at a time. bluetoothHeadset = (BluetoothHeadset) proxy; updateAudioDeviceState(); @@ -470,7 +472,9 @@ public void onServiceDisconnected(int profile) { if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) { return; } - Log.d(Config.LOGTAG, "BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState); + Log.d( + Config.LOGTAG, + "BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState); stopScoAudio(); bluetoothHeadset = null; bluetoothDevice = null; @@ -495,12 +499,20 @@ public void onReceive(Context context, Intent intent) { // headset while audio is active using another audio device. if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) { final int state = - intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_DISCONNECTED); - Log.d(Config.LOGTAG, "BluetoothHeadsetBroadcastReceiver.onReceive: " - + "a=ACTION_CONNECTION_STATE_CHANGED, " - + "s=" + stateToString(state) + ", " - + "sb=" + isInitialStickyBroadcast() + ", " - + "BT state: " + bluetoothState); + intent.getIntExtra( + BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_DISCONNECTED); + Log.d( + Config.LOGTAG, + "BluetoothHeadsetBroadcastReceiver.onReceive: " + + "a=ACTION_CONNECTION_STATE_CHANGED, " + + "s=" + + stateToString(state) + + ", " + + "sb=" + + isInitialStickyBroadcast() + + ", " + + "BT state: " + + bluetoothState); if (state == BluetoothHeadset.STATE_CONNECTED) { scoConnectionAttempts = 0; updateAudioDeviceState(); @@ -516,13 +528,22 @@ public void onReceive(Context context, Intent intent) { // Change in the audio (SCO) connection state of the Headset profile. // Typically received after call to startScoAudio() has finalized. } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) { - final int state = intent.getIntExtra( - BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_AUDIO_DISCONNECTED); - Log.d(Config.LOGTAG, "BluetoothHeadsetBroadcastReceiver.onReceive: " - + "a=ACTION_AUDIO_STATE_CHANGED, " - + "s=" + stateToString(state) + ", " - + "sb=" + isInitialStickyBroadcast() + ", " - + "BT state: " + bluetoothState); + final int state = + intent.getIntExtra( + BluetoothHeadset.EXTRA_STATE, + BluetoothHeadset.STATE_AUDIO_DISCONNECTED); + Log.d( + Config.LOGTAG, + "BluetoothHeadsetBroadcastReceiver.onReceive: " + + "a=ACTION_AUDIO_STATE_CHANGED, " + + "s=" + + stateToString(state) + + ", " + + "sb=" + + isInitialStickyBroadcast() + + ", " + + "BT state: " + + bluetoothState); if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) { cancelTimer(); if (bluetoothState == State.SCO_CONNECTING) { @@ -531,14 +552,18 @@ public void onReceive(Context context, Intent intent) { scoConnectionAttempts = 0; updateAudioDeviceState(); } else { - Log.w(Config.LOGTAG, "Unexpected state BluetoothHeadset.STATE_AUDIO_CONNECTED"); + Log.w( + Config.LOGTAG, + "Unexpected state BluetoothHeadset.STATE_AUDIO_CONNECTED"); } } else if (state == BluetoothHeadset.STATE_AUDIO_CONNECTING) { Log.d(Config.LOGTAG, "+++ Bluetooth audio SCO is now connecting..."); } else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) { Log.d(Config.LOGTAG, "+++ Bluetooth audio SCO is now disconnected"); if (isInitialStickyBroadcast()) { - Log.d(Config.LOGTAG, "Ignore STATE_AUDIO_DISCONNECTED initial sticky broadcast."); + Log.d( + Config.LOGTAG, + "Ignore STATE_AUDIO_DISCONNECTED initial sticky broadcast."); return; } updateAudioDeviceState(); @@ -547,4 +572,4 @@ public void onReceive(Context context, Intent intent) { Log.d(Config.LOGTAG, "onReceive done: BT state=" + bluetoothState); } } -} \ No newline at end of file +} From 5aeed638444dad5d710350900e159e0dca9f5503 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 8 Aug 2022 21:08:28 +0200 Subject: [PATCH 137/394] request bluetooth connect permission fixes #4338 --- .../ui/ConversationFragment.java | 1962 +++++++++++------ .../conversations/ui/RtpSessionActivity.java | 19 +- .../conversations/utils/PermissionUtils.java | 39 +- 3 files changed, 1295 insertions(+), 725 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 2eb602257..0471c014f 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1,5 +1,12 @@ package eu.siacs.conversations.ui; +import static eu.siacs.conversations.ui.XmppActivity.EXTRA_ACCOUNT; +import static eu.siacs.conversations.ui.XmppActivity.REQUEST_INVITE_TO_CONVERSATION; +import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.hideSoftKeyboard; +import static eu.siacs.conversations.utils.PermissionUtils.allGranted; +import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied; +import static eu.siacs.conversations.utils.PermissionUtils.writeGranted; + import android.Manifest; import android.annotation.SuppressLint; import android.app.Activity; @@ -55,6 +62,9 @@ import androidx.databinding.DataBindingUtil; import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; + +import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.Arrays; @@ -114,6 +124,7 @@ import eu.siacs.conversations.utils.MessageUtils; import eu.siacs.conversations.utils.NickValidityChecker; import eu.siacs.conversations.utils.Patterns; +import eu.siacs.conversations.utils.PermissionUtils; import eu.siacs.conversations.utils.QuickLoader; import eu.siacs.conversations.utils.StylingHelper; import eu.siacs.conversations.utils.TimeFrameUtils; @@ -129,18 +140,10 @@ import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession; import eu.siacs.conversations.xmpp.jingle.RtpCapability; -import static eu.siacs.conversations.ui.XmppActivity.EXTRA_ACCOUNT; -import static eu.siacs.conversations.ui.XmppActivity.REQUEST_INVITE_TO_CONVERSATION; -import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.hideSoftKeyboard; -import static eu.siacs.conversations.utils.PermissionUtils.allGranted; -import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied; -import static eu.siacs.conversations.utils.PermissionUtils.writeGranted; - -import org.jetbrains.annotations.NotNull; - - -public class ConversationFragment extends XmppFragment implements EditMessage.KeyboardListener, MessageAdapter.OnContactPictureLongClicked, MessageAdapter.OnContactPictureClicked { - +public class ConversationFragment extends XmppFragment + implements EditMessage.KeyboardListener, + MessageAdapter.OnContactPictureLongClicked, + MessageAdapter.OnContactPictureClicked { public static final int REQUEST_SEND_MESSAGE = 0x0201; public static final int REQUEST_DECRYPT_PGP = 0x0202; @@ -161,10 +164,14 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke public static final int ATTACHMENT_CHOICE_RECORD_VIDEO = 0x0307; public static final String RECENTLY_USED_QUICK_ACTION = "recently_used_quick_action"; - public static final String STATE_CONVERSATION_UUID = ConversationFragment.class.getName() + ".uuid"; - public static final String STATE_SCROLL_POSITION = ConversationFragment.class.getName() + ".scroll_position"; - public static final String STATE_PHOTO_URI = ConversationFragment.class.getName() + ".media_previews"; - public static final String STATE_MEDIA_PREVIEWS = ConversationFragment.class.getName() + ".take_photo_uri"; + public static final String STATE_CONVERSATION_UUID = + ConversationFragment.class.getName() + ".uuid"; + public static final String STATE_SCROLL_POSITION = + ConversationFragment.class.getName() + ".scroll_position"; + public static final String STATE_PHOTO_URI = + ConversationFragment.class.getName() + ".media_previews"; + public static final String STATE_MEDIA_PREVIEWS = + ConversationFragment.class.getName() + ".take_photo_uri"; private static final String STATE_LAST_MESSAGE_UUID = "state_last_message_uuid"; private final List messageList = new ArrayList<>(); @@ -185,282 +192,376 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke private Toast messageLoaderToast; private ConversationsActivity activity; private boolean reInitRequiredOnStart = true; - private final OnClickListener clickToMuc = new OnClickListener() { + private final OnClickListener clickToMuc = + new OnClickListener() { - @Override - public void onClick(View v) { - ConferenceDetailsActivity.open(getActivity(), conversation); - } - }; - private final OnClickListener leaveMuc = new OnClickListener() { - - @Override - public void onClick(View v) { - activity.xmppConnectionService.archiveConversation(conversation); - } - }; - private final OnClickListener joinMuc = new OnClickListener() { - - @Override - public void onClick(View v) { - activity.xmppConnectionService.joinMuc(conversation); - } - }; - - private final OnClickListener acceptJoin = new OnClickListener() { - @Override - public void onClick(View v) { - conversation.setAttribute("accept_non_anonymous", true); - activity.xmppConnectionService.updateConversation(conversation); - activity.xmppConnectionService.joinMuc(conversation); - } - }; + @Override + public void onClick(View v) { + ConferenceDetailsActivity.open(getActivity(), conversation); + } + }; + private final OnClickListener leaveMuc = + new OnClickListener() { - private final OnClickListener enterPassword = new OnClickListener() { + @Override + public void onClick(View v) { + activity.xmppConnectionService.archiveConversation(conversation); + } + }; + private final OnClickListener joinMuc = + new OnClickListener() { - @Override - public void onClick(View v) { - MucOptions muc = conversation.getMucOptions(); - String password = muc.getPassword(); - if (password == null) { - password = ""; - } - activity.quickPasswordEdit(password, value -> { - activity.xmppConnectionService.providePasswordForMuc(conversation, value); - return null; - }); - } - }; - private final OnScrollListener mOnScrollListener = new OnScrollListener() { + @Override + public void onClick(View v) { + activity.xmppConnectionService.joinMuc(conversation); + } + }; + + private final OnClickListener acceptJoin = + new OnClickListener() { + @Override + public void onClick(View v) { + conversation.setAttribute("accept_non_anonymous", true); + activity.xmppConnectionService.updateConversation(conversation); + activity.xmppConnectionService.joinMuc(conversation); + } + }; - @Override - public void onScrollStateChanged(AbsListView view, int scrollState) { - if (AbsListView.OnScrollListener.SCROLL_STATE_IDLE == scrollState) { - fireReadEvent(); - } - } + private final OnClickListener enterPassword = + new OnClickListener() { - @Override - public void onScroll(final AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { - toggleScrollDownButton(view); - synchronized (ConversationFragment.this.messageList) { - if (firstVisibleItem < 5 && conversation != null && conversation.messagesLoaded.compareAndSet(true, false) && messageList.size() > 0) { - long timestamp; - if (messageList.get(0).getType() == Message.TYPE_STATUS && messageList.size() >= 2) { - timestamp = messageList.get(1).getTimeSent(); - } else { - timestamp = messageList.get(0).getTimeSent(); + @Override + public void onClick(View v) { + MucOptions muc = conversation.getMucOptions(); + String password = muc.getPassword(); + if (password == null) { + password = ""; } - activity.xmppConnectionService.loadMoreMessages(conversation, timestamp, new XmppConnectionService.OnMoreMessagesLoaded() { - @Override - public void onMoreMessagesLoaded(final int c, final Conversation conversation) { - if (ConversationFragment.this.conversation != conversation) { - conversation.messagesLoaded.set(true); - return; - } - runOnUiThread(() -> { - synchronized (messageList) { - final int oldPosition = binding.messagesView.getFirstVisiblePosition(); - Message message = null; - int childPos; - for (childPos = 0; childPos + oldPosition < messageList.size(); ++childPos) { - message = messageList.get(oldPosition + childPos); - if (message.getType() != Message.TYPE_STATUS) { - break; - } - } - final String uuid = message != null ? message.getUuid() : null; - View v = binding.messagesView.getChildAt(childPos); - final int pxOffset = (v == null) ? 0 : v.getTop(); - ConversationFragment.this.conversation.populateWithMessages(ConversationFragment.this.messageList); - try { - updateStatusMessages(); - } catch (IllegalStateException e) { - Log.d(Config.LOGTAG, "caught illegal state exception while updating status messages"); - } - messageListAdapter.notifyDataSetChanged(); - int pos = Math.max(getIndexOf(uuid, messageList), 0); - binding.messagesView.setSelectionFromTop(pos, pxOffset); - if (messageLoaderToast != null) { - messageLoaderToast.cancel(); - } - conversation.messagesLoaded.set(true); - } + activity.quickPasswordEdit( + password, + value -> { + activity.xmppConnectionService.providePasswordForMuc( + conversation, value); + return null; }); - } - - @Override - public void informUser(final int resId) { + } + }; + private final OnScrollListener mOnScrollListener = + new OnScrollListener() { + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + if (AbsListView.OnScrollListener.SCROLL_STATE_IDLE == scrollState) { + fireReadEvent(); + } + } - runOnUiThread(() -> { - if (messageLoaderToast != null) { - messageLoaderToast.cancel(); - } - if (ConversationFragment.this.conversation != conversation) { - return; - } - messageLoaderToast = Toast.makeText(view.getContext(), resId, Toast.LENGTH_LONG); - messageLoaderToast.show(); - }); + @Override + public void onScroll( + final AbsListView view, + int firstVisibleItem, + int visibleItemCount, + int totalItemCount) { + toggleScrollDownButton(view); + synchronized (ConversationFragment.this.messageList) { + if (firstVisibleItem < 5 + && conversation != null + && conversation.messagesLoaded.compareAndSet(true, false) + && messageList.size() > 0) { + long timestamp; + if (messageList.get(0).getType() == Message.TYPE_STATUS + && messageList.size() >= 2) { + timestamp = messageList.get(1).getTimeSent(); + } else { + timestamp = messageList.get(0).getTimeSent(); + } + activity.xmppConnectionService.loadMoreMessages( + conversation, + timestamp, + new XmppConnectionService.OnMoreMessagesLoaded() { + @Override + public void onMoreMessagesLoaded( + final int c, final Conversation conversation) { + if (ConversationFragment.this.conversation + != conversation) { + conversation.messagesLoaded.set(true); + return; + } + runOnUiThread( + () -> { + synchronized (messageList) { + final int oldPosition = + binding.messagesView + .getFirstVisiblePosition(); + Message message = null; + int childPos; + for (childPos = 0; + childPos + oldPosition + < messageList.size(); + ++childPos) { + message = + messageList.get( + oldPosition + + childPos); + if (message.getType() + != Message.TYPE_STATUS) { + break; + } + } + final String uuid = + message != null + ? message.getUuid() + : null; + View v = + binding.messagesView.getChildAt( + childPos); + final int pxOffset = + (v == null) ? 0 : v.getTop(); + ConversationFragment.this.conversation + .populateWithMessages( + ConversationFragment + .this + .messageList); + try { + updateStatusMessages(); + } catch (IllegalStateException e) { + Log.d( + Config.LOGTAG, + "caught illegal state exception while updating status messages"); + } + messageListAdapter + .notifyDataSetChanged(); + int pos = + Math.max( + getIndexOf( + uuid, + messageList), + 0); + binding.messagesView + .setSelectionFromTop( + pos, pxOffset); + if (messageLoaderToast != null) { + messageLoaderToast.cancel(); + } + conversation.messagesLoaded.set(true); + } + }); + } + @Override + public void informUser(final int resId) { + + runOnUiThread( + () -> { + if (messageLoaderToast != null) { + messageLoaderToast.cancel(); + } + if (ConversationFragment.this.conversation + != conversation) { + return; + } + messageLoaderToast = + Toast.makeText( + view.getContext(), + resId, + Toast.LENGTH_LONG); + messageLoaderToast.show(); + }); + } + }); } - }); - + } } - } - } - }; - private final EditMessage.OnCommitContentListener mEditorContentListener = new EditMessage.OnCommitContentListener() { - @Override - public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts, String[] contentMimeTypes) { - // try to get permission to read the image, if applicable - if ((flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) { - try { - inputContentInfo.requestPermission(); - } catch (Exception e) { - Log.e(Config.LOGTAG, "InputContentInfoCompat#requestPermission() failed.", e); - Toast.makeText(getActivity(), activity.getString(R.string.no_permission_to_access_x, inputContentInfo.getDescription()), Toast.LENGTH_LONG - ).show(); - return false; + }; + private final EditMessage.OnCommitContentListener mEditorContentListener = + new EditMessage.OnCommitContentListener() { + @Override + public boolean onCommitContent( + InputContentInfoCompat inputContentInfo, + int flags, + Bundle opts, + String[] contentMimeTypes) { + // try to get permission to read the image, if applicable + if ((flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) + != 0) { + try { + inputContentInfo.requestPermission(); + } catch (Exception e) { + Log.e( + Config.LOGTAG, + "InputContentInfoCompat#requestPermission() failed.", + e); + Toast.makeText( + getActivity(), + activity.getString( + R.string.no_permission_to_access_x, + inputContentInfo.getDescription()), + Toast.LENGTH_LONG) + .show(); + return false; + } + } + if (hasPermissions( + REQUEST_ADD_EDITOR_CONTENT, + Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + attachEditorContentToConversation(inputContentInfo.getContentUri()); + } else { + mPendingEditorContent = inputContentInfo.getContentUri(); + } + return true; } - } - if (hasPermissions(REQUEST_ADD_EDITOR_CONTENT, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - attachEditorContentToConversation(inputContentInfo.getContentUri()); - } else { - mPendingEditorContent = inputContentInfo.getContentUri(); - } - return true; - } - }; + }; private Message selectedMessage; - private final OnClickListener mEnableAccountListener = new OnClickListener() { - @Override - public void onClick(View v) { - final Account account = conversation == null ? null : conversation.getAccount(); - if (account != null) { - account.setOption(Account.OPTION_DISABLED, false); - activity.xmppConnectionService.updateAccount(account); - } - } - }; - private final OnClickListener mUnblockClickListener = new OnClickListener() { - @Override - public void onClick(final View v) { - v.post(() -> v.setVisibility(View.INVISIBLE)); - if (conversation.isDomainBlocked()) { - BlockContactDialog.show(activity, conversation); - } else { - unblockConversation(conversation); - } - } - }; + private final OnClickListener mEnableAccountListener = + new OnClickListener() { + @Override + public void onClick(View v) { + final Account account = conversation == null ? null : conversation.getAccount(); + if (account != null) { + account.setOption(Account.OPTION_DISABLED, false); + activity.xmppConnectionService.updateAccount(account); + } + } + }; + private final OnClickListener mUnblockClickListener = + new OnClickListener() { + @Override + public void onClick(final View v) { + v.post(() -> v.setVisibility(View.INVISIBLE)); + if (conversation.isDomainBlocked()) { + BlockContactDialog.show(activity, conversation); + } else { + unblockConversation(conversation); + } + } + }; private final OnClickListener mBlockClickListener = this::showBlockSubmenu; - private final OnClickListener mAddBackClickListener = new OnClickListener() { - - @Override - public void onClick(View v) { - final Contact contact = conversation == null ? null : conversation.getContact(); - if (contact != null) { - activity.xmppConnectionService.createContact(contact, true); - activity.switchToContactDetails(contact); - } - } - }; + private final OnClickListener mAddBackClickListener = + new OnClickListener() { + + @Override + public void onClick(View v) { + final Contact contact = conversation == null ? null : conversation.getContact(); + if (contact != null) { + activity.xmppConnectionService.createContact(contact, true); + activity.switchToContactDetails(contact); + } + } + }; private final View.OnLongClickListener mLongPressBlockListener = this::showBlockSubmenu; - private final OnClickListener mAllowPresenceSubscription = new OnClickListener() { - @Override - public void onClick(View v) { - final Contact contact = conversation == null ? null : conversation.getContact(); - if (contact != null) { - activity.xmppConnectionService.sendPresencePacket(contact.getAccount(), - activity.xmppConnectionService.getPresenceGenerator() - .sendPresenceUpdatesTo(contact)); - hideSnackbar(); - } - } - }; - protected OnClickListener clickToDecryptListener = new OnClickListener() { - - @Override - public void onClick(View v) { - PendingIntent pendingIntent = conversation.getAccount().getPgpDecryptionService().getPendingIntent(); - if (pendingIntent != null) { - try { - getActivity().startIntentSenderForResult(pendingIntent.getIntentSender(), - REQUEST_DECRYPT_PGP, - null, - 0, - 0, - 0); - } catch (SendIntentException e) { - Toast.makeText(getActivity(), R.string.unable_to_connect_to_keychain, Toast.LENGTH_SHORT).show(); - conversation.getAccount().getPgpDecryptionService().continueDecryption(true); + private final OnClickListener mAllowPresenceSubscription = + new OnClickListener() { + @Override + public void onClick(View v) { + final Contact contact = conversation == null ? null : conversation.getContact(); + if (contact != null) { + activity.xmppConnectionService.sendPresencePacket( + contact.getAccount(), + activity.xmppConnectionService + .getPresenceGenerator() + .sendPresenceUpdatesTo(contact)); + hideSnackbar(); + } } - } - updateSnackBar(conversation); - } - }; + }; + protected OnClickListener clickToDecryptListener = + new OnClickListener() { + + @Override + public void onClick(View v) { + PendingIntent pendingIntent = + conversation.getAccount().getPgpDecryptionService().getPendingIntent(); + if (pendingIntent != null) { + try { + getActivity() + .startIntentSenderForResult( + pendingIntent.getIntentSender(), + REQUEST_DECRYPT_PGP, + null, + 0, + 0, + 0); + } catch (SendIntentException e) { + Toast.makeText( + getActivity(), + R.string.unable_to_connect_to_keychain, + Toast.LENGTH_SHORT) + .show(); + conversation + .getAccount() + .getPgpDecryptionService() + .continueDecryption(true); + } + } + updateSnackBar(conversation); + } + }; private final AtomicBoolean mSendingPgpMessage = new AtomicBoolean(false); - private final OnEditorActionListener mEditorActionListener = (v, actionId, event) -> { - if (actionId == EditorInfo.IME_ACTION_SEND) { - InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); - if (imm != null && imm.isFullscreenMode()) { - imm.hideSoftInputFromWindow(v.getWindowToken(), 0); - } - sendMessage(); - return true; - } else { - return false; - } - }; - private final OnClickListener mScrollButtonListener = new OnClickListener() { - - @Override - public void onClick(View v) { - stopScrolling(); - setSelection(binding.messagesView.getCount() - 1, true); - } - }; - private final OnClickListener mSendButtonListener = new OnClickListener() { - - @Override - public void onClick(View v) { - Object tag = v.getTag(); - if (tag instanceof SendButtonAction) { - SendButtonAction action = (SendButtonAction) tag; - switch (action) { - case TAKE_PHOTO: - case RECORD_VIDEO: - case SEND_LOCATION: - case RECORD_VOICE: - case CHOOSE_PICTURE: - attachFile(action.toChoice()); - break; - case CANCEL: - if (conversation != null) { - if (conversation.setCorrectingMessage(null)) { - binding.textinput.setText(""); - binding.textinput.append(conversation.getDraftMessage()); - conversation.setDraftMessage(null); - } else if (conversation.getMode() == Conversation.MODE_MULTI) { - conversation.setNextCounterpart(null); - binding.textinput.setText(""); - } else { - binding.textinput.setText(""); - } - updateChatMsgHint(); - updateSendButton(); - updateEditablity(); + private final OnEditorActionListener mEditorActionListener = + (v, actionId, event) -> { + if (actionId == EditorInfo.IME_ACTION_SEND) { + InputMethodManager imm = + (InputMethodManager) + activity.getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null && imm.isFullscreenMode()) { + imm.hideSoftInputFromWindow(v.getWindowToken(), 0); + } + sendMessage(); + return true; + } else { + return false; + } + }; + private final OnClickListener mScrollButtonListener = + new OnClickListener() { + + @Override + public void onClick(View v) { + stopScrolling(); + setSelection(binding.messagesView.getCount() - 1, true); + } + }; + private final OnClickListener mSendButtonListener = + new OnClickListener() { + + @Override + public void onClick(View v) { + Object tag = v.getTag(); + if (tag instanceof SendButtonAction) { + SendButtonAction action = (SendButtonAction) tag; + switch (action) { + case TAKE_PHOTO: + case RECORD_VIDEO: + case SEND_LOCATION: + case RECORD_VOICE: + case CHOOSE_PICTURE: + attachFile(action.toChoice()); + break; + case CANCEL: + if (conversation != null) { + if (conversation.setCorrectingMessage(null)) { + binding.textinput.setText(""); + binding.textinput.append(conversation.getDraftMessage()); + conversation.setDraftMessage(null); + } else if (conversation.getMode() == Conversation.MODE_MULTI) { + conversation.setNextCounterpart(null); + binding.textinput.setText(""); + } else { + binding.textinput.setText(""); + } + updateChatMsgHint(); + updateSendButton(); + updateEditablity(); + } + break; + default: + sendMessage(); } - break; - default: + } else { sendMessage(); + } } - } else { - sendMessage(); - } - } - }; + }; private int completionIndex = 0; private int lastCompletionLength = 0; private String incomplete; @@ -531,7 +632,9 @@ public static ConversationFragment get(Activity activity) { return (ConversationFragment) fragment; } else { fragment = fragmentManager.findFragmentById(R.id.secondary_fragment); - return fragment instanceof ConversationFragment ? (ConversationFragment) fragment : null; + return fragment instanceof ConversationFragment + ? (ConversationFragment) fragment + : null; } } @@ -593,7 +696,6 @@ private int getIndexOf(String uuid, List messages) { } next = next.next(); } - } } return -1; @@ -601,7 +703,9 @@ private int getIndexOf(String uuid, List messages) { private ScrollState getScrollPosition() { final ListView listView = this.binding == null ? null : this.binding.messagesView; - if (listView == null || listView.getCount() == 0 || listView.getLastVisiblePosition() == listView.getCount() - 1) { + if (listView == null + || listView.getCount() == 0 + || listView.getLastVisiblePosition() == listView.getCount() - 1) { return null; } else { final int pos = listView.getFirstVisiblePosition(); @@ -619,10 +723,12 @@ private void setScrollPosition(ScrollState scrollPosition, String lastMessageUui this.lastMessageUuid = lastMessageUuid; if (lastMessageUuid != null) { - binding.unreadCountCustomView.setUnreadCount(conversation.getReceivedMessagesCountSinceUuid(lastMessageUuid)); + binding.unreadCountCustomView.setUnreadCount( + conversation.getReceivedMessagesCountSinceUuid(lastMessageUuid)); } - //TODO maybe this needs a 'post' - this.binding.messagesView.setSelectionFromTop(scrollPosition.position, scrollPosition.offset); + // TODO maybe this needs a 'post' + this.binding.messagesView.setSelectionFromTop( + scrollPosition.position, scrollPosition.offset); toggleScrollDownButton(); } } @@ -631,61 +737,65 @@ private void attachLocationToConversation(Conversation conversation, Uri uri) { if (conversation == null) { return; } - activity.xmppConnectionService.attachLocationToConversation(conversation, uri, new UiCallback() { - - @Override - public void success(Message message) { - - } + activity.xmppConnectionService.attachLocationToConversation( + conversation, + uri, + new UiCallback() { - @Override - public void error(int errorCode, Message object) { - //TODO show possible pgp error - } + @Override + public void success(Message message) {} - @Override - public void userInputRequired(PendingIntent pi, Message object) { + @Override + public void error(int errorCode, Message object) { + // TODO show possible pgp error + } - } - }); + @Override + public void userInputRequired(PendingIntent pi, Message object) {} + }); } private void attachFileToConversation(Conversation conversation, Uri uri, String type) { if (conversation == null) { return; } - final Toast prepareFileToast = Toast.makeText(getActivity(), getText(R.string.preparing_file), Toast.LENGTH_LONG); + final Toast prepareFileToast = + Toast.makeText(getActivity(), getText(R.string.preparing_file), Toast.LENGTH_LONG); prepareFileToast.show(); activity.delegateUriPermissionsToService(uri); - activity.xmppConnectionService.attachFileToConversation(conversation, uri, type, new UiInformableCallback() { - @Override - public void inform(final String text) { - hidePrepareFileToast(prepareFileToast); - runOnUiThread(() -> activity.replaceToast(text)); - } - - @Override - public void success(Message message) { - runOnUiThread(() -> activity.hideToast()); - hidePrepareFileToast(prepareFileToast); - } + activity.xmppConnectionService.attachFileToConversation( + conversation, + uri, + type, + new UiInformableCallback() { + @Override + public void inform(final String text) { + hidePrepareFileToast(prepareFileToast); + runOnUiThread(() -> activity.replaceToast(text)); + } - @Override - public void error(final int errorCode, Message message) { - hidePrepareFileToast(prepareFileToast); - runOnUiThread(() -> activity.replaceToast(getString(errorCode))); + @Override + public void success(Message message) { + runOnUiThread(() -> activity.hideToast()); + hidePrepareFileToast(prepareFileToast); + } - } + @Override + public void error(final int errorCode, Message message) { + hidePrepareFileToast(prepareFileToast); + runOnUiThread(() -> activity.replaceToast(getString(errorCode))); + } - @Override - public void userInputRequired(PendingIntent pi, Message message) { - hidePrepareFileToast(prepareFileToast); - } - }); + @Override + public void userInputRequired(PendingIntent pi, Message message) { + hidePrepareFileToast(prepareFileToast); + } + }); } public void attachEditorContentToConversation(Uri uri) { - mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), uri, Attachment.Type.FILE)); + mediaPreviewAdapter.addMediaPreviews( + Attachment.of(getActivity(), uri, Attachment.Type.FILE)); toggleInputMethod(); } @@ -693,10 +803,14 @@ private void attachImageToConversation(Conversation conversation, Uri uri, Strin if (conversation == null) { return; } - final Toast prepareFileToast = Toast.makeText(getActivity(), getText(R.string.preparing_image), Toast.LENGTH_LONG); + final Toast prepareFileToast = + Toast.makeText(getActivity(), getText(R.string.preparing_image), Toast.LENGTH_LONG); prepareFileToast.show(); activity.delegateUriPermissionsToService(uri); - activity.xmppConnectionService.attachImageToConversation(conversation, uri, type, + activity.xmppConnectionService.attachImageToConversation( + conversation, + uri, + type, new UiCallback() { @Override @@ -762,19 +876,31 @@ private void sendMessage() { } private boolean trustKeysIfNeeded(final Conversation conversation, final int requestCode) { - return conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL && trustKeysIfNeeded(requestCode); + return conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL + && trustKeysIfNeeded(requestCode); } protected boolean trustKeysIfNeeded(int requestCode) { AxolotlService axolotlService = conversation.getAccount().getAxolotlService(); final List targets = axolotlService.getCryptoTargets(conversation); boolean hasUnaccepted = !conversation.getAcceptedCryptoTargets().containsAll(targets); - boolean hasUndecidedOwn = !axolotlService.getKeysWithTrust(FingerprintStatus.createActiveUndecided()).isEmpty(); - boolean hasUndecidedContacts = !axolotlService.getKeysWithTrust(FingerprintStatus.createActiveUndecided(), targets).isEmpty(); + boolean hasUndecidedOwn = + !axolotlService + .getKeysWithTrust(FingerprintStatus.createActiveUndecided()) + .isEmpty(); + boolean hasUndecidedContacts = + !axolotlService + .getKeysWithTrust(FingerprintStatus.createActiveUndecided(), targets) + .isEmpty(); boolean hasPendingKeys = !axolotlService.findDevicesWithoutSession(conversation).isEmpty(); boolean hasNoTrustedKeys = axolotlService.anyTargetHasNoTrustedKeys(targets); boolean downloadInProgress = axolotlService.hasPendingKeyFetches(targets); - if (hasUndecidedOwn || hasUndecidedContacts || hasPendingKeys || hasNoTrustedKeys || hasUnaccepted || downloadInProgress) { + if (hasUndecidedOwn + || hasUndecidedContacts + || hasPendingKeys + || hasNoTrustedKeys + || hasUnaccepted + || downloadInProgress) { axolotlService.createSessionsIfNeeded(conversation); Intent intent = new Intent(getActivity(), TrustKeysActivity.class); String[] contacts = new String[targets.size()]; @@ -782,7 +908,9 @@ protected boolean trustKeysIfNeeded(int requestCode) { contacts[i] = targets.get(i).toString(); } intent.putExtra("contacts", contacts); - intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toEscapedString()); + intent.putExtra( + EXTRA_ACCOUNT, + conversation.getAccount().getJid().asBareJid().toEscapedString()); intent.putExtra("conversation", conversation.getUuid()); startActivityForResult(intent, requestCode); return true; @@ -799,9 +927,10 @@ public void updateChatMsgHint() { } else if (multi && conversation.getNextCounterpart() != null) { this.binding.textinput.setHint(R.string.send_unencrypted_message); this.binding.textInputHint.setVisibility(View.VISIBLE); - this.binding.textInputHint.setText(getString( - R.string.send_private_message_to, - conversation.getNextCounterpart().getResource())); + this.binding.textInputHint.setText( + getString( + R.string.send_private_message_to, + conversation.getNextCounterpart().getResource())); } else if (multi && !conversation.getMucOptions().participating()) { this.binding.textInputHint.setVisibility(View.GONE); this.binding.textinput.setHint(R.string.you_are_not_participating); @@ -839,14 +968,16 @@ private void handlePositiveActivityResult(int requestCode, final Intent data) { triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL); break; case ATTACHMENT_CHOICE_CHOOSE_IMAGE: - final List imageUris = Attachment.extractAttachments(getActivity(), data, Attachment.Type.IMAGE); + final List imageUris = + Attachment.extractAttachments(getActivity(), data, Attachment.Type.IMAGE); mediaPreviewAdapter.addMediaPreviews(imageUris); toggleInputMethod(); break; case ATTACHMENT_CHOICE_TAKE_PHOTO: final Uri takePhotoUri = pendingTakePhotoUri.pop(); if (takePhotoUri != null) { - mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), takePhotoUri, Attachment.Type.IMAGE)); + mediaPreviewAdapter.addMediaPreviews( + Attachment.of(getActivity(), takePhotoUri, Attachment.Type.IMAGE)); toggleInputMethod(); } else { Log.d(Config.LOGTAG, "lost take photo uri. unable to to attach"); @@ -855,8 +986,12 @@ private void handlePositiveActivityResult(int requestCode, final Intent data) { case ATTACHMENT_CHOICE_CHOOSE_FILE: case ATTACHMENT_CHOICE_RECORD_VIDEO: case ATTACHMENT_CHOICE_RECORD_VOICE: - final Attachment.Type type = requestCode == ATTACHMENT_CHOICE_RECORD_VOICE ? Attachment.Type.RECORDING : Attachment.Type.FILE; - final List fileUris = Attachment.extractAttachments(getActivity(), data, type); + final Attachment.Type type = + requestCode == ATTACHMENT_CHOICE_RECORD_VOICE + ? Attachment.Type.RECORDING + : Attachment.Type.FILE; + final List fileUris = + Attachment.extractAttachments(getActivity(), data, type); mediaPreviewAdapter.addMediaPreviews(fileUris); toggleInputMethod(); break; @@ -870,14 +1005,17 @@ private void handlePositiveActivityResult(int requestCode, final Intent data) { } else { geo = Uri.parse(String.format("geo:%s,%s", latitude, longitude)); } - mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), geo, Attachment.Type.LOCATION)); + mediaPreviewAdapter.addMediaPreviews( + Attachment.of(getActivity(), geo, Attachment.Type.LOCATION)); toggleInputMethod(); break; case REQUEST_INVITE_TO_CONVERSATION: XmppActivity.ConferenceInvite invite = XmppActivity.ConferenceInvite.parse(data); if (invite != null) { if (invite.execute(activity)) { - activity.mToast = Toast.makeText(activity, R.string.creating_conference, Toast.LENGTH_LONG); + activity.mToast = + Toast.makeText( + activity, R.string.creating_conference, Toast.LENGTH_LONG); activity.mToast.show(); } } @@ -887,40 +1025,51 @@ private void handlePositiveActivityResult(int requestCode, final Intent data) { private void commitAttachments() { final List attachments = mediaPreviewAdapter.getAttachments(); - if (anyNeedsExternalStoragePermission(attachments) && !hasPermissions(REQUEST_COMMIT_ATTACHMENTS, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + if (anyNeedsExternalStoragePermission(attachments) + && !hasPermissions( + REQUEST_COMMIT_ATTACHMENTS, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { return; } if (trustKeysIfNeeded(conversation, REQUEST_TRUST_KEYS_ATTACHMENTS)) { return; } - final PresenceSelector.OnPresenceSelected callback = () -> { - for (Iterator i = attachments.iterator(); i.hasNext(); i.remove()) { - final Attachment attachment = i.next(); - if (attachment.getType() == Attachment.Type.LOCATION) { - attachLocationToConversation(conversation, attachment.getUri()); - } else if (attachment.getType() == Attachment.Type.IMAGE) { - Log.d(Config.LOGTAG, "ConversationsActivity.commitAttachments() - attaching image to conversations. CHOOSE_IMAGE"); - attachImageToConversation(conversation, attachment.getUri(), attachment.getMime()); - } else { - Log.d(Config.LOGTAG, "ConversationsActivity.commitAttachments() - attaching file to conversations. CHOOSE_FILE/RECORD_VOICE/RECORD_VIDEO"); - attachFileToConversation(conversation, attachment.getUri(), attachment.getMime()); - } - } - mediaPreviewAdapter.notifyDataSetChanged(); - toggleInputMethod(); - }; + final PresenceSelector.OnPresenceSelected callback = + () -> { + for (Iterator i = attachments.iterator(); i.hasNext(); i.remove()) { + final Attachment attachment = i.next(); + if (attachment.getType() == Attachment.Type.LOCATION) { + attachLocationToConversation(conversation, attachment.getUri()); + } else if (attachment.getType() == Attachment.Type.IMAGE) { + Log.d( + Config.LOGTAG, + "ConversationsActivity.commitAttachments() - attaching image to conversations. CHOOSE_IMAGE"); + attachImageToConversation( + conversation, attachment.getUri(), attachment.getMime()); + } else { + Log.d( + Config.LOGTAG, + "ConversationsActivity.commitAttachments() - attaching file to conversations. CHOOSE_FILE/RECORD_VOICE/RECORD_VIDEO"); + attachFileToConversation( + conversation, attachment.getUri(), attachment.getMime()); + } + } + mediaPreviewAdapter.notifyDataSetChanged(); + toggleInputMethod(); + }; if (conversation == null || conversation.getMode() == Conversation.MODE_MULTI || Attachment.canBeSendInband(attachments) - || (conversation.getAccount().httpUploadAvailable() && FileBackend.allFilesUnderSize(getActivity(), attachments, getMaxHttpUploadSize(conversation)))) { + || (conversation.getAccount().httpUploadAvailable() + && FileBackend.allFilesUnderSize( + getActivity(), attachments, getMaxHttpUploadSize(conversation)))) { callback.onPresenceSelected(); } else { activity.selectPresence(conversation, callback); } } - - private static boolean anyNeedsExternalStoragePermission(final Collection attachments) { + private static boolean anyNeedsExternalStoragePermission( + final Collection attachments) { for (final Attachment attachment : attachments) { if (attachment.getType() != Attachment.Type.LOCATION) { return true; @@ -940,7 +1089,9 @@ private void handleNegativeActivityResult(int requestCode) { switch (requestCode) { case ATTACHMENT_CHOICE_TAKE_PHOTO: if (pendingTakePhotoUri.clear()) { - Log.d(Config.LOGTAG, "cleared pending photo uri after negative activity result"); + Log.d( + Config.LOGTAG, + "cleared pending photo uri after negative activity result"); } break; } @@ -968,14 +1119,15 @@ public void onAttach(Activity activity) { if (activity instanceof ConversationsActivity) { this.activity = (ConversationsActivity) activity; } else { - throw new IllegalStateException("Trying to attach fragment to activity that is not the ConversationsActivity"); + throw new IllegalStateException( + "Trying to attach fragment to activity that is not the ConversationsActivity"); } } @Override public void onDetach() { super.onDetach(); - this.activity = null; //TODO maybe not a good idea since some callbacks really need it + this.activity = null; // TODO maybe not a good idea since some callbacks really need it } @Override @@ -997,30 +1149,42 @@ public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { final MenuItem menuVideoCall = menu.findItem(R.id.action_video_call); final MenuItem menuTogglePinned = menu.findItem(R.id.action_toggle_pinned); - if (conversation != null) { if (conversation.getMode() == Conversation.MODE_MULTI) { menuContactDetails.setVisible(false); menuInviteContact.setVisible(conversation.getMucOptions().canInvite()); - menuMucDetails.setTitle(conversation.getMucOptions().isPrivateAndNonAnonymous() ? R.string.action_muc_details : R.string.channel_details); + menuMucDetails.setTitle( + conversation.getMucOptions().isPrivateAndNonAnonymous() + ? R.string.action_muc_details + : R.string.channel_details); menuCall.setVisible(false); menuOngoingCall.setVisible(false); } else { - final XmppConnectionService service = activity == null ? null : activity.xmppConnectionService; - final Optional ongoingRtpSession = service == null ? Optional.absent() : service.getJingleConnectionManager().getOngoingRtpConnection(conversation.getContact()); + final XmppConnectionService service = + activity == null ? null : activity.xmppConnectionService; + final Optional ongoingRtpSession = + service == null + ? Optional.absent() + : service.getJingleConnectionManager() + .getOngoingRtpConnection(conversation.getContact()); if (ongoingRtpSession.isPresent()) { menuOngoingCall.setVisible(true); menuCall.setVisible(false); } else { menuOngoingCall.setVisible(false); - final RtpCapability.Capability rtpCapability = RtpCapability.check(conversation.getContact()); - final boolean cameraAvailable = activity != null && activity.isCameraFeatureAvailable(); + final RtpCapability.Capability rtpCapability = + RtpCapability.check(conversation.getContact()); + final boolean cameraAvailable = + activity != null && activity.isCameraFeatureAvailable(); menuCall.setVisible(rtpCapability != RtpCapability.Capability.NONE); - menuVideoCall.setVisible(rtpCapability == RtpCapability.Capability.VIDEO && cameraAvailable); + menuVideoCall.setVisible( + rtpCapability == RtpCapability.Capability.VIDEO && cameraAvailable); } menuContactDetails.setVisible(!this.conversation.withSelf()); menuMucDetails.setVisible(false); - menuInviteContact.setVisible(service != null && service.findConferenceServer(conversation.getAccount()) != null); + menuInviteContact.setVisible( + service != null + && service.findConferenceServer(conversation.getAccount()) != null); } if (conversation.isMuted()) { menuMute.setVisible(false); @@ -1039,14 +1203,17 @@ public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { } @Override - public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - this.binding = DataBindingUtil.inflate(inflater, R.layout.fragment_conversation, container, false); - binding.getRoot().setOnClickListener(null); //TODO why the fuck did we do this? + public View onCreateView( + final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + this.binding = + DataBindingUtil.inflate(inflater, R.layout.fragment_conversation, container, false); + binding.getRoot().setOnClickListener(null); // TODO why the fuck did we do this? - binding.textinput.addTextChangedListener(new StylingHelper.MessageEditorStyler(binding.textinput)); + binding.textinput.addTextChangedListener( + new StylingHelper.MessageEditorStyler(binding.textinput)); binding.textinput.setOnEditorActionListener(mEditorActionListener); - binding.textinput.setRichContentListener(new String[]{"image/*"}, mEditorContentListener); + binding.textinput.setRichContentListener(new String[] {"image/*"}, mEditorContentListener); binding.textSendButton.setOnClickListener(this.mSendButtonListener); @@ -1063,7 +1230,8 @@ public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bun registerForContextMenu(binding.messagesView); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - this.binding.textinput.setCustomInsertionActionModeCallback(new EditMessageActionModeCallback(this.binding.textinput)); + this.binding.textinput.setCustomInsertionActionModeCallback( + new EditMessageActionModeCallback(this.binding.textinput)); } return binding.getRoot(); @@ -1081,9 +1249,12 @@ private void quoteText(String text) { if (binding.textinput.isEnabled()) { binding.textinput.insertAsQuote(text); binding.textinput.requestFocus(); - InputMethodManager inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + InputMethodManager inputMethodManager = + (InputMethodManager) + getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); if (inputMethodManager != null) { - inputMethodManager.showSoftInput(binding.textinput, InputMethodManager.SHOW_IMPLICIT); + inputMethodManager.showSoftInput( + binding.textinput, InputMethodManager.SHOW_IMPLICIT); } } } @@ -1094,7 +1265,7 @@ private void quoteMessage(Message message) { @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { - //This should cancel any remaining click events that would otherwise trigger links + // This should cancel any remaining click events that would otherwise trigger links v.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0)); synchronized (this.messageList) { super.onCreateContextMenu(menu, v, menuInfo); @@ -1113,18 +1284,26 @@ private void populateContextMenu(ContextMenu menu) { } if (m.getType() != Message.TYPE_STATUS && m.getType() != Message.TYPE_RTP_SESSION) { - if (m.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || m.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) { + if (m.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE + || m.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) { return; } - if (m.getStatus() == Message.STATUS_RECEIVED && t != null && (t.getStatus() == Transferable.STATUS_CANCELLED || t.getStatus() == Transferable.STATUS_FAILED)) { + if (m.getStatus() == Message.STATUS_RECEIVED + && t != null + && (t.getStatus() == Transferable.STATUS_CANCELLED + || t.getStatus() == Transferable.STATUS_FAILED)) { return; } final boolean deleted = m.isDeleted(); - final boolean encrypted = m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED - || m.getEncryption() == Message.ENCRYPTION_PGP; - final boolean receiving = m.getStatus() == Message.STATUS_RECEIVED && (t instanceof JingleFileTransferConnection || t instanceof HttpDownloadConnection); + final boolean encrypted = + m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED + || m.getEncryption() == Message.ENCRYPTION_PGP; + final boolean receiving = + m.getStatus() == Message.STATUS_RECEIVED + && (t instanceof JingleFileTransferConnection + || t instanceof HttpDownloadConnection); activity.getMenuInflater().inflate(R.menu.message_context, menu); menu.setHeaderTitle(R.string.message_options); MenuItem openWith = menu.findItem(R.id.open_with); @@ -1141,8 +1320,16 @@ private void populateContextMenu(ContextMenu menu) { MenuItem deleteFile = menu.findItem(R.id.delete_file); MenuItem showErrorMessage = menu.findItem(R.id.show_error_message); final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(m); - final boolean showError = m.getStatus() == Message.STATUS_SEND_FAILED && m.getErrorMessage() != null && !Message.ERROR_MESSAGE_CANCELLED.equals(m.getErrorMessage()); - if (!m.isFileOrImage() && !encrypted && !m.isGeoUri() && !m.treatAsDownloadable() && !unInitiatedButKnownSize && t == null) { + final boolean showError = + m.getStatus() == Message.STATUS_SEND_FAILED + && m.getErrorMessage() != null + && !Message.ERROR_MESSAGE_CANCELLED.equals(m.getErrorMessage()); + if (!m.isFileOrImage() + && !encrypted + && !m.isGeoUri() + && !m.treatAsDownloadable() + && !unInitiatedButKnownSize + && t == null) { copyMessage.setVisible(true); quoteMessage.setVisible(!showError && MessageUtils.prepareQuote(m).length() > 0); String body = m.getMergedBody().toString(); @@ -1163,7 +1350,10 @@ private void populateContextMenu(ContextMenu menu) { && m.getConversation() instanceof Conversation) { correctMessage.setVisible(true); } - if ((m.isFileOrImage() && !deleted && !receiving) || (m.getType() == Message.TYPE_TEXT && !m.treatAsDownloadable()) && !unInitiatedButKnownSize && t == null) { + if ((m.isFileOrImage() && !deleted && !receiving) + || (m.getType() == Message.TYPE_TEXT && !m.treatAsDownloadable()) + && !unInitiatedButKnownSize + && t == null) { shareWith.setVisible(true); } if (m.getStatus() == Message.STATUS_SEND_FAILED) { @@ -1178,27 +1368,38 @@ private void populateContextMenu(ContextMenu menu) { } if (m.isFileOrImage() && deleted && m.hasFileOnRemoteHost()) { downloadFile.setVisible(true); - downloadFile.setTitle(activity.getString(R.string.download_x_file, UIHelper.getFileDescriptionString(activity, m))); - } - final boolean waitingOfferedSending = m.getStatus() == Message.STATUS_WAITING - || m.getStatus() == Message.STATUS_UNSEND - || m.getStatus() == Message.STATUS_OFFERED; - final boolean cancelable = (t != null && !deleted) || waitingOfferedSending && m.needsUploading(); + downloadFile.setTitle( + activity.getString( + R.string.download_x_file, + UIHelper.getFileDescriptionString(activity, m))); + } + final boolean waitingOfferedSending = + m.getStatus() == Message.STATUS_WAITING + || m.getStatus() == Message.STATUS_UNSEND + || m.getStatus() == Message.STATUS_OFFERED; + final boolean cancelable = + (t != null && !deleted) || waitingOfferedSending && m.needsUploading(); if (cancelable) { cancelTransmission.setVisible(true); } if (m.isFileOrImage() && !deleted && !cancelable) { final String path = m.getRelativeFilePath(); - if (path == null || !path.startsWith("/") || FileBackend.inConversationsDirectory(requireActivity(), path)) { + if (path == null + || !path.startsWith("/") + || FileBackend.inConversationsDirectory(requireActivity(), path)) { deleteFile.setVisible(true); - deleteFile.setTitle(activity.getString(R.string.delete_x_file, UIHelper.getFileDescriptionString(activity, m))); + deleteFile.setTitle( + activity.getString( + R.string.delete_x_file, + UIHelper.getFileDescriptionString(activity, m))); } } if (showError) { showErrorMessage.setVisible(true); } final String mime = m.isFileOrImage() ? m.getMimeType() : null; - if ((m.isGeoUri() && GeoHelper.openInOsmAnd(getActivity(), m)) || (mime != null && mime.startsWith("audio/"))) { + if ((m.isGeoUri() && GeoHelper.openInOsmAnd(getActivity(), m)) + || (mime != null && mime.startsWith("audio/"))) { openWith.setVisible(true); } } @@ -1285,7 +1486,9 @@ public boolean onOptionsItemSelected(final MenuItem item) { ConferenceDetailsActivity.open(getActivity(), conversation); break; case R.id.action_invite: - startActivityForResult(ChooseContactActivity.create(activity, conversation), REQUEST_INVITE_TO_CONVERSATION); + startActivityForResult( + ChooseContactActivity.create(activity, conversation), + REQUEST_INVITE_TO_CONVERSATION); break; case R.id.action_clear_history: clearHistoryDialog(conversation); @@ -1294,7 +1497,7 @@ public boolean onOptionsItemSelected(final MenuItem item) { muteConversationDialog(conversation); break; case R.id.action_unmute: - unmuteConversation(conversation); + unMuteConversation(conversation); break; case R.id.action_block: case R.id.action_unblock: @@ -1328,11 +1531,16 @@ private void startSearch() { } private void returnToOngoingCall() { - final Optional ongoingRtpSession = activity.xmppConnectionService.getJingleConnectionManager().getOngoingRtpConnection(conversation.getContact()); + final Optional ongoingRtpSession = + activity.xmppConnectionService + .getJingleConnectionManager() + .getOngoingRtpConnection(conversation.getContact()); if (ongoingRtpSession.isPresent()) { final OngoingRtpSession id = ongoingRtpSession.get(); final Intent intent = new Intent(getActivity(), RtpSessionActivity.class); - intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, id.getAccount().getJid().asBareJid().toEscapedString()); + intent.putExtra( + RtpSessionActivity.EXTRA_ACCOUNT, + id.getAccount().getJid().asBareJid().toEscapedString()); intent.putExtra(RtpSessionActivity.EXTRA_WITH, id.getWith().toEscapedString()); if (id instanceof AbstractJingleConnection.Id) { intent.setAction(Intent.ACTION_VIEW); @@ -1346,11 +1554,11 @@ private void returnToOngoingCall() { } startActivity(intent); } - } private void togglePinned() { - final boolean pinned = conversation.getBooleanAttribute(Conversation.ATTRIBUTE_PINNED_ON_TOP, false); + final boolean pinned = + conversation.getBooleanAttribute(Conversation.ATTRIBUTE_PINNED_ON_TOP, false); conversation.setAttribute(Conversation.ATTRIBUTE_PINNED_ON_TOP, !pinned); activity.xmppConnectionService.updateConversation(conversation); activity.invalidateOptionsMenu(); @@ -1361,7 +1569,16 @@ private void checkPermissionAndTriggerAudioCall() { Toast.makeText(activity, R.string.disable_tor_to_make_call, Toast.LENGTH_SHORT).show(); return; } - if (hasPermissions(REQUEST_START_AUDIO_CALL, Manifest.permission.RECORD_AUDIO)) { + final List permissions; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + permissions = + Arrays.asList( + Manifest.permission.RECORD_AUDIO, + Manifest.permission.BLUETOOTH_CONNECT); + } else { + permissions = Collections.singletonList(Manifest.permission.RECORD_AUDIO); + } + if (hasPermissions(REQUEST_START_AUDIO_CALL, permissions)) { triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VOICE_CALL); } } @@ -1371,15 +1588,26 @@ private void checkPermissionAndTriggerVideoCall() { Toast.makeText(activity, R.string.disable_tor_to_make_call, Toast.LENGTH_SHORT).show(); return; } - if (hasPermissions(REQUEST_START_VIDEO_CALL, Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)) { + final List permissions; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + permissions = + Arrays.asList( + Manifest.permission.RECORD_AUDIO, + Manifest.permission.CAMERA, + Manifest.permission.BLUETOOTH_CONNECT); + } else { + permissions = + Arrays.asList(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA); + } + if (hasPermissions(REQUEST_START_VIDEO_CALL, permissions)) { triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL); } } - private void triggerRtpSession(final String action) { if (activity.xmppConnectionService.getJingleConnectionManager().isBusy()) { - Toast.makeText(getActivity(), R.string.only_one_call_at_a_time, Toast.LENGTH_LONG).show(); + Toast.makeText(getActivity(), R.string.only_one_call_at_a_time, Toast.LENGTH_LONG) + .show(); return; } final Contact contact = conversation.getContact(); @@ -1392,9 +1620,13 @@ private void triggerRtpSession(final String action) { } else { capability = RtpCapability.Capability.AUDIO; } - PresenceSelector.selectFullJidForDirectRtpConnection(activity, contact, capability, fullJid -> { - triggerRtpSession(contact.getAccount(), fullJid, action); - }); + PresenceSelector.selectFullJidForDirectRtpConnection( + activity, + contact, + capability, + fullJid -> { + triggerRtpSession(contact.getAccount(), fullJid, action); + }); } } @@ -1448,7 +1680,11 @@ private void handleEncryptionSelection(MenuItem item) { item.setChecked(true); } else { updated = false; - activity.announcePgp(conversation.getAccount(), conversation, null, activity.onOpenPGPKeyPublished); + activity.announcePgp( + conversation.getAccount(), + conversation, + null, + activity.onOpenPGPKeyPublished); } } else { activity.showInstallPgpDialog(); @@ -1456,8 +1692,11 @@ private void handleEncryptionSelection(MenuItem item) { } break; case R.id.encryption_choice_axolotl: - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(conversation.getAccount()) - + "Enabled axolotl for Contact " + conversation.getContact().getJid()); + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(conversation.getAccount()) + + "Enabled axolotl for Contact " + + conversation.getContact().getJid()); updated = conversation.setNextEncryption(Message.ENCRYPTION_AXOLOTL); item.setChecked(true); break; @@ -1479,11 +1718,18 @@ public void attachFile(final int attachmentChoice) { public void attachFile(final int attachmentChoice, final boolean updateRecentlyUsed) { if (attachmentChoice == ATTACHMENT_CHOICE_RECORD_VOICE) { - if (!hasPermissions(attachmentChoice, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO)) { + if (!hasPermissions( + attachmentChoice, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.RECORD_AUDIO)) { return; } - } else if (attachmentChoice == ATTACHMENT_CHOICE_TAKE_PHOTO || attachmentChoice == ATTACHMENT_CHOICE_RECORD_VIDEO) { - if (!hasPermissions(attachmentChoice, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA)) { + } else if (attachmentChoice == ATTACHMENT_CHOICE_TAKE_PHOTO + || attachmentChoice == ATTACHMENT_CHOICE_RECORD_VIDEO) { + if (!hasPermissions( + attachmentChoice, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.CAMERA)) { return; } } else if (attachmentChoice != ATTACHMENT_CHOICE_LOCATION) { @@ -1498,39 +1744,50 @@ public void attachFile(final int attachmentChoice, final boolean updateRecentlyU final int mode = conversation.getMode(); if (encryption == Message.ENCRYPTION_PGP) { if (activity.hasPgp()) { - if (mode == Conversation.MODE_SINGLE && conversation.getContact().getPgpKeyId() != 0) { - activity.xmppConnectionService.getPgpEngine().hasKey( - conversation.getContact(), - new UiCallback() { - - @Override - public void userInputRequired(PendingIntent pi, Contact contact) { - startPendingIntent(pi, attachmentChoice); - } + if (mode == Conversation.MODE_SINGLE + && conversation.getContact().getPgpKeyId() != 0) { + activity.xmppConnectionService + .getPgpEngine() + .hasKey( + conversation.getContact(), + new UiCallback() { + + @Override + public void userInputRequired( + PendingIntent pi, Contact contact) { + startPendingIntent(pi, attachmentChoice); + } - @Override - public void success(Contact contact) { - invokeAttachFileIntent(attachmentChoice); - } + @Override + public void success(Contact contact) { + invokeAttachFileIntent(attachmentChoice); + } - @Override - public void error(int error, Contact contact) { - activity.replaceToast(getString(error)); - } - }); - } else if (mode == Conversation.MODE_MULTI && conversation.getMucOptions().pgpKeysInUse()) { + @Override + public void error(int error, Contact contact) { + activity.replaceToast(getString(error)); + } + }); + } else if (mode == Conversation.MODE_MULTI + && conversation.getMucOptions().pgpKeysInUse()) { if (!conversation.getMucOptions().everybodyHasKeys()) { - Toast warning = Toast.makeText(getActivity(), R.string.missing_public_keys, Toast.LENGTH_LONG); + Toast warning = + Toast.makeText( + getActivity(), + R.string.missing_public_keys, + Toast.LENGTH_LONG); warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0); warning.show(); } invokeAttachFileIntent(attachmentChoice); } else { - showNoPGPKeyDialog(false, (dialog, which) -> { - conversation.setNextEncryption(Message.ENCRYPTION_NONE); - activity.xmppConnectionService.updateConversation(conversation); - invokeAttachFileIntent(attachmentChoice); - }); + showNoPGPKeyDialog( + false, + (dialog, which) -> { + conversation.setNextEncryption(Message.ENCRYPTION_NONE); + activity.xmppConnectionService.updateConversation(conversation); + invokeAttachFileIntent(attachmentChoice); + }); } } else { activity.showInstallPgpDialog(); @@ -1542,18 +1799,24 @@ public void error(int error, Contact contact) { private void storeRecentlyUsedQuickAction(final int attachmentChoice) { try { - activity.getPreferences().edit() - .putString(RECENTLY_USED_QUICK_ACTION, SendButtonAction.of(attachmentChoice).toString()) + activity.getPreferences() + .edit() + .putString( + RECENTLY_USED_QUICK_ACTION, + SendButtonAction.of(attachmentChoice).toString()) .apply(); } catch (IllegalArgumentException e) { - //just do not save + // just do not save } } @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + final PermissionUtils.PermissionResult permissionResult = + PermissionUtils.removeBluetoothConnect(permissions, grantResults); if (grantResults.length > 0) { - if (allGranted(grantResults)) { + if (allGranted(permissionResult.grantResults)) { switch (requestCode) { case REQUEST_START_DOWNLOAD: if (this.mPendingDownloadableMessage != null) { @@ -1580,7 +1843,8 @@ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permis } } else { @StringRes int res; - String firstDenied = getFirstDenied(grantResults, permissions); + String firstDenied = + getFirstDenied(permissionResult.grantResults, permissionResult.permissions); if (Manifest.permission.RECORD_AUDIO.equals(firstDenied)) { res = R.string.no_microphone_permission; } else if (Manifest.permission.CAMERA.equals(firstDenied)) { @@ -1588,7 +1852,11 @@ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permis } else { res = R.string.no_storage_permission; } - Toast.makeText(getActivity(), getString(res, getString(R.string.app_name)), Toast.LENGTH_SHORT).show(); + Toast.makeText( + getActivity(), + getString(res, getString(R.string.app_name)), + Toast.LENGTH_SHORT) + .show(); } } if (writeGranted(grantResults, permissions)) { @@ -1613,41 +1881,53 @@ public void startDownloadable(Message message) { } if (!transferable.start()) { Log.d(Config.LOGTAG, "type: " + transferable.getClass().getName()); - Toast.makeText(getActivity(), R.string.not_connected_try_again, Toast.LENGTH_SHORT).show(); + Toast.makeText(getActivity(), R.string.not_connected_try_again, Toast.LENGTH_SHORT) + .show(); } - } else if (message.treatAsDownloadable() || message.hasFileOnRemoteHost() || MessageUtils.unInitiatedButKnownSize(message)) { + } else if (message.treatAsDownloadable() + || message.hasFileOnRemoteHost() + || MessageUtils.unInitiatedButKnownSize(message)) { createNewConnection(message); } else { - Log.d(Config.LOGTAG, message.getConversation().getAccount() + ": unable to start downloadable"); + Log.d( + Config.LOGTAG, + message.getConversation().getAccount() + ": unable to start downloadable"); } } private void createNewConnection(final Message message) { if (!activity.xmppConnectionService.hasInternetConnection()) { - Toast.makeText(getActivity(), R.string.not_connected_try_again, Toast.LENGTH_SHORT).show(); + Toast.makeText(getActivity(), R.string.not_connected_try_again, Toast.LENGTH_SHORT) + .show(); return; } - activity.xmppConnectionService.getHttpConnectionManager().createNewDownloadConnection(message, true); + activity.xmppConnectionService + .getHttpConnectionManager() + .createNewDownloadConnection(message, true); } @SuppressLint("InflateParams") protected void clearHistoryDialog(final Conversation conversation) { final AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()); builder.setTitle(getString(R.string.clear_conversation_history)); - final View dialogView = requireActivity().getLayoutInflater().inflate(R.layout.dialog_clear_history, null); - final CheckBox endConversationCheckBox = dialogView.findViewById(R.id.end_conversation_checkbox); + final View dialogView = + requireActivity().getLayoutInflater().inflate(R.layout.dialog_clear_history, null); + final CheckBox endConversationCheckBox = + dialogView.findViewById(R.id.end_conversation_checkbox); builder.setView(dialogView); builder.setNegativeButton(getString(R.string.cancel), null); - builder.setPositiveButton(getString(R.string.confirm), (dialog, which) -> { - this.activity.xmppConnectionService.clearConversationHistory(conversation); - if (endConversationCheckBox.isChecked()) { - this.activity.xmppConnectionService.archiveConversation(conversation); - this.activity.onConversationArchived(conversation); - } else { - activity.onConversationsListItemUpdated(); - refresh(); - } - }); + builder.setPositiveButton( + getString(R.string.confirm), + (dialog, which) -> { + this.activity.xmppConnectionService.clearConversationHistory(conversation); + if (endConversationCheckBox.isChecked()) { + this.activity.xmppConnectionService.archiveConversation(conversation); + this.activity.onConversationArchived(conversation); + } else { + activity.onConversationsListItemUpdated(); + refresh(); + } + }); builder.create().show(); } @@ -1663,27 +1943,30 @@ protected void muteConversationDialog(final Conversation conversation) { labels[i] = TimeFrameUtils.resolve(activity, 1000L * durations[i]); } } - builder.setItems(labels, (dialog, which) -> { - final long till; - if (durations[which] == -1) { - till = Long.MAX_VALUE; - } else { - till = System.currentTimeMillis() + (durations[which] * 1000L); - } - conversation.setMutedTill(till); - activity.xmppConnectionService.updateConversation(conversation); - activity.onConversationsListItemUpdated(); - refresh(); - requireActivity().invalidateOptionsMenu(); - }); + builder.setItems( + labels, + (dialog, which) -> { + final long till; + if (durations[which] == -1) { + till = Long.MAX_VALUE; + } else { + till = System.currentTimeMillis() + (durations[which] * 1000L); + } + conversation.setMutedTill(till); + activity.xmppConnectionService.updateConversation(conversation); + activity.onConversationsListItemUpdated(); + refresh(); + requireActivity().invalidateOptionsMenu(); + }); builder.create().show(); } - private boolean hasPermissions(int requestCode, String... permissions) { + private boolean hasPermissions(int requestCode, List permissions) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { final List missingPermissions = new ArrayList<>(); for (String permission : permissions) { - if (Config.ONLY_INTERNAL_STORAGE && permission.equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + if (Config.ONLY_INTERNAL_STORAGE + && permission.equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { continue; } if (activity.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) { @@ -1693,7 +1976,9 @@ private boolean hasPermissions(int requestCode, String... permissions) { if (missingPermissions.size() == 0) { return true; } else { - requestPermissions(missingPermissions.toArray(new String[missingPermissions.size()]), requestCode); + requestPermissions( + missingPermissions.toArray(new String[0]), + requestCode); return false; } } else { @@ -1701,7 +1986,11 @@ private boolean hasPermissions(int requestCode, String... permissions) { } } - public void unmuteConversation(final Conversation conversation) { + private boolean hasPermissions(int requestCode, String... permissions) { + return hasPermissions(requestCode, ImmutableList.copyOf(permissions)); + } + + public void unMuteConversation(final Conversation conversation) { conversation.setMutedTill(0); this.activity.xmppConnectionService.updateConversation(conversation); this.activity.onConversationsListItemUpdated(); @@ -1709,7 +1998,6 @@ public void unmuteConversation(final Conversation conversation) { requireActivity().invalidateOptionsMenu(); } - protected void invokeAttachFileIntent(final int attachmentChoice) { Intent intent = new Intent(); boolean chooser = false; @@ -1789,7 +2077,8 @@ private String getLastVisibleMessageUuid() { try { message = (Message) binding.messagesView.getItemAtPosition(i); } catch (IndexOutOfBoundsException e) { - //should not happen if we synchronize properly. however if that fails we just gonna try item -1 + // should not happen if we synchronize properly. however if that fails we + // just gonna try item -1 continue; } if (message.getType() != Message.TYPE_STATUS) { @@ -1811,7 +2100,8 @@ private void openWith(final Message message) { if (message.isGeoUri()) { GeoHelper.view(getActivity(), message); } else { - final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); + final DownloadableFile file = + activity.xmppConnectionService.getFileBackend().getFile(message); ViewUtil.view(activity, file); } } @@ -1820,7 +2110,8 @@ private void showErrorMessage(final Message message) { AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()); builder.setTitle(R.string.error_message); final String errorMessage = message.getErrorMessage(); - final String[] errorMessageParts = errorMessage == null ? new String[0] : errorMessage.split("\\u001f"); + final String[] errorMessageParts = + errorMessage == null ? new String[0] : errorMessage.split("\\u001f"); final String displayError; if (errorMessageParts.length == 2) { displayError = errorMessageParts[1]; @@ -1828,31 +2119,37 @@ private void showErrorMessage(final Message message) { displayError = errorMessage; } builder.setMessage(displayError); - builder.setNegativeButton(R.string.copy_to_clipboard, (dialog, which) -> { - activity.copyTextToClipboard(displayError, R.string.error_message); - Toast.makeText(activity, R.string.error_message_copied_to_clipboard, Toast.LENGTH_SHORT).show(); - }); + builder.setNegativeButton( + R.string.copy_to_clipboard, + (dialog, which) -> { + activity.copyTextToClipboard(displayError, R.string.error_message); + Toast.makeText( + activity, + R.string.error_message_copied_to_clipboard, + Toast.LENGTH_SHORT) + .show(); + }); builder.setPositiveButton(R.string.confirm, null); builder.create().show(); } - private void deleteFile(final Message message) { AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()); builder.setNegativeButton(R.string.cancel, null); builder.setTitle(R.string.delete_file_dialog); builder.setMessage(R.string.delete_file_dialog_msg); - builder.setPositiveButton(R.string.confirm, (dialog, which) -> { - if (activity.xmppConnectionService.getFileBackend().deleteFile(message)) { - message.setDeleted(true); - activity.xmppConnectionService.evictPreview(message.getUuid()); - activity.xmppConnectionService.updateMessage(message, false); - activity.onConversationsListItemUpdated(); - refresh(); - } - }); + builder.setPositiveButton( + R.string.confirm, + (dialog, which) -> { + if (activity.xmppConnectionService.getFileBackend().deleteFile(message)) { + message.setDeleted(true); + activity.xmppConnectionService.evictPreview(message.getUuid()); + activity.xmppConnectionService.updateMessage(message, false); + activity.onConversationsListItemUpdated(); + refresh(); + } + }); builder.create().show(); - } private void resendMessage(final Message message) { @@ -1861,21 +2158,29 @@ private void resendMessage(final Message message) { return; } final Conversation conversation = (Conversation) message.getConversation(); - final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); + final DownloadableFile file = + activity.xmppConnectionService.getFileBackend().getFile(message); if ((file.exists() && file.canRead()) || message.hasFileOnRemoteHost()) { final XmppConnection xmppConnection = conversation.getAccount().getXmppConnection(); if (!message.hasFileOnRemoteHost() && xmppConnection != null && conversation.getMode() == Conversational.MODE_SINGLE - && !xmppConnection.getFeatures().httpUpload(message.getFileParams().getSize())) { - activity.selectPresence(conversation, () -> { - message.setCounterpart(conversation.getNextCounterpart()); - activity.xmppConnectionService.resendFailedMessages(message); - new Handler().post(() -> { - int size = messageList.size(); - this.binding.messagesView.setSelection(size - 1); - }); - }); + && !xmppConnection + .getFeatures() + .httpUpload(message.getFileParams().getSize())) { + activity.selectPresence( + conversation, + () -> { + message.setCounterpart(conversation.getNextCounterpart()); + activity.xmppConnectionService.resendFailedMessages(message); + new Handler() + .post( + () -> { + int size = messageList.size(); + this.binding.messagesView.setSelection( + size - 1); + }); + }); return; } } else if (!Compatibility.hasStoragePermission(getActivity())) { @@ -1891,10 +2196,12 @@ private void resendMessage(final Message message) { } } activity.xmppConnectionService.resendFailedMessages(message); - new Handler().post(() -> { - int size = messageList.size(); - this.binding.messagesView.setSelection(size - 1); - }); + new Handler() + .post( + () -> { + int size = messageList.size(); + this.binding.messagesView.setSelection(size - 1); + }); } private void cancelTransmission(Message message) { @@ -1902,7 +2209,8 @@ private void cancelTransmission(Message message) { if (transferable != null) { transferable.cancel(); } else if (message.getStatus() != Message.STATUS_RECEIVED) { - activity.xmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED, Message.ERROR_MESSAGE_CANCELLED); + activity.xmppConnectionService.markMessage( + message, Message.STATUS_SEND_FAILED, Message.ERROR_MESSAGE_CANCELLED); } } @@ -1933,7 +2241,6 @@ private void correctMessage(Message message) { this.conversation.setDraftMessage(editable.toString()); this.binding.textinput.setText(""); this.binding.textinput.append(message.getBody()); - } private void highlightInConference(String nick) { @@ -1949,14 +2256,22 @@ private void highlightInConference(String nick) { editable.insert(pos, nick + ": "); } else { if (pos > 2 && editable.subSequence(pos - 2, pos).toString().equals(": ")) { - if (NickValidityChecker.check(conversation, Arrays.asList(editable.subSequence(0, pos - 2).toString().split(", ")))) { + if (NickValidityChecker.check( + conversation, + Arrays.asList( + editable.subSequence(0, pos - 2).toString().split(", ")))) { editable.insert(pos - 2, ", " + nick); return; } } - editable.insert(pos, (Character.isWhitespace(before) ? "" : " ") + nick + (Character.isWhitespace(after) ? "" : " ")); + editable.insert( + pos, + (Character.isWhitespace(before) ? "" : " ") + + nick + + (Character.isWhitespace(after) ? "" : " ")); if (Character.isWhitespace(after)) { - this.binding.textinput.setSelection(this.binding.textinput.getSelectionStart() + 1); + this.binding.textinput.setSelection( + this.binding.textinput.getSelectionStart() + 1); } } } @@ -1985,7 +2300,10 @@ public void onSaveInstanceState(@NotNull Bundle outState) { if (scrollState != null) { outState.putParcelable(STATE_SCROLL_POSITION, scrollState); } - final ArrayList attachments = mediaPreviewAdapter == null ? new ArrayList<>() : mediaPreviewAdapter.getAttachments(); + final ArrayList attachments = + mediaPreviewAdapter == null + ? new ArrayList<>() + : mediaPreviewAdapter.getAttachments(); if (attachments.size() > 0) { outState.putParcelableArrayList(STATE_MEDIA_PREVIEWS, attachments); } @@ -1999,7 +2317,8 @@ public void onActivityCreated(Bundle savedInstanceState) { return; } String uuid = savedInstanceState.getString(STATE_CONVERSATION_UUID); - ArrayList attachments = savedInstanceState.getParcelableArrayList(STATE_MEDIA_PREVIEWS); + ArrayList attachments = + savedInstanceState.getParcelableArrayList(STATE_MEDIA_PREVIEWS); pendingLastMessageUuid.push(savedInstanceState.getString(STATE_LAST_MESSAGE_UUID, null)); if (uuid != null) { QuickLoader.set(uuid); @@ -2024,9 +2343,14 @@ public void onStart() { if (extras != null) { processExtras(extras); } - } else if (conversation == null && activity != null && activity.xmppConnectionService != null) { + } else if (conversation == null + && activity != null + && activity.xmppConnectionService != null) { final String uuid = pendingConversationsUuid.pop(); - Log.d(Config.LOGTAG, "ConversationFragment.onStart() - activity was bound but no conversation loaded. uuid=" + uuid); + Log.d( + Config.LOGTAG, + "ConversationFragment.onStart() - activity was bound but no conversation loaded. uuid=" + + uuid); if (uuid != null) { findAndReInitByUuidOrArchive(uuid); } @@ -2101,7 +2425,8 @@ private boolean reInit(final Conversation conversation, final boolean hasExtras) return false; } this.conversation = conversation; - //once we set the conversation all is good and it will automatically do the right thing in onStart() + // once we set the conversation all is good and it will automatically do the right thing in + // onStart() if (this.activity == null || this.binding == null) { return false; } @@ -2121,12 +2446,16 @@ private boolean reInit(final Conversation conversation, final boolean hasExtras) setupIme(); - final boolean scrolledToBottomAndNoPending = this.scrolledToBottom() && pendingScrollState.peek() == null; + final boolean scrolledToBottomAndNoPending = + this.scrolledToBottom() && pendingScrollState.peek() == null; - this.binding.textSendButton.setContentDescription(activity.getString(R.string.send_message_to_x, conversation.getName())); + this.binding.textSendButton.setContentDescription( + activity.getString(R.string.send_message_to_x, conversation.getName())); this.binding.textinput.setKeyboardListener(null); this.binding.textinput.setText(""); - final boolean participating = conversation.getMode() == Conversational.MODE_SINGLE || conversation.getMucOptions().participating(); + final boolean participating = + conversation.getMode() == Conversational.MODE_SINGLE + || conversation.getMucOptions().participating(); if (participating) { this.binding.textinput.append(this.conversation.getNextMessage()); } @@ -2157,10 +2486,12 @@ private boolean reInit(final Conversation conversation, final boolean hasExtras) } } - this.binding.messagesView.post(this::fireReadEvent); - //TODO if we only do this when this fragment is running on main it won't *bing* in tablet layout which might be unnecessary since we can *see* it - activity.xmppConnectionService.getNotificationService().setOpenConversation(this.conversation); + // TODO if we only do this when this fragment is running on main it won't *bing* in tablet + // layout which might be unnecessary since we can *see* it + activity.xmppConnectionService + .getNotificationService() + .setOpenConversation(this.conversation); return true; } @@ -2180,11 +2511,11 @@ private void hideUnreadMessagesCount() { private void setSelection(int pos, boolean jumpToBottom) { ListViewUtils.setSelection(this.binding.messagesView, pos, jumpToBottom); - this.binding.messagesView.post(() -> ListViewUtils.setSelection(this.binding.messagesView, pos, jumpToBottom)); + this.binding.messagesView.post( + () -> ListViewUtils.setSelection(this.binding.messagesView, pos, jumpToBottom)); this.binding.messagesView.post(this::fireReadEvent); } - private boolean scrolledToBottom() { return this.binding != null && scrolledToBottom(this.binding.messagesView); } @@ -2193,18 +2524,22 @@ private void processExtras(final Bundle extras) { final String downloadUuid = extras.getString(ConversationsActivity.EXTRA_DOWNLOAD_UUID); final String text = extras.getString(Intent.EXTRA_TEXT); final String nick = extras.getString(ConversationsActivity.EXTRA_NICK); - final String postInitAction = extras.getString(ConversationsActivity.EXTRA_POST_INIT_ACTION); + final String postInitAction = + extras.getString(ConversationsActivity.EXTRA_POST_INIT_ACTION); final boolean asQuote = extras.getBoolean(ConversationsActivity.EXTRA_AS_QUOTE); final boolean pm = extras.getBoolean(ConversationsActivity.EXTRA_IS_PRIVATE_MESSAGE, false); - final boolean doNotAppend = extras.getBoolean(ConversationsActivity.EXTRA_DO_NOT_APPEND, false); + final boolean doNotAppend = + extras.getBoolean(ConversationsActivity.EXTRA_DO_NOT_APPEND, false); final String type = extras.getString(ConversationsActivity.EXTRA_TYPE); final List uris = extractUris(extras); if (uris != null && uris.size() > 0) { if (uris.size() == 1 && "geo".equals(uris.get(0).getScheme())) { - mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), uris.get(0), Attachment.Type.LOCATION)); + mediaPreviewAdapter.addMediaPreviews( + Attachment.of(getActivity(), uris.get(0), Attachment.Type.LOCATION)); } else { final List cleanedUris = cleanUris(new ArrayList<>(uris)); - mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), cleanedUris, type)); + mediaPreviewAdapter.addMediaPreviews( + Attachment.of(getActivity(), cleanedUris, type)); } toggleInputMethod(); return; @@ -2216,7 +2551,7 @@ private void processExtras(final Bundle extras) { Jid next = Jid.of(jid.getLocal(), jid.getDomain(), nick); privateMessageWith(next); } catch (final IllegalArgumentException ignored) { - //do nothing + // do nothing } } else { final MucOptions mucOptions = conversation.getMucOptions(); @@ -2226,7 +2561,8 @@ private void processExtras(final Bundle extras) { } } else { if (text != null && GeoHelper.GEO_URI.matcher(text).matches()) { - mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), Uri.parse(text), Attachment.Type.LOCATION)); + mediaPreviewAdapter.addMediaPreviews( + Attachment.of(getActivity(), Uri.parse(text), Attachment.Type.LOCATION)); toggleInputMethod(); return; } else if (text != null && asQuote) { @@ -2239,7 +2575,8 @@ private void processExtras(final Bundle extras) { attachFile(ATTACHMENT_CHOICE_RECORD_VOICE, false); return; } - final Message message = downloadUuid == null ? null : conversation.findMessageWithFileAndUuid(downloadUuid); + final Message message = + downloadUuid == null ? null : conversation.findMessageWithFileAndUuid(downloadUuid); if (message != null) { startDownloadable(message); } @@ -2264,7 +2601,11 @@ private List cleanUris(final List uris) { final Uri uri = iterator.next(); if (FileBackend.weOwnFile(uri)) { iterator.remove(); - Toast.makeText(getActivity(), R.string.security_violation_not_attaching_file, Toast.LENGTH_SHORT).show(); + Toast.makeText( + getActivity(), + R.string.security_violation_not_attaching_file, + Toast.LENGTH_SHORT) + .show(); } } return uris; @@ -2272,27 +2613,37 @@ private List cleanUris(final List uris) { private boolean showBlockSubmenu(View view) { final Jid jid = conversation.getJid(); - final boolean showReject = !conversation.isWithStranger() && conversation.getContact().getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST); + final boolean showReject = + !conversation.isWithStranger() + && conversation + .getContact() + .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST); PopupMenu popupMenu = new PopupMenu(getActivity(), view); popupMenu.inflate(R.menu.block); popupMenu.getMenu().findItem(R.id.block_contact).setVisible(jid.getLocal() != null); popupMenu.getMenu().findItem(R.id.reject).setVisible(showReject); - popupMenu.setOnMenuItemClickListener(menuItem -> { - Blockable blockable; - switch (menuItem.getItemId()) { - case R.id.reject: - activity.xmppConnectionService.stopPresenceUpdatesTo(conversation.getContact()); - updateSnackBar(conversation); + popupMenu.setOnMenuItemClickListener( + menuItem -> { + Blockable blockable; + switch (menuItem.getItemId()) { + case R.id.reject: + activity.xmppConnectionService.stopPresenceUpdatesTo( + conversation.getContact()); + updateSnackBar(conversation); + return true; + case R.id.block_domain: + blockable = + conversation + .getAccount() + .getRoster() + .getContact(jid.getDomain()); + break; + default: + blockable = conversation; + } + BlockContactDialog.show(activity, blockable); return true; - case R.id.block_domain: - blockable = conversation.getAccount().getRoster().getContact(jid.getDomain()); - break; - default: - blockable = conversation; - } - BlockContactDialog.show(activity, blockable); - return true; - }); + }); popupMenu.show(); return true; } @@ -2306,13 +2657,27 @@ private void updateSnackBar(final Conversation conversation) { return; } if (account.getStatus() == Account.State.DISABLED) { - showSnackbar(R.string.this_account_is_disabled, R.string.enable, this.mEnableAccountListener); + showSnackbar( + R.string.this_account_is_disabled, + R.string.enable, + this.mEnableAccountListener); } else if (conversation.isBlocked()) { showSnackbar(R.string.contact_blocked, R.string.unblock, this.mUnblockClickListener); - } else if (contact != null && !contact.showInRoster() && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { - showSnackbar(R.string.contact_added_you, R.string.add_back, this.mAddBackClickListener, this.mLongPressBlockListener); - } else if (contact != null && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { - showSnackbar(R.string.contact_asks_for_presence_subscription, R.string.allow, this.mAllowPresenceSubscription, this.mLongPressBlockListener); + } else if (contact != null + && !contact.showInRoster() + && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { + showSnackbar( + R.string.contact_added_you, + R.string.add_back, + this.mAddBackClickListener, + this.mLongPressBlockListener); + } else if (contact != null + && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { + showSnackbar( + R.string.contact_asks_for_presence_subscription, + R.string.allow, + this.mAllowPresenceSubscription, + this.mLongPressBlockListener); } else if (mode == Conversation.MODE_MULTI && !conversation.getMucOptions().online() && account.getStatus() == Account.State.ONLINE) { @@ -2338,7 +2703,10 @@ private void updateSnackBar(final Conversation conversation) { } break; case PASSWORD_REQUIRED: - showSnackbar(R.string.conference_requires_password, R.string.enter_password, enterPassword); + showSnackbar( + R.string.conference_requires_password, + R.string.enter_password, + enterPassword); break; case BANNED: showSnackbar(R.string.conference_banned, R.string.leave, leaveMuc); @@ -2347,7 +2715,8 @@ private void updateSnackBar(final Conversation conversation) { showSnackbar(R.string.conference_members_only, R.string.leave, leaveMuc); break; case RESOURCE_CONSTRAINT: - showSnackbar(R.string.conference_resource_constraint, R.string.try_again, joinMuc); + showSnackbar( + R.string.conference_resource_constraint, R.string.try_again, joinMuc); break; case KICKED: showSnackbar(R.string.conference_kicked, R.string.join, joinMuc); @@ -2364,7 +2733,10 @@ private void updateSnackBar(final Conversation conversation) { showSnackbar(R.string.conference_destroyed, R.string.leave, leaveMuc); break; case NON_ANONYMOUS: - showSnackbar(R.string.group_chat_will_make_your_jabber_id_public, R.string.join, acceptJoin); + showSnackbar( + R.string.group_chat_will_make_your_jabber_id_public, + R.string.join, + acceptJoin); break; default: hideSnackbar(); @@ -2377,7 +2749,8 @@ private void updateSnackBar(final Conversation conversation) { && conversation.countMessages() != 0 && !conversation.isBlocked() && conversation.isWithStranger()) { - showSnackbar(R.string.received_message_from_stranger, R.string.block, mBlockClickListener); + showSnackbar( + R.string.received_message_from_stranger, R.string.block, mBlockClickListener); } else { hideSnackbar(); } @@ -2386,10 +2759,14 @@ private void updateSnackBar(final Conversation conversation) { @Override public void refresh() { if (this.binding == null) { - Log.d(Config.LOGTAG, "ConversationFragment.refresh() skipped updated because view binding was null"); + Log.d( + Config.LOGTAG, + "ConversationFragment.refresh() skipped updated because view binding was null"); return; } - if (this.conversation != null && this.activity != null && this.activity.xmppConnectionService != null) { + if (this.conversation != null + && this.activity != null + && this.activity.xmppConnectionService != null) { if (!activity.xmppConnectionService.isConversationStillOpen(this.conversation)) { activity.onConversationArchived(this.conversation); return; @@ -2406,7 +2783,8 @@ private void refresh(boolean notifyConversationRead) { updateStatusMessages(); if (conversation.getReceivedMessagesCountSinceUuid(lastMessageUuid) != 0) { binding.unreadCountCustomView.setVisibility(View.VISIBLE); - binding.unreadCountCustomView.setUnreadCount(conversation.getReceivedMessagesCountSinceUuid(lastMessageUuid)); + binding.unreadCountCustomView.setUnreadCount( + conversation.getReceivedMessagesCountSinceUuid(lastMessageUuid)); } this.messageListAdapter.notifyDataSetChanged(); updateChatMsgHint(); @@ -2429,12 +2807,17 @@ protected void messageSent() { storeNextMessage(); updateChatMsgHint(); SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(activity); - final boolean prefScrollToBottom = p.getBoolean("scroll_to_bottom", activity.getResources().getBoolean(R.bool.scroll_to_bottom)); + final boolean prefScrollToBottom = + p.getBoolean( + "scroll_to_bottom", + activity.getResources().getBoolean(R.bool.scroll_to_bottom)); if (prefScrollToBottom || scrolledToBottom()) { - new Handler().post(() -> { - int size = messageList.size(); - this.binding.messagesView.setSelection(size - 1); - }); + new Handler() + .post( + () -> { + int size = messageList.size(); + this.binding.messagesView.setSelection(size - 1); + }); } } @@ -2443,8 +2826,12 @@ private boolean storeNextMessage() { } private boolean storeNextMessage(String msg) { - final boolean participating = conversation.getMode() == Conversational.MODE_SINGLE || conversation.getMucOptions().participating(); - if (this.conversation.getStatus() != Conversation.STATUS_ARCHIVED && participating && this.conversation.setNextMessage(msg)) { + final boolean participating = + conversation.getMode() == Conversational.MODE_SINGLE + || conversation.getMucOptions().participating(); + if (this.conversation.getStatus() != Conversation.STATUS_ARCHIVED + && participating + && this.conversation.setNextMessage(msg)) { this.activity.xmppConnectionService.updateConversation(this.conversation); return true; } @@ -2461,7 +2848,10 @@ public long getMaxHttpUploadSize(Conversation conversation) { } private void updateEditablity() { - boolean canWrite = this.conversation.getMode() == Conversation.MODE_SINGLE || this.conversation.getMucOptions().participating() || this.conversation.getNextCounterpart() != null; + boolean canWrite = + this.conversation.getMode() == Conversation.MODE_SINGLE + || this.conversation.getMucOptions().participating() + || this.conversation.getNextCounterpart() != null; this.binding.textinput.setFocusable(canWrite); this.binding.textinput.setFocusableInTouchMode(canWrite); this.binding.textSendButton.setEnabled(canWrite); @@ -2470,10 +2860,12 @@ private void updateEditablity() { } public void updateSendButton() { - boolean hasAttachments = mediaPreviewAdapter != null && mediaPreviewAdapter.hasAttachments(); + boolean hasAttachments = + mediaPreviewAdapter != null && mediaPreviewAdapter.hasAttachments(); final Conversation c = this.conversation; final Presence.Status status; - final String text = this.binding.textinput == null ? "" : this.binding.textinput.getText().toString(); + final String text = + this.binding.textinput == null ? "" : this.binding.textinput.getText().toString(); final SendButtonAction action; if (hasAttachments) { action = SendButtonAction.TEXT; @@ -2481,12 +2873,17 @@ public void updateSendButton() { action = SendButtonTool.getAction(getActivity(), c, text); } if (c.getAccount().getStatus() == Account.State.ONLINE) { - if (activity != null && activity.xmppConnectionService != null && activity.xmppConnectionService.getMessageArchiveService().isCatchingUp(c)) { + if (activity != null + && activity.xmppConnectionService != null + && activity.xmppConnectionService.getMessageArchiveService().isCatchingUp(c)) { status = Presence.Status.OFFLINE; } else if (c.getMode() == Conversation.MODE_SINGLE) { status = c.getContact().getShownStatus(); } else { - status = c.getMucOptions().online() ? Presence.Status.ONLINE : Presence.Status.OFFLINE; + status = + c.getMucOptions().online() + ? Presence.Status.ONLINE + : Presence.Status.OFFLINE; } } else { status = Presence.Status.OFFLINE; @@ -2494,7 +2891,8 @@ public void updateSendButton() { this.binding.textSendButton.setTag(action); final Activity activity = getActivity(); if (activity != null) { - this.binding.textSendButton.setImageResource(SendButtonTool.getSendButtonImageResource(activity, action, status)); + this.binding.textSendButton.setImageResource( + SendButtonTool.getSendButtonImageResource(activity, action, status)); } } @@ -2506,9 +2904,17 @@ protected void updateStatusMessages() { if (conversation.getMode() == Conversation.MODE_SINGLE) { ChatState state = conversation.getIncomingChatState(); if (state == ChatState.COMPOSING) { - this.messageList.add(Message.createStatusMessage(conversation, getString(R.string.contact_is_typing, conversation.getName()))); + this.messageList.add( + Message.createStatusMessage( + conversation, + getString(R.string.contact_is_typing, conversation.getName()))); } else if (state == ChatState.PAUSED) { - this.messageList.add(Message.createStatusMessage(conversation, getString(R.string.contact_has_stopped_typing, conversation.getName()))); + this.messageList.add( + Message.createStatusMessage( + conversation, + getString( + R.string.contact_has_stopped_typing, + conversation.getName()))); } else { for (int i = this.messageList.size() - 1; i >= 0; --i) { final Message message = this.messageList.get(i); @@ -2517,8 +2923,13 @@ protected void updateStatusMessages() { return; } else { if (message.getStatus() == Message.STATUS_SEND_DISPLAYED) { - this.messageList.add(i + 1, - Message.createStatusMessage(conversation, getString(R.string.contact_has_read_up_to_this_point, conversation.getName()))); + this.messageList.add( + i + 1, + Message.createStatusMessage( + conversation, + getString( + R.string.contact_has_read_up_to_this_point, + conversation.getName()))); return; } } @@ -2530,18 +2941,22 @@ protected void updateStatusMessages() { final List allUsers = mucOptions.getUsers(); final Set addedMarkers = new HashSet<>(); ChatState state = ChatState.COMPOSING; - List users = conversation.getMucOptions().getUsersWithChatState(state, 5); + List users = + conversation.getMucOptions().getUsersWithChatState(state, 5); if (users.size() == 0) { state = ChatState.PAUSED; users = conversation.getMucOptions().getUsersWithChatState(state, 5); } if (mucOptions.isPrivateAndNonAnonymous()) { for (int i = this.messageList.size() - 1; i >= 0; --i) { - final Set markersForMessage = messageList.get(i).getReadByMarkers(); + final Set markersForMessage = + messageList.get(i).getReadByMarkers(); final List shownMarkers = new ArrayList<>(); for (ReadByMarker marker : markersForMessage) { if (!ReadByMarker.contains(marker, addedMarkers)) { - addedMarkers.add(marker); //may be put outside this condition. set should do dedup anyway + addedMarkers.add( + marker); // may be put outside this condition. set should do + // dedup anyway MucOptions.User user = mucOptions.findUser(marker); if (user != null && !users.contains(user)) { shownMarkers.add(user); @@ -2554,16 +2969,29 @@ protected void updateStatusMessages() { if (size > 1) { final String body; if (size <= 4) { - body = getString(R.string.contacts_have_read_up_to_this_point, UIHelper.concatNames(shownMarkers)); - } else if (ReadByMarker.allUsersRepresented(allUsers, markersForMessage, markerForSender)) { + body = + getString( + R.string.contacts_have_read_up_to_this_point, + UIHelper.concatNames(shownMarkers)); + } else if (ReadByMarker.allUsersRepresented( + allUsers, markersForMessage, markerForSender)) { body = getString(R.string.everyone_has_read_up_to_this_point); } else { - body = getString(R.string.contacts_and_n_more_have_read_up_to_this_point, UIHelper.concatNames(shownMarkers, 3), size - 3); + body = + getString( + R.string.contacts_and_n_more_have_read_up_to_this_point, + UIHelper.concatNames(shownMarkers, 3), + size - 3); } statusMessage = Message.createStatusMessage(conversation, body); statusMessage.setCounterparts(shownMarkers); } else if (size == 1) { - statusMessage = Message.createStatusMessage(conversation, getString(R.string.contact_has_read_up_to_this_point, UIHelper.getDisplayName(shownMarkers.get(0)))); + statusMessage = + Message.createStatusMessage( + conversation, + getString( + R.string.contact_has_read_up_to_this_point, + UIHelper.getDisplayName(shownMarkers.get(0)))); statusMessage.setCounterpart(shownMarkers.get(0).getFullJid()); statusMessage.setTrueCounterpart(shownMarkers.get(0).getRealJid()); } else { @@ -2582,18 +3010,27 @@ protected void updateStatusMessages() { Message statusMessage; if (users.size() == 1) { MucOptions.User user = users.get(0); - int id = state == ChatState.COMPOSING ? R.string.contact_is_typing : R.string.contact_has_stopped_typing; - statusMessage = Message.createStatusMessage(conversation, getString(id, UIHelper.getDisplayName(user))); + int id = + state == ChatState.COMPOSING + ? R.string.contact_is_typing + : R.string.contact_has_stopped_typing; + statusMessage = + Message.createStatusMessage( + conversation, getString(id, UIHelper.getDisplayName(user))); statusMessage.setTrueCounterpart(user.getRealJid()); statusMessage.setCounterpart(user.getFullJid()); } else { - int id = state == ChatState.COMPOSING ? R.string.contacts_are_typing : R.string.contacts_have_stopped_typing; - statusMessage = Message.createStatusMessage(conversation, getString(id, UIHelper.concatNames(users))); + int id = + state == ChatState.COMPOSING + ? R.string.contacts_are_typing + : R.string.contacts_have_stopped_typing; + statusMessage = + Message.createStatusMessage( + conversation, getString(id, UIHelper.concatNames(users))); statusMessage.setCounterparts(users); } this.messageList.add(statusMessage); } - } } @@ -2608,8 +3045,14 @@ private boolean showLoadMoreMessages(final Conversation c) { return false; } final boolean mam = hasMamSupport(c) && !c.getContact().isBlocked(); - final MessageArchiveService service = activity.xmppConnectionService.getMessageArchiveService(); - return mam && (c.getLastClearHistory().getTimestamp() != 0 || (c.countMessages() == 0 && c.messagesLoaded.get() && c.hasMessagesLeftOnServer() && !service.queryInProgress(c))); + final MessageArchiveService service = + activity.xmppConnectionService.getMessageArchiveService(); + return mam + && (c.getLastClearHistory().getTimestamp() != 0 + || (c.countMessages() == 0 + && c.messagesLoaded.get() + && c.hasMessagesLeftOnServer() + && !service.queryInProgress(c))); } private boolean hasMamSupport(final Conversation c) { @@ -2621,11 +3064,16 @@ private boolean hasMamSupport(final Conversation c) { } } - protected void showSnackbar(final int message, final int action, final OnClickListener clickListener) { + protected void showSnackbar( + final int message, final int action, final OnClickListener clickListener) { showSnackbar(message, action, clickListener, null); } - protected void showSnackbar(final int message, final int action, final OnClickListener clickListener, final View.OnLongClickListener longClickListener) { + protected void showSnackbar( + final int message, + final int action, + final OnClickListener clickListener, + final View.OnLongClickListener longClickListener) { this.binding.snackbar.setVisibility(View.VISIBLE); this.binding.snackbar.setOnClickListener(null); this.binding.snackbarMessage.setText(message); @@ -2655,7 +3103,8 @@ protected void sendPgpMessage(final Message message) { return; } if (conversation.getAccount().getPgpSignature() == null) { - activity.announcePgp(conversation.getAccount(), conversation, null, activity.onOpenPGPKeyPublished); + activity.announcePgp( + conversation.getAccount(), conversation, null, activity.onOpenPGPKeyPublished); return; } if (!mSendingPgpMessage.compareAndSet(false, true)) { @@ -2663,85 +3112,107 @@ protected void sendPgpMessage(final Message message) { } if (conversation.getMode() == Conversation.MODE_SINGLE) { if (contact.getPgpKeyId() != 0) { - xmppService.getPgpEngine().hasKey(contact, - new UiCallback() { - - @Override - public void userInputRequired(PendingIntent pi, Contact contact) { - startPendingIntent(pi, REQUEST_ENCRYPT_MESSAGE); - } + xmppService + .getPgpEngine() + .hasKey( + contact, + new UiCallback() { + + @Override + public void userInputRequired( + PendingIntent pi, Contact contact) { + startPendingIntent(pi, REQUEST_ENCRYPT_MESSAGE); + } - @Override - public void success(Contact contact) { - encryptTextMessage(message); - } + @Override + public void success(Contact contact) { + encryptTextMessage(message); + } - @Override - public void error(int error, Contact contact) { - activity.runOnUiThread(() -> Toast.makeText(activity, - R.string.unable_to_connect_to_keychain, - Toast.LENGTH_SHORT - ).show()); - mSendingPgpMessage.set(false); - } - }); + @Override + public void error(int error, Contact contact) { + activity.runOnUiThread( + () -> + Toast.makeText( + activity, + R.string + .unable_to_connect_to_keychain, + Toast.LENGTH_SHORT) + .show()); + mSendingPgpMessage.set(false); + } + }); } else { - showNoPGPKeyDialog(false, (dialog, which) -> { - conversation.setNextEncryption(Message.ENCRYPTION_NONE); - xmppService.updateConversation(conversation); - message.setEncryption(Message.ENCRYPTION_NONE); - xmppService.sendMessage(message); - messageSent(); - }); + showNoPGPKeyDialog( + false, + (dialog, which) -> { + conversation.setNextEncryption(Message.ENCRYPTION_NONE); + xmppService.updateConversation(conversation); + message.setEncryption(Message.ENCRYPTION_NONE); + xmppService.sendMessage(message); + messageSent(); + }); } } else { if (conversation.getMucOptions().pgpKeysInUse()) { if (!conversation.getMucOptions().everybodyHasKeys()) { - Toast warning = Toast - .makeText(getActivity(), - R.string.missing_public_keys, - Toast.LENGTH_LONG); + Toast warning = + Toast.makeText( + getActivity(), R.string.missing_public_keys, Toast.LENGTH_LONG); warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0); warning.show(); } encryptTextMessage(message); } else { - showNoPGPKeyDialog(true, (dialog, which) -> { - conversation.setNextEncryption(Message.ENCRYPTION_NONE); - message.setEncryption(Message.ENCRYPTION_NONE); - xmppService.updateConversation(conversation); - xmppService.sendMessage(message); - messageSent(); - }); + showNoPGPKeyDialog( + true, + (dialog, which) -> { + conversation.setNextEncryption(Message.ENCRYPTION_NONE); + message.setEncryption(Message.ENCRYPTION_NONE); + xmppService.updateConversation(conversation); + xmppService.sendMessage(message); + messageSent(); + }); } } } public void encryptTextMessage(Message message) { - activity.xmppConnectionService.getPgpEngine().encrypt(message, - new UiCallback() { + activity.xmppConnectionService + .getPgpEngine() + .encrypt( + message, + new UiCallback() { - @Override - public void userInputRequired(PendingIntent pi, Message message) { - startPendingIntent(pi, REQUEST_SEND_MESSAGE); - } + @Override + public void userInputRequired(PendingIntent pi, Message message) { + startPendingIntent(pi, REQUEST_SEND_MESSAGE); + } - @Override - public void success(Message message) { - //TODO the following two call can be made before the callback - getActivity().runOnUiThread(() -> messageSent()); - } + @Override + public void success(Message message) { + // TODO the following two call can be made before the callback + getActivity().runOnUiThread(() -> messageSent()); + } - @Override - public void error(final int error, Message message) { - getActivity().runOnUiThread(() -> { - doneSendingPgpMessage(); - Toast.makeText(getActivity(), error == 0 ? R.string.unable_to_connect_to_keychain : error, Toast.LENGTH_SHORT).show(); + @Override + public void error(final int error, Message message) { + getActivity() + .runOnUiThread( + () -> { + doneSendingPgpMessage(); + Toast.makeText( + getActivity(), + error == 0 + ? R.string + .unable_to_connect_to_keychain + : error, + Toast.LENGTH_SHORT) + .show(); + }); + } }); - - } - }); } public void showNoPGPKeyDialog(boolean plural, DialogInterface.OnClickListener listener) { @@ -2766,12 +3237,14 @@ public void appendText(String text, final boolean doNotAppend) { final Editable editable = this.binding.textinput.getText(); String previous = editable == null ? "" : editable.toString(); if (doNotAppend && !TextUtils.isEmpty(previous)) { - Toast.makeText(getActivity(), R.string.already_drafting_message, Toast.LENGTH_LONG).show(); + Toast.makeText(getActivity(), R.string.already_drafting_message, Toast.LENGTH_LONG) + .show(); return; } if (UIHelper.isLastLineQuote(previous)) { text = '\n' + text; - } else if (previous.length() != 0 && !Character.isWhitespace(previous.charAt(previous.length() - 1))) { + } else if (previous.length() != 0 + && !Character.isWhitespace(previous.charAt(previous.length() - 1))) { text = " " + text; } this.binding.textinput.append(text); @@ -2792,24 +3265,28 @@ private boolean enterIsSend() { } public boolean onArrowUpCtrlPressed() { - final Message lastEditableMessage = conversation == null ? null : conversation.getLastEditableMessage(); + final Message lastEditableMessage = + conversation == null ? null : conversation.getLastEditableMessage(); if (lastEditableMessage != null) { correctMessage(lastEditableMessage); return true; } else { - Toast.makeText(getActivity(), R.string.could_not_correct_message, Toast.LENGTH_LONG).show(); + Toast.makeText(getActivity(), R.string.could_not_correct_message, Toast.LENGTH_LONG) + .show(); return false; } } @Override public void onTypingStarted() { - final XmppConnectionService service = activity == null ? null : activity.xmppConnectionService; + final XmppConnectionService service = + activity == null ? null : activity.xmppConnectionService; if (service == null) { return; } final Account.State status = conversation.getAccount().getStatus(); - if (status == Account.State.ONLINE && conversation.setOutgoingChatState(ChatState.COMPOSING)) { + if (status == Account.State.ONLINE + && conversation.setOutgoingChatState(ChatState.COMPOSING)) { service.sendChatState(conversation); } runOnUiThread(this::updateSendButton); @@ -2817,7 +3294,8 @@ public void onTypingStarted() { @Override public void onTypingStopped() { - final XmppConnectionService service = activity == null ? null : activity.xmppConnectionService; + final XmppConnectionService service = + activity == null ? null : activity.xmppConnectionService; if (service == null) { return; } @@ -2829,21 +3307,24 @@ public void onTypingStopped() { @Override public void onTextDeleted() { - final XmppConnectionService service = activity == null ? null : activity.xmppConnectionService; + final XmppConnectionService service = + activity == null ? null : activity.xmppConnectionService; if (service == null) { return; } final Account.State status = conversation.getAccount().getStatus(); - if (status == Account.State.ONLINE && conversation.setOutgoingChatState(Config.DEFAULT_CHAT_STATE)) { + if (status == Account.State.ONLINE + && conversation.setOutgoingChatState(Config.DEFAULT_CHAT_STATE)) { service.sendChatState(conversation); } if (storeNextMessage()) { - runOnUiThread(() -> { - if (activity == null) { - return; - } - activity.onConversationsListItemUpdated(); - }); + runOnUiThread( + () -> { + if (activity == null) { + return; + } + activity.onConversationsListItemUpdated(); + }); } runOnUiThread(this::updateSendButton); } @@ -2867,7 +3348,10 @@ public boolean onTabPressed(boolean repeated) { completionIndex = 0; final String content = this.binding.textinput.getText().toString(); lastCompletionCursor = this.binding.textinput.getSelectionEnd(); - int start = lastCompletionCursor > 0 ? content.lastIndexOf(" ", lastCompletionCursor - 1) + 1 : 0; + int start = + lastCompletionCursor > 0 + ? content.lastIndexOf(" ", lastCompletionCursor - 1) + 1 + : 0; firstWord = start == 0; incomplete = content.substring(start, lastCompletionCursor); } @@ -2881,12 +3365,18 @@ public boolean onTabPressed(boolean repeated) { Collections.sort(completions); if (completions.size() > completionIndex) { String completion = completions.get(completionIndex).substring(incomplete.length()); - this.binding.textinput.getEditableText().delete(lastCompletionCursor, lastCompletionCursor + lastCompletionLength); + this.binding + .textinput + .getEditableText() + .delete(lastCompletionCursor, lastCompletionCursor + lastCompletionLength); this.binding.textinput.getEditableText().insert(lastCompletionCursor, completion); lastCompletionLength = completion.length(); } else { completionIndex = -1; - this.binding.textinput.getEditableText().delete(lastCompletionCursor, lastCompletionCursor + lastCompletionLength); + this.binding + .textinput + .getEditableText() + .delete(lastCompletionCursor, lastCompletionCursor + lastCompletionLength); lastCompletionLength = 0; } return true; @@ -2894,7 +3384,9 @@ public boolean onTabPressed(boolean repeated) { private void startPendingIntent(PendingIntent pendingIntent, int requestCode) { try { - getActivity().startIntentSenderForResult(pendingIntent.getIntentSender(), requestCode, null, 0, 0, 0); + getActivity() + .startIntentSenderForResult( + pendingIntent.getIntentSender(), requestCode, null, 0, 0, 0); } catch (final SendIntentException ignored) { } } @@ -2968,63 +3460,85 @@ public Conversation getConversation() { @Override public void onContactPictureLongClicked(View v, final Message message) { final String fingerprint; - if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + if (message.getEncryption() == Message.ENCRYPTION_PGP + || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { fingerprint = "pgp"; } else { fingerprint = message.getFingerprint(); } final PopupMenu popupMenu = new PopupMenu(getActivity(), v); final Contact contact = message.getContact(); - if (message.getStatus() <= Message.STATUS_RECEIVED && (contact == null || !contact.isSelf())) { + if (message.getStatus() <= Message.STATUS_RECEIVED + && (contact == null || !contact.isSelf())) { if (message.getConversation().getMode() == Conversation.MODE_MULTI) { final Jid cp = message.getCounterpart(); if (cp == null || cp.isBareJid()) { return; } final Jid tcp = message.getTrueCounterpart(); - final User userByRealJid = tcp != null ? conversation.getMucOptions().findOrCreateUserByRealJid(tcp, cp) : null; - final User user = userByRealJid != null ? userByRealJid : conversation.getMucOptions().findUserByFullJid(cp); + final User userByRealJid = + tcp != null + ? conversation.getMucOptions().findOrCreateUserByRealJid(tcp, cp) + : null; + final User user = + userByRealJid != null + ? userByRealJid + : conversation.getMucOptions().findUserByFullJid(cp); popupMenu.inflate(R.menu.muc_details_context); final Menu menu = popupMenu.getMenu(); - MucDetailsContextMenuHelper.configureMucDetailsContextMenu(activity, menu, conversation, user); - popupMenu.setOnMenuItemClickListener(menuItem -> MucDetailsContextMenuHelper.onContextItemSelected(menuItem, user, activity, fingerprint)); + MucDetailsContextMenuHelper.configureMucDetailsContextMenu( + activity, menu, conversation, user); + popupMenu.setOnMenuItemClickListener( + menuItem -> + MucDetailsContextMenuHelper.onContextItemSelected( + menuItem, user, activity, fingerprint)); } else { popupMenu.inflate(R.menu.one_on_one_context); - popupMenu.setOnMenuItemClickListener(item -> { - switch (item.getItemId()) { - case R.id.action_contact_details: - activity.switchToContactDetails(message.getContact(), fingerprint); - break; - case R.id.action_show_qr_code: - activity.showQrCode("xmpp:" + message.getContact().getJid().asBareJid().toEscapedString()); - break; - } - return true; - }); + popupMenu.setOnMenuItemClickListener( + item -> { + switch (item.getItemId()) { + case R.id.action_contact_details: + activity.switchToContactDetails( + message.getContact(), fingerprint); + break; + case R.id.action_show_qr_code: + activity.showQrCode( + "xmpp:" + + message.getContact() + .getJid() + .asBareJid() + .toEscapedString()); + break; + } + return true; + }); } } else { popupMenu.inflate(R.menu.account_context); final Menu menu = popupMenu.getMenu(); - menu.findItem(R.id.action_manage_accounts).setVisible(QuickConversationsService.isConversations()); - popupMenu.setOnMenuItemClickListener(item -> { - final XmppActivity activity = this.activity; - if (activity == null) { - Log.e(Config.LOGTAG,"Unable to perform action. no context provided"); - return true; - } - switch (item.getItemId()) { - case R.id.action_show_qr_code: - activity.showQrCode(conversation.getAccount().getShareableUri()); - break; - case R.id.action_account_details: - activity.switchToAccount(message.getConversation().getAccount(), fingerprint); - break; - case R.id.action_manage_accounts: - AccountUtils.launchManageAccounts(activity); - break; - } - return true; - }); + menu.findItem(R.id.action_manage_accounts) + .setVisible(QuickConversationsService.isConversations()); + popupMenu.setOnMenuItemClickListener( + item -> { + final XmppActivity activity = this.activity; + if (activity == null) { + Log.e(Config.LOGTAG, "Unable to perform action. no context provided"); + return true; + } + switch (item.getItemId()) { + case R.id.action_show_qr_code: + activity.showQrCode(conversation.getAccount().getShareableUri()); + break; + case R.id.action_account_details: + activity.switchToAccount( + message.getConversation().getAccount(), fingerprint); + break; + case R.id.action_manage_accounts: + AccountUtils.launchManageAccounts(activity); + break; + } + return true; + }); } popupMenu.show(); } @@ -3032,25 +3546,43 @@ public void onContactPictureLongClicked(View v, final Message message) { @Override public void onContactPictureClicked(Message message) { String fingerprint; - if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + if (message.getEncryption() == Message.ENCRYPTION_PGP + || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { fingerprint = "pgp"; } else { fingerprint = message.getFingerprint(); } final boolean received = message.getStatus() <= Message.STATUS_RECEIVED; if (received) { - if (message.getConversation() instanceof Conversation && message.getConversation().getMode() == Conversation.MODE_MULTI) { + if (message.getConversation() instanceof Conversation + && message.getConversation().getMode() == Conversation.MODE_MULTI) { Jid tcp = message.getTrueCounterpart(); Jid user = message.getCounterpart(); if (user != null && !user.isBareJid()) { - final MucOptions mucOptions = ((Conversation) message.getConversation()).getMucOptions(); - if (mucOptions.participating() || ((Conversation) message.getConversation()).getNextCounterpart() != null) { - if (!mucOptions.isUserInRoom(user) && mucOptions.findUserByRealJid(tcp == null ? null : tcp.asBareJid()) == null) { - Toast.makeText(getActivity(), activity.getString(R.string.user_has_left_conference, user.getResource()), Toast.LENGTH_SHORT).show(); + final MucOptions mucOptions = + ((Conversation) message.getConversation()).getMucOptions(); + if (mucOptions.participating() + || ((Conversation) message.getConversation()).getNextCounterpart() + != null) { + if (!mucOptions.isUserInRoom(user) + && mucOptions.findUserByRealJid( + tcp == null ? null : tcp.asBareJid()) + == null) { + Toast.makeText( + getActivity(), + activity.getString( + R.string.user_has_left_conference, + user.getResource()), + Toast.LENGTH_SHORT) + .show(); } highlightInConference(user.getResource()); } else { - Toast.makeText(getActivity(), R.string.you_are_not_participating, Toast.LENGTH_SHORT).show(); + Toast.makeText( + getActivity(), + R.string.you_are_not_participating, + Toast.LENGTH_SHORT) + .show(); } } return; diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 0e286c8dc..fc7b50449 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -272,14 +272,16 @@ private void acceptCall(View view) { } private void requestPermissionsAndAcceptCall() { - final List permissions; + final ImmutableList.Builder permissions = ImmutableList.builder(); if (getMedia().contains(Media.VIDEO)) { - permissions = - ImmutableList.of(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO); + permissions.add(Manifest.permission.CAMERA).add(Manifest.permission.RECORD_AUDIO); } else { - permissions = ImmutableList.of(Manifest.permission.RECORD_AUDIO); + permissions.add(Manifest.permission.RECORD_AUDIO); } - if (PermissionUtils.hasPermission(this, permissions, REQUEST_ACCEPT_CALL)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + permissions.add(Manifest.permission.BLUETOOTH_CONNECT); + } + if (PermissionUtils.hasPermission(this, permissions.build(), REQUEST_ACCEPT_CALL)) { putScreenInCallMode(); checkRecorderAndAcceptCall(); } @@ -491,13 +493,16 @@ private void proposeJingleRtpSession( public void onRequestPermissionsResult( int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); - if (PermissionUtils.allGranted(grantResults)) { + final PermissionUtils.PermissionResult permissionResult = + PermissionUtils.removeBluetoothConnect(permissions, grantResults); + if (PermissionUtils.allGranted(permissionResult.grantResults)) { if (requestCode == REQUEST_ACCEPT_CALL) { checkRecorderAndAcceptCall(); } } else { @StringRes int res; - final String firstDenied = getFirstDenied(grantResults, permissions); + final String firstDenied = + getFirstDenied(permissionResult.grantResults, permissionResult.permissions); if (Manifest.permission.RECORD_AUDIO.equals(firstDenied)) { res = R.string.no_microphone_permission; } else if (Manifest.permission.CAMERA.equals(firstDenied)) { diff --git a/src/main/java/eu/siacs/conversations/utils/PermissionUtils.java b/src/main/java/eu/siacs/conversations/utils/PermissionUtils.java index 80b58d8cb..004676156 100644 --- a/src/main/java/eu/siacs/conversations/utils/PermissionUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/PermissionUtils.java @@ -8,7 +8,9 @@ import androidx.core.app.ActivityCompat; import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Ints; +import java.util.ArrayList; import java.util.List; public class PermissionUtils { @@ -40,11 +42,41 @@ public static String getFirstDenied(int[] grantResults, String[] permissions) { return null; } - public static boolean hasPermission(final Activity activity, final List permissions, final int requestCode) { + public static class PermissionResult { + public final String[] permissions; + public final int[] grantResults; + + public PermissionResult(String[] permissions, int[] grantResults) { + this.permissions = permissions; + this.grantResults = grantResults; + } + } + + public static PermissionResult removeBluetoothConnect( + final String[] inPermissions, final int[] inGrantResults) { + final List outPermissions = new ArrayList<>(); + final List outGrantResults = new ArrayList<>(); + for (int i = 0; i < Math.min(inPermissions.length, inGrantResults.length); ++i) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (inPermissions[i].equals(Manifest.permission.BLUETOOTH_CONNECT)) { + continue; + } + } + outPermissions.add(inPermissions[i]); + outGrantResults.add(inGrantResults[i]); + } + + return new PermissionResult( + outPermissions.toArray(new String[0]), Ints.toArray(outGrantResults)); + } + + public static boolean hasPermission( + final Activity activity, final List permissions, final int requestCode) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { final ImmutableList.Builder missingPermissions = new ImmutableList.Builder<>(); for (final String permission : permissions) { - if (ActivityCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) { + if (ActivityCompat.checkSelfPermission(activity, permission) + != PackageManager.PERMISSION_GRANTED) { missingPermissions.add(permission); } } @@ -52,7 +84,8 @@ public static boolean hasPermission(final Activity activity, final List if (missing.size() == 0) { return true; } - ActivityCompat.requestPermissions(activity, missing.toArray(new String[0]), requestCode); + ActivityCompat.requestPermissions( + activity, missing.toArray(new String[0]), requestCode); return false; } else { return true; From b3a3f2b9308deadd5ab1af55e1e66b0e7b704ba5 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 9 Aug 2022 09:40:01 +0200 Subject: [PATCH 138/394] try to detect if a container contains video or audio fixes #4321 --- .../persistance/FileBackend.java | 31 +++- .../services/XmppConnectionService.java | 6 +- .../ui/adapter/ConversationAdapter.java | 153 +++++++++++++----- .../siacs/conversations/utils/MimeUtils.java | 18 ++- .../siacs/conversations/utils/UIHelper.java | 4 +- src/main/res/values/strings.xml | 1 + 6 files changed, 159 insertions(+), 54 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index faeca6308..c3093f2cf 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -1496,6 +1496,7 @@ public void updateFileParams(Message message, String url) { DownloadableFile file = getFile(message); final String mime = file.getMimeType(); final boolean privateMessage = message.isPrivateMessage(); + final boolean ambiguous = MimeUtils.AMBIGUOUS_CONTAINER_FORMATS.contains(mime); final boolean image = message.getType() == Message.TYPE_IMAGE || (mime != null && mime.startsWith("image/")); @@ -1507,7 +1508,21 @@ public void updateFileParams(Message message, String url) { body.append(url); } body.append('|').append(file.getSize()); - if (image || video || pdf) { + if (ambiguous) { + try { + final Dimensions dimensions = getVideoDimensions(file); + if (dimensions.valid()) { + Log.d(Config.LOGTAG,"ambiguous file "+mime+" is video"); + body.append('|').append(dimensions.width).append('|').append(dimensions.height); + } else { + Log.d(Config.LOGTAG,"ambiguous file "+mime+" is audio"); + body.append("|0|0|").append(getMediaRuntime(file)); + } + } catch (final NotAVideoFile e) { + Log.d(Config.LOGTAG,"ambiguous file "+mime+" is audio"); + body.append("|0|0|").append(getMediaRuntime(file)); + } + } else if (image || video || pdf) { try { final Dimensions dimensions; if (video) { @@ -1537,14 +1552,16 @@ public void updateFileParams(Message message, String url) { : (image ? Message.TYPE_IMAGE : Message.TYPE_FILE)); } - private int getMediaRuntime(File file) { + private int getMediaRuntime(final File file) { try { - MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); + final MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); mediaMetadataRetriever.setDataSource(file.toString()); - return Integer.parseInt( - mediaMetadataRetriever.extractMetadata( - MediaMetadataRetriever.METADATA_KEY_DURATION)); - } catch (RuntimeException e) { + final String value = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + if (Strings.isNullOrEmpty(value)) { + return 0; + } + return Integer.parseInt(value); + } catch (NumberFormatException e) { return 0; } } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index a6823e670..b15d9bccb 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1384,6 +1384,7 @@ private void schedulePostConnectivityChange() { final Intent intent = new Intent(this, EventReceiver.class); intent.setAction(ACTION_POST_CONNECTIVITY_CHANGE); try { + //TODO add immutable flag final PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 1, intent, 0); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { alarmManager.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, pendingIntent); @@ -1430,7 +1431,8 @@ private void scheduleNextIdlePing() { final Intent intent = new Intent(this, EventReceiver.class); intent.setAction(ACTION_IDLE_PING); try { - PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0); + //TODO add immutable flag + final PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0); alarmManager.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingIntent); } catch (RuntimeException e) { Log.d(Config.LOGTAG, "unable to schedule alarm for idle ping", e); @@ -1443,7 +1445,7 @@ public XmppConnection createConnection(final Account account) { connection.setOnStatusChangedListener(this.statusListener); connection.setOnPresencePacketReceivedListener(this.mPresenceParser); connection.setOnUnregisteredIqPacketReceivedListener(this.mIqParser); - connection.setOnJinglePacketReceivedListener(((a, jp) -> mJingleConnectionManager.deliverPacket(a, jp))); + connection.setOnJinglePacketReceivedListener((mJingleConnectionManager::deliverPacket)); connection.setOnBindListener(this.mOnBindListener); connection.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener); connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java index 662120d84..9822ac004 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java @@ -11,6 +11,7 @@ import androidx.recyclerview.widget.RecyclerView; import com.google.common.base.Optional; +import com.google.common.base.Strings; import java.util.List; @@ -24,11 +25,13 @@ import eu.siacs.conversations.ui.util.AvatarWorkerTask; import eu.siacs.conversations.ui.util.StyledAttributes; import eu.siacs.conversations.utils.IrregularUnicodeDetector; +import eu.siacs.conversations.utils.MimeUtils; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession; -public class ConversationAdapter extends RecyclerView.Adapter { +public class ConversationAdapter + extends RecyclerView.Adapter { private final XmppActivity activity; private final List conversations; @@ -39,11 +42,15 @@ public ConversationAdapter(XmppActivity activity, List conversatio this.conversations = conversations; } - @NonNull @Override public ConversationViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - return new ConversationViewHolder(DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.conversation_list_row, parent, false)); + return new ConversationViewHolder( + DataBindingUtil.inflate( + LayoutInflater.from(parent.getContext()), + R.layout.conversation_list_row, + parent, + false)); } @Override @@ -54,15 +61,18 @@ public void onBindViewHolder(@NonNull ConversationViewHolder viewHolder, int pos } CharSequence name = conversation.getName(); if (name instanceof Jid) { - viewHolder.binding.conversationName.setText(IrregularUnicodeDetector.style(activity, (Jid) name)); + viewHolder.binding.conversationName.setText( + IrregularUnicodeDetector.style(activity, (Jid) name)); } else { viewHolder.binding.conversationName.setText(name); } if (conversation == ConversationFragment.getConversation(activity)) { - viewHolder.binding.frame.setBackgroundColor(StyledAttributes.getColor(activity, R.attr.color_background_tertiary)); + viewHolder.binding.frame.setBackgroundColor( + StyledAttributes.getColor(activity, R.attr.color_background_tertiary)); } else { - viewHolder.binding.frame.setBackgroundColor(StyledAttributes.getColor(activity, R.attr.color_background_primary)); + viewHolder.binding.frame.setBackgroundColor( + StyledAttributes.getColor(activity, R.attr.color_background_primary)); } Message message = conversation.getLatestMessage(); @@ -92,31 +102,70 @@ public void onBindViewHolder(@NonNull ConversationViewHolder viewHolder, int pos } else { final boolean fileAvailable = !message.isDeleted(); final boolean showPreviewText; - if (fileAvailable && (message.isFileOrImage() || message.treatAsDownloadable() || message.isGeoUri())) { + if (fileAvailable + && (message.isFileOrImage() + || message.treatAsDownloadable() + || message.isGeoUri())) { final int imageResource; if (message.isGeoUri()) { - imageResource = activity.getThemeResource(R.attr.ic_attach_location, R.drawable.ic_attach_location); + imageResource = + activity.getThemeResource( + R.attr.ic_attach_location, R.drawable.ic_attach_location); showPreviewText = false; } else { - //TODO move this into static MediaPreview method and use same icons as in MediaAdapter + // TODO move this into static MediaPreview method and use same icons as in + // MediaAdapter final String mime = message.getMimeType(); - switch (mime == null ? "" : mime.split("/")[0]) { - case "image": - imageResource = activity.getThemeResource(R.attr.ic_attach_photo, R.drawable.ic_attach_photo); - showPreviewText = false; - break; - case "video": - imageResource = activity.getThemeResource(R.attr.ic_attach_videocam, R.drawable.ic_attach_videocam); + if (MimeUtils.AMBIGUOUS_CONTAINER_FORMATS.contains(mime)) { + final Message.FileParams fileParams = message.getFileParams(); + if (fileParams.width > 0 && fileParams.height > 0) { + imageResource = + activity.getThemeResource( + R.attr.ic_attach_videocam, + R.drawable.ic_attach_videocam); showPreviewText = false; - break; - case "audio": - imageResource = activity.getThemeResource(R.attr.ic_attach_record, R.drawable.ic_attach_record); + } else if (fileParams.runtime > 0) { + imageResource = + activity.getThemeResource( + R.attr.ic_attach_record, R.drawable.ic_attach_record); showPreviewText = false; - break; - default: - imageResource = activity.getThemeResource(R.attr.ic_attach_document, R.drawable.ic_attach_document); + } else { + imageResource = + activity.getThemeResource( + R.attr.ic_attach_document, + R.drawable.ic_attach_document); showPreviewText = true; - break; + } + } else { + switch (Strings.nullToEmpty(mime).split("/")[0]) { + case "image": + imageResource = + activity.getThemeResource( + R.attr.ic_attach_photo, R.drawable.ic_attach_photo); + showPreviewText = false; + break; + case "video": + imageResource = + activity.getThemeResource( + R.attr.ic_attach_videocam, + R.drawable.ic_attach_videocam); + showPreviewText = false; + break; + case "audio": + imageResource = + activity.getThemeResource( + R.attr.ic_attach_record, + R.drawable.ic_attach_record); + showPreviewText = false; + break; + default: + imageResource = + activity.getThemeResource( + R.attr.ic_attach_document, + R.drawable.ic_attach_document); + showPreviewText = true; + break; + } } } viewHolder.binding.conversationLastmsgImg.setImageResource(imageResource); @@ -125,13 +174,18 @@ public void onBindViewHolder(@NonNull ConversationViewHolder viewHolder, int pos viewHolder.binding.conversationLastmsgImg.setVisibility(View.GONE); showPreviewText = true; } - final Pair preview = UIHelper.getMessagePreview(activity, message, viewHolder.binding.conversationLastmsg.getCurrentTextColor()); + final Pair preview = + UIHelper.getMessagePreview( + activity, + message, + viewHolder.binding.conversationLastmsg.getCurrentTextColor()); if (showPreviewText) { viewHolder.binding.conversationLastmsg.setText(UIHelper.shorten(preview.first)); } else { viewHolder.binding.conversationLastmsgImg.setContentDescription(preview.first); } - viewHolder.binding.conversationLastmsg.setVisibility(showPreviewText ? View.VISIBLE : View.GONE); + viewHolder.binding.conversationLastmsg.setVisibility( + showPreviewText ? View.VISIBLE : View.GONE); if (preview.second) { if (isRead) { viewHolder.binding.conversationLastmsg.setTypeface(null, Typeface.ITALIC); @@ -152,7 +206,8 @@ public void onBindViewHolder(@NonNull ConversationViewHolder viewHolder, int pos if (message.getStatus() == Message.STATUS_RECEIVED) { if (conversation.getMode() == Conversation.MODE_MULTI) { viewHolder.binding.senderName.setVisibility(View.VISIBLE); - viewHolder.binding.senderName.setText(UIHelper.getMessageDisplayName(message).split("\\s+")[0] + ':'); + viewHolder.binding.senderName.setText( + UIHelper.getMessageDisplayName(message).split("\\s+")[0] + ':'); } else { viewHolder.binding.senderName.setVisibility(View.GONE); } @@ -164,33 +219,47 @@ public void onBindViewHolder(@NonNull ConversationViewHolder viewHolder, int pos } } - final Optional ongoingCall; if (conversation.getMode() == Conversational.MODE_MULTI) { ongoingCall = Optional.absent(); } else { - ongoingCall = activity.xmppConnectionService.getJingleConnectionManager().getOngoingRtpConnection(conversation.getContact()); + ongoingCall = + activity.xmppConnectionService + .getJingleConnectionManager() + .getOngoingRtpConnection(conversation.getContact()); } if (ongoingCall.isPresent()) { viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE); - final int ic_ongoing_call = activity.getThemeResource(R.attr.ic_ongoing_call_hint, R.drawable.ic_phone_in_talk_black_18dp); - viewHolder.binding.notificationStatus.setImageResource(ic_ongoing_call); + final int ic_ongoing_call = + activity.getThemeResource( + R.attr.ic_ongoing_call_hint, R.drawable.ic_phone_in_talk_black_18dp); + viewHolder.binding.notificationStatus.setImageResource(ic_ongoing_call); } else { - final long muted_till = conversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL, 0); + final long muted_till = + conversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL, 0); if (muted_till == Long.MAX_VALUE) { viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE); - int ic_notifications_off = activity.getThemeResource(R.attr.icon_notifications_off, R.drawable.ic_notifications_off_black_24dp); + int ic_notifications_off = + activity.getThemeResource( + R.attr.icon_notifications_off, + R.drawable.ic_notifications_off_black_24dp); viewHolder.binding.notificationStatus.setImageResource(ic_notifications_off); } else if (muted_till >= System.currentTimeMillis()) { viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE); - int ic_notifications_paused = activity.getThemeResource(R.attr.icon_notifications_paused, R.drawable.ic_notifications_paused_black_24dp); + int ic_notifications_paused = + activity.getThemeResource( + R.attr.icon_notifications_paused, + R.drawable.ic_notifications_paused_black_24dp); viewHolder.binding.notificationStatus.setImageResource(ic_notifications_paused); } else if (conversation.alwaysNotify()) { viewHolder.binding.notificationStatus.setVisibility(View.GONE); } else { viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE); - int ic_notifications_none = activity.getThemeResource(R.attr.icon_notifications_none, R.drawable.ic_notifications_none_black_24dp); + int ic_notifications_none = + activity.getThemeResource( + R.attr.icon_notifications_none, + R.drawable.ic_notifications_none_black_24dp); viewHolder.binding.notificationStatus.setImageResource(ic_notifications_none); } } @@ -201,9 +270,16 @@ public void onBindViewHolder(@NonNull ConversationViewHolder viewHolder, int pos } else { timestamp = conversation.getLatestMessage().getTimeSent(); } - viewHolder.binding.pinnedOnTop.setVisibility(conversation.getBooleanAttribute(Conversation.ATTRIBUTE_PINNED_ON_TOP,false) ? View.VISIBLE : View.GONE); - viewHolder.binding.conversationLastupdate.setText(UIHelper.readableTimeDifference(activity, timestamp)); - AvatarWorkerTask.loadAvatar(conversation, viewHolder.binding.conversationImage, R.dimen.avatar_on_conversation_overview); + viewHolder.binding.pinnedOnTop.setVisibility( + conversation.getBooleanAttribute(Conversation.ATTRIBUTE_PINNED_ON_TOP, false) + ? View.VISIBLE + : View.GONE); + viewHolder.binding.conversationLastupdate.setText( + UIHelper.readableTimeDifference(activity, timestamp)); + AvatarWorkerTask.loadAvatar( + conversation, + viewHolder.binding.conversationImage, + R.dimen.avatar_on_conversation_overview); viewHolder.itemView.setOnClickListener(v -> listener.onConversationClick(v, conversation)); } @@ -216,7 +292,6 @@ public void setConversationClickListener(OnConversationClickListener listener) { this.listener = listener; } - public void insert(Conversation c, int position) { conversations.add(position, c); notifyDataSetChanged(); @@ -238,7 +313,5 @@ private ConversationViewHolder(ConversationListRowBinding binding) { super(binding.getRoot()); this.binding = binding; } - } - } diff --git a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java index 90f27f65f..d112a9224 100644 --- a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java @@ -22,12 +22,14 @@ import android.util.Log; import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Properties; @@ -40,6 +42,13 @@ * Used to implement java.net.URLConnection and android.webkit.MimeTypeMap. */ public final class MimeUtils { + + public static final List AMBIGUOUS_CONTAINER_FORMATS = ImmutableList.of( + "application/ogg", + "video/3gpp", // .3gp files can contain audio, video or both + "video/3gpp2" + ); + private static final Map mimeTypeToExtensionMap = new HashMap<>(); private static final Map extensionToMimeTypeMap = new HashMap<>(); @@ -225,7 +234,12 @@ public final class MimeUtils { add("application/x-xcf", "xcf"); add("application/x-xfig", "fig"); add("application/xhtml+xml", "xhtml"); + add("video/3gpp", "3gpp"); + add("video/3gpp", "3gp"); + add("video/3gpp2", "3gpp2"); + add("video/3gpp2", "3g2"); add("audio/3gpp", "3gpp"); + add("audio/3gpp", "3gp"); add("audio/aac", "aac"); add("audio/aac-adts", "aac"); add("audio/amr", "amr"); @@ -365,10 +379,6 @@ public final class MimeUtils { add("text/x-tex", "cls"); add("text/x-vcalendar", "vcs"); add("text/x-vcard", "vcf"); - add("video/3gpp", "3gpp"); - add("video/3gpp", "3gp"); - add("video/3gpp2", "3gpp2"); - add("video/3gpp2", "3g2"); add("video/avi", "avi"); add("video/dl", "dl"); add("video/dv", "dif"); diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java index 26732b501..b70bfc558 100644 --- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java @@ -477,8 +477,10 @@ public static String concatNames(List users, int max) { public static String getFileDescriptionString(final Context context, final Message message) { final String mime = message.getMimeType(); - if (mime == null) { + if (Strings.isNullOrEmpty(mime)) { return context.getString(R.string.file); + } else if (MimeUtils.AMBIGUOUS_CONTAINER_FORMATS.contains(mime)) { + return context.getString(R.string.multimedia_file); } else if (mime.startsWith("audio/")) { return context.getString(R.string.audio); } else if (mime.startsWith("video/")) { diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index ee5cbce81..8b1fd1de3 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -415,6 +415,7 @@ video image vector graphic + multimedia file PDF document Android App Contact From cc80a2a758be3953a149b5bcc55064bd8d31d5fd Mon Sep 17 00:00:00 2001 From: Licaon_Kter Date: Tue, 9 Aug 2022 12:34:39 +0000 Subject: [PATCH 139/394] Fix typo --- src/main/java/eu/siacs/conversations/utils/Compatibility.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/utils/Compatibility.java b/src/main/java/eu/siacs/conversations/utils/Compatibility.java index 21004e26a..c28b8fe29 100644 --- a/src/main/java/eu/siacs/conversations/utils/Compatibility.java +++ b/src/main/java/eu/siacs/conversations/utils/Compatibility.java @@ -34,7 +34,7 @@ public class Compatibility { "notification_ringtone", "notification_headsup", "vibrate_on_notification"); - private static final List UNUESD_SETTINGS_PRE_TWENTYSIX = + private static final List UNUSED_SETTINGS_PRE_TWENTYSIX = Collections.singletonList("message_notification_settings"); public static boolean hasStoragePermission(Context context) { @@ -115,7 +115,7 @@ public static void removeUnusedPreferences(SettingsFragment settingsFragment) { for (String key : (runsTwentySix() ? UNUSED_SETTINGS_POST_TWENTYSIX - : UNUESD_SETTINGS_PRE_TWENTYSIX)) { + : UNUSED_SETTINGS_PRE_TWENTYSIX)) { Preference preference = settingsFragment.findPreference(key); if (preference != null) { for (PreferenceCategory category : categories) { From 2c5601ccf1c85f6de72cd98591bb470ce9fc847e Mon Sep 17 00:00:00 2001 From: Millesimus <32270710+Millesimus@users.noreply.github.com> Date: Tue, 9 Aug 2022 17:29:01 +0200 Subject: [PATCH 140/394] add migration tutorial * Create migrating_to_new_device.md * Add link to Readme for further information. * Added some info * Make separate backup guide and integrate Readme information with new guides. Co-authored-by: Licaon_Kter --- README.md | 12 ++------ docs/user/backup.md | 19 +++++++++++++ docs/user/migrating_to_new_device.md | 42 ++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 9 deletions(-) create mode 100644 docs/user/backup.md create mode 100644 docs/user/migrating_to_new_device.md diff --git a/README.md b/README.md index 689af33d5..6e7833282 100644 --- a/README.md +++ b/README.md @@ -273,16 +273,10 @@ the translation team and then step by our group chat on and introduce yourself to `iNPUTmice` so he can approve your join request. #### How do I backup / move Conversations to a new device? -On the one hand Conversations supports Message Archive Management to keep a server side history of your messages so when migrating to a new device that device can display your entire history. However that does not work if you enable OMEMO due to its forward secrecy. (Read [The State of Mobile XMPP in 2016](https://gultsch.de/xmpp_2016.html) especially the section on encryption.) -As of version 2.4.0 an integrated Backup & Restore function will help with this, go to Settings and you’ll find a setting called Create backup. A notification will pop-up during the creation process that will announce you when it's ready. After the files, one for each account, are created, you can move the **Conversations** folder *(if you want your old media files too)* or only the **Conversations/Backup** folder *(for OMEMO keys and history only)* to your new device (or to a storage place) where a freshly installed Conversations can restore each account. Don't forget to enable the accounts after a successfull restore. - -This backup method will include your OMEMO keys. Due to forward secrecy you will not be able to recover messages sent and received between creating the backup and restoring it. If you have a server side archive (MAM) those messages will be retrieved but displayed as *unable to decrypt*. For technical reasons you might also lose the first message you either sent or receive after the restore; for each conversation you have. This message will then also show up as *unable to decrypt*, but this will automatically recover itself as long as both participants are on Conversations 2.3.11+. Note that this doesn’t happen if you just transfer to a new phone and no messages have been exchanged between backup and restore. - -In the vast, vast majority of cases you won’t have to manually delete OMEMO keys or do anything like that. Conversations only introduced the official backup feature in 2.4.0 after making sure the *OMEMO self healing* mechanism introduced in 2.3.11 works fine. - -**WARNING**: Be sure to know your accounts passwords or find ways to reset them **before** doing the backup as the files are encrypted using those passwords and the Restore process will ask for them. -**WARNING**: Do not use the restore backup feature in an attempt to clone (run simultaneously) an installation. Restoring a backup is only meant for migrations or in case you’ve lost the original device. +See the dedicated guides for +- [backups](docs/user/backup.md) +- [migrations](docs/user/migrating_to_new_device.md) #### Conversations is missing a certain feature diff --git a/docs/user/backup.md b/docs/user/backup.md new file mode 100644 index 000000000..4d81d8ddd --- /dev/null +++ b/docs/user/backup.md @@ -0,0 +1,19 @@ +# Making a backup of Conversations + +This tutorial explains how you can backup your Conversations data. + +**WARNING**: Do not use the restore backup feature in an attempt to clone (run simultaneously) an installation. Restoring a backup is only meant for migrations or in case you’ve lost the original device. + +1. Make sure that you know the password to your account(s)! You will need it later to decrypt your backup. +2. Deactivate all your account(s): on the chat screen, tap on the three buttons in the upper right, and go to "manage accounts". +3. Go back to Settings, scroll down until you find the option to create a new backup. Tap on that option. +4. Wait, until the notification tells you that the backup is finished. +5. Move the backup to whatever location you feel save with. + +Done! + +## Further information / troubleshooting +### Unable to decrypt +This backup method will include your OMEMO keys. Due to forward secrecy you will not be able to recover messages sent and received between creating the backup and restoring it. If you have a server side archive (MAM) those messages will be retrieved but displayed as *unable to decrypt*. For technical reasons you might also lose the first message you either sent or receive after the restore; for each conversation you have. This message will then also show up as *unable to decrypt*, but this will automatically recover itself as long as both participants are on Conversations 2.3.11+. Note that this doesn’t happen if you just transfer to a new phone and no messages have been exchanged between backup and restore. + +In the vast, vast majority of cases you won’t have to manually delete OMEMO keys or do anything like that. Conversations only introduced the official backup feature in 2.4.0 after making sure the *OMEMO self healing* mechanism introduced in 2.3.11 works fine. diff --git a/docs/user/migrating_to_new_device.md b/docs/user/migrating_to_new_device.md new file mode 100644 index 000000000..401a15386 --- /dev/null +++ b/docs/user/migrating_to_new_device.md @@ -0,0 +1,42 @@ +# Migrating to a new device + +This tutorial explains how you can transfer your Conversations data from an old to a new device. It assumes that you do not have Conversations installed on your new device, yet. It basically consists of three steps: + +1. Make a backup (old device) +2. Move that backup to your new device +3. Import the backup (new device) + +**WARNING**: Do not use the restore backup feature in an attempt to clone (run simultaneously) an installation. Restoring a backup is only meant for migrations or in case you’ve lost the original device. + +## 1. Make a backup (old device) +1. Make sure that you know the password to your account(s)! You will need it later to decrypt your backup. +2. Deactivate all your account(s): on the chat screen, tap on the three buttons in the upper right, and go to "manage accounts". +3. Go back to Settings, scroll down until you find the option to create a new backup. Tap on that option. +4. Wait, until the notification tells you that the backup is finished. + +## 2. Move that backup to your new device +1. Locate the backup. You should find it in your Files, either in *Conversations/Backup* or in *Download/Conversations/Backup*. The file is named after your account (*e.g. kim@example.org*). If you have multiple accounts, you find one file for each. +2. Use your USB cable or bluetooth, your Nextcloud or other cloud storage or pretty much anything you want to copy the backup from the old device to the new device. +3. Remember the location you saved your backup to. For instance, you might want to save them to the *Download* folder. + +## 3. Import the backup (new device) +1. Install Conversations on your new device. +2. Open Conversations for the first time. +3. Tap on "Use other server" +4. Tap on the three dot menu in the upper right corner and tap on "Import backup" +5. If your backup files are not listed, tap on the cloud symbol in the upper right corner to choose the files from the where you saved them. +6. Enter your account password to decrypt the backup. +7. Remember to activate your account (head back to "manage accounts", see step 1.2). +8. Check if chats work. + +Once confirmed that the new device is running fine you can just uninstall the app from the old device. + +Note: The backup only contains your text chats and required encryption keys, all the files need to be transferred separately and put on the new device in the same locations. + +Done! + +## Further information / troubleshooting +### Unable to decrypt +This backup method will include your OMEMO keys. Due to forward secrecy you will not be able to recover messages sent and received between creating the backup and restoring it. If you have a server side archive (MAM) those messages will be retrieved but displayed as *unable to decrypt*. For technical reasons you might also lose the first message you either sent or receive after the restore; for each conversation you have. This message will then also show up as *unable to decrypt*, but this will automatically recover itself as long as both participants are on Conversations 2.3.11+. Note that this doesn’t happen if you just transfer to a new phone and no messages have been exchanged between backup and restore. + +In the vast, vast majority of cases you won’t have to manually delete OMEMO keys or do anything like that. Conversations only introduced the official backup feature in 2.4.0 after making sure the *OMEMO self healing* mechanism introduced in 2.3.11 works fine. From 508e1ac1bd22d16503a1d05150317d783ac83c0b Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 9 Aug 2022 19:42:57 +0200 Subject: [PATCH 141/394] add immutable flag to pending alarm intents --- .../conversations/http/HttpUploadConnection.java | 1 + .../services/XmppConnectionService.java | 14 +++++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java index f1bae9956..20db7bfbd 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java @@ -144,6 +144,7 @@ public void onSuccess(@NullableDecl SlotRequester.Slot result) { @Override public void onFailure(@NotNull final Throwable throwable) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to request slot", throwable); + // TODO consider fall back to jingle in 1-on-1 chats with exactly one online presence fail(throwable.getMessage()); } }, MoreExecutors.directExecutor()); diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index b15d9bccb..43d0e769f 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.services; +import static eu.siacs.conversations.utils.Compatibility.s; + import android.Manifest; import android.annotation.SuppressLint; import android.annotation.TargetApi; @@ -1384,8 +1386,9 @@ private void schedulePostConnectivityChange() { final Intent intent = new Intent(this, EventReceiver.class); intent.setAction(ACTION_POST_CONNECTIVITY_CHANGE); try { - //TODO add immutable flag - final PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 1, intent, 0); + final PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 1, intent, s() + ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { alarmManager.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, pendingIntent); } else { @@ -1397,7 +1400,7 @@ private void schedulePostConnectivityChange() { } public void scheduleWakeUpCall(int seconds, int requestCode) { - final long timeToWake = SystemClock.elapsedRealtime() + (seconds < 0 ? 1 : seconds + 1) * 1000; + final long timeToWake = SystemClock.elapsedRealtime() + (seconds < 0 ? 1 : seconds + 1) * 1000L; final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); if (alarmManager == null) { return; @@ -1431,8 +1434,9 @@ private void scheduleNextIdlePing() { final Intent intent = new Intent(this, EventReceiver.class); intent.setAction(ACTION_IDLE_PING); try { - //TODO add immutable flag - final PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0); + final PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, s() + ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); alarmManager.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingIntent); } catch (RuntimeException e) { Log.d(Config.LOGTAG, "unable to schedule alarm for idle ping", e); From fe3433e427d7d271fb1baf0e0a071550550ff9e9 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 10 Aug 2022 09:11:09 +0200 Subject: [PATCH 142/394] do not accept empty credentials as ice-restart --- .../java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java index e95a7e36d..e7693d6a8 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -182,6 +182,9 @@ public IceUdpTransportInfo.Credentials getDistinctCredentials() { final IceUdpTransportInfo.Credentials credentials = Iterables.getFirst(allCredentials, null); if (allCredentials.size() == 1 && credentials != null) { + if (Strings.isNullOrEmpty(credentials.password) || Strings.isNullOrEmpty(credentials.ufrag)) { + throw new IllegalStateException("Credentials are missing password or ufrag"); + } return credentials; } throw new IllegalStateException("Content map does not have distinct credentials"); From e559b1472910453a40c7cb0dc7ba5291867c39ef Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 10 Aug 2022 19:11:58 +0200 Subject: [PATCH 143/394] pulled translations from transifex --- src/main/res/values-de/strings.xml | 1 + src/main/res/values-pt-rBR/strings.xml | 1 + src/main/res/values-ro-rRO/strings.xml | 1 + src/main/res/values-zh-rCN/strings.xml | 1 + src/main/res/values-zh-rTW/strings.xml | 331 +++++++++++++++++++------ 5 files changed, 253 insertions(+), 82 deletions(-) diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 8372ff2dc..6d85f67d9 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -417,6 +417,7 @@ Video Bild Vektorgrafik + Multimediadatei PDF-Dokument Android App Kontakt diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index 57fe94c67..0847137b3 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -420,6 +420,7 @@ vídeo imagem gráfico vetorial + arquivo multimídia Documento PDF Aplicativo Android Contato diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index c0c15cb4a..17459ba0d 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -420,6 +420,7 @@ video imagine grafic vectorial + fișier multimedia document PDF Aplicație Android Contact diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index be4aa4fac..17d185105 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -414,6 +414,7 @@ 视频 图片 矢量图 + 多媒体文件 PDF文档 Android App 联系人 diff --git a/src/main/res/values-zh-rTW/strings.xml b/src/main/res/values-zh-rTW/strings.xml index c2ca60d1a..0179f1008 100644 --- a/src/main/res/values-zh-rTW/strings.xml +++ b/src/main/res/values-zh-rTW/strings.xml @@ -1,97 +1,132 @@ 設定 - 新對話 + 新會話 管理帳戶 - 聯絡人詳情 + 管理帳戶 + 關閉會話 + 聯絡人詳細資料 + 群組聊天詳細資料 + 頻道詳細資料 新增帳戶 - 編輯姓名 - 添加到地址薄 - 從列表中刪除 + 編輯名稱 + 新增至通訊錄 + 從名冊中刪除 封鎖連絡人 解除封鎖連絡人 封鎖網域 解除封鎖網域 + 封鎖成員 + 解除封鎖成員 管理帳戶 - 設置 - 分享到 Conversation + 設定 + 分享至 Conversation 開始會話 + 選擇聯絡人 + 選擇聯絡人 + 透過帳戶分享 封鎖清單 剛剛 1 分鐘前 - %d分鐘前 - 正在發送… - 訊息解密中,請稍候… - OpenPGP 加密的信息 - 該名稱已存在 + %d 分鐘前 + + %d 則未讀會話 + + + 正在傳送… + 正在解密訊息,請稍候… + OpenPGP 已加密的訊息 + 暱稱已有人使用 + 無效的暱稱 管理員 - 所有者 + 擁有者 版主 - 參與者 + 成員 訪客 - 要封鎖 %s 讓它不能送訊息給你嗎? - 要解除封鎖 %s 讓它可以送訊息給你嗎? - 要封鎖來自 %s 的所有連絡人嗎? - 要解除封鎖來自 %s 的所有連絡人嗎? + 要將 %s 從你的聯絡人清單中移除嗎?與此聯絡人的會話將不會被移除。 + 要封鎖 %s 向您傳送訊息嗎? + 要解除封鎖 %s 並允許他們向您傳送訊息嗎? + 要封鎖來自 %s 的所有聯絡人嗎? + 要解除封鎖來自 %s 的所有聯絡人嗎? 連絡人已封鎖 + 已封鎖 + 要從書籤中移除 %s 嗎?與此書籤相關的會話將不會被移除。 在伺服器上註冊新帳戶 - 在伺服器上改變密碼 - 分享… - 連絡人 - 連絡人 + 在伺服器上變更密碼 + 分享至… + 開始會話 + 邀請聯絡人 + 邀請 + 聯絡人 + 聯絡人 取消 - 設置 - 添加 + 設定 + 新增 編輯 刪除 封鎖 解除封鎖 - 保存 + 儲存 完成 - 現在發送 + %1$s 已當機 + 立即傳送 不再詢問 + 無法連線至帳戶 + 無法連線至多個帳戶 + 輕觸以管理你的帳戶 附加檔案 - 添加連絡人 + 要將這位遺失的聯絡人新增至你的聯絡人清單嗎? + 新增聯絡人 傳遞失敗 - 正在分享檔案中,請稍候… + 正在準備傳送圖片 + 正在準備傳送圖片 + 正在分享檔案,請稍候… 清除歷史記錄 清除會話記錄 - 選擇設備 - 發送未加密的訊息 - 送訊息 - 送訊息給 %s - 送 OMEMO 加密訊息 - 送 v\\OMEMO 加密訊息 - 送 OpenPGP 加密訊息 - 不加密發送 + 刪除檔案 + 之後關閉此會話 + 選擇裝置 + 傳送未加密的訊息 + 傳送訊息 + 傳送訊息至 %s + 傳送 OMEMO 加密訊息 + 傳送 v\\OMEMO 加密訊息 + 傳送 OpenPGP 加密訊息 + 新暱稱已被使用 + 不加密傳送 解密失敗,可能是私密金鑰不正確。 OpenKeychain - 重啟 + 重新啟動 安裝 請安裝 OpenKeychain 以解密 - 輸入… - 等待… - 未發現 OpenPGP 金鑰 + 正在提供… + 正在等候… + 找不到 OpenPGP 金鑰 未找到 OpenPGP 金鑰 - 常規 - 接收檔案 - 自動接收小於 … 的檔案 + 一般 + 接受檔案 + 自動接受小於此大小的檔案 附件 通知 震動 收到新訊息時震動 - LED 燈通知 + LED 通知 收到新訊息時閃爍通知燈 鈴聲 + 通知音效 + 收到新訊息時發出通知音效 + 來電時響鈴 靜默期限 - 高級 - 總不發送崩潰報告 + 進階 + 永不傳送當機報告 確認訊息 - 讓聯絡人知道它們的訊息已經收到以及讀取 + 讓你的聯絡人知道你已經收到並閱讀了他們的訊息 + 防止截圖 + 在多工畫面隱藏應用程式聯絡人並且封鎖螢幕截圖 UI 接受 產生了一個錯誤 - 你的帳號 + 你的帳戶 發送線上連絡人列表更新 接收線上連絡人列表更新 請求線上連絡人列表更新 @@ -114,12 +149,12 @@ 註冊完成 違反政策 伺服器不相容 - 流錯誤 + 串流錯誤 未加密 OTR OpenPGP OMEMO - 刪除帳號 + 刪除帳戶 暫時不可用 發佈頭像 發佈 OpenPGP 公開金鑰 @@ -128,16 +163,21 @@ 啟用帳戶 確定? 錄音 + XMPP 位址 + 封鎖 XMPP 位址 username@example.com 密碼 - 是否添加 %s 到地址薄? + 這不是有效的 XMPP 位址 + 記憶體不足,圖片過大 + 要將 %s 新增至通訊錄嗎? 伺服器資訊 XEP-0313: MAM XEP-0280: 訊息複本 XEP-0352: 用戶端狀態指示 - XEP-0191: 封鎖指令 - XEP-0237: 花名冊版本控制 - XEP-0198: 流管理 + XEP-0191: 封鎖命令 + XEP-0237: 名冊版本設定 + XEP-0198: 串流管理 + XEP-0215: 外部服務探索 XEP-0163: PEP (替身 / OMEMO) XEP-0363: HTTP 檔案上傳 XEP-0357: Push @@ -151,25 +191,31 @@ OpenPGP 金鑰 ID OMEMO 指紋 v\\OMEMO 指紋 - 其他設備 + 其他裝置 信任的 OMEMO 指紋 - 獲取金鑰中 + 正在擷取金鑰… 完成 解密 - 查找 - 輸入連絡人 - 查看連絡人詳細資訊 - 封鎖連絡人 - 解除封鎖連絡人 - 創建 - 選擇 - 連絡人已存在 + 書籤 + 尋找 + 輸入聯絡人 + 刪除聯絡人 + 檢視聯絡人詳細資料 + 封鎖聯絡人 + 解除封鎖聯絡人 + 建立 + 選取 + 聯絡人已存在 加入 - 保存為書簽 - 刪除書簽 + channel@conference.example.com/nick + channel@conference.example.com + 儲存為書籤 + 刪除書籤 + 主旨 + 正在加入群組聊天… 離開 - 連絡人已添加你到連絡人列表 - 反向添加 + 聯絡人已新增至你的聯絡人清單 + 新增回 %s 已讀此句 發佈 正在發佈… @@ -180,25 +226,29 @@ 至 %s 送私密訊息給 %s 連接 - 該帳號已存在 + 此帳戶已存在 下一步 - 忽略 + 工作階段已建立 + 跳過 關閉通知 打開通知 + 群組聊天需要密碼 輸入密碼 - 現在發送請求 + 立即要求 忽略 - 安全 + 安全性 允許更正訊息 允許您的連絡人追回編輯他們的訊息 - 高級設置 + 專家設定 請謹慎使用 + 關於 %s 靜默時間段 開始時間 結束時間 啟用靜默時間段 在靜默時間段內通知將保持靜音 其他 + 同步處理書籤 用帳戶 %s 正在 HTTP 伺服器中檢查 %s 你沒有連接。請稍後重試 @@ -208,7 +258,11 @@ 引用 拷貝原始URL 再次發送 - 檔案位址(URL) + 檔案 URL + 已複製 URL 到剪貼簿 + 已複製 XMPP 位址到剪貼簿 + 已複製錯誤訊息到剪貼簿 + 網頁地址 掃描二維條碼 顯示二維條碼 顯示封鎖清單 @@ -216,15 +270,26 @@ 確認 再試一遍 防止作業系統中斷你的連接 - 選檔案 - 接收中 %1$s (已完成 %2$d%%) + 建立備份 + 備份檔案將被儲存至 %s + 正在建立備份檔案 + 你的備份已建立 + 此備份檔案已被儲存至 %s + 正在還原備份 + 你的備份已還原 + 不要忘記啟用帳戶。 + 選擇檔案 + 正在接收 %1$s (已完成 %2$d%%) 下載 %s 刪除 %s 檔案 - 打開 %s - 發送中 (已完成 %1$d%%) + 開啟 %s + 正在傳送 (已完成 %1$d%%) + 正在準備分享檔案 可以下載 %s 取消傳送 + 無法分享檔案 + 檔案已刪除 在連絡人下方顯示唯讀標籤 啟用通知 帳戶頭像 @@ -418,20 +483,122 @@ 再試解密ㄧ次 通訊對話錯誤 頭條通知 - 消息已經拷貝到剪貼板 + 今天 + 昨天 + 錄製影片 + 複製到剪貼簿 + 訊息已複製到剪貼簿 + 訊息 + 私密訊息已停用 + 受保護的應用程式 + 接受未知憑證? + 憑證詳細資料: + 僅一次 + 捲動至底部 + 傳送訊息後向下捲動 + 編輯狀態訊息 + 編輯訊息 + 停用加密 + 無法擷取裝置清單 + 無法擷取加密金鑰 + 立即停用 OMEMO 加密 一對一以及私人群組的聊天一定會用 OMEMO 新的對話預設會用 OMEMO 加密 - 新的對話必須要手動開啟 OMEMO 加密 + 新的會話必須要手動開啟 OMEMO 加密 + 建立捷徑 字型大小 - App 中所使用的相對字型大小 + 應用程式中使用的相對字型大小 預設開啟 預設關閉 - 適中 + + 復原 + 位置分享已停用 + 固定位置 + 取消固定位置 + 複製位置 + 分享位置 + 方向 + 分享位置 顯示位置 + 分享 + 無法開始錄製 + 請稍候… + 授予 %1$s 以存取麥克風 搜尋訊息 + GIF + 檢視會話 + 分享位置外掛程式 + 使用分享位置外掛程式而非內建地圖 + 複製網站位址 + 複製 XMPP 位址 + 直接搜尋 + 暱稱 + 名稱 聊天群組名稱 + 無法儲存錄製 + 狀態資訊 + 訊息 + 通話 + 訊息 + 來電 + 正在進行的通話 + 無聲訊息 + 訊息通知設定 + 來電通知設定 + 影片壓縮 + 檢視媒體 + 成員 + 媒體瀏覽器 + 影片質量 + 低質量意味這更小的檔案 + 中 (360P) + 高 (720P) + 已取消 + 無效的國家碼 + 選擇國家 + 電話號碼 + 驗證電話號碼 + 請輸入您的電話號碼。 + 搜尋國家 + 驗證 %s + 重新傳送簡訊 + 重新傳送簡訊 (%s) + 請等候 (%s) + 返回 + + + 正在驗證… + 正在要求簡訊… + 未知網路錯誤。 + 沒有網路連線。 + 更新 + 你的名稱 + 輸入你的名稱 + 拒絕要求 + 電子書 + 開啟為… + 選擇帳戶 + 還原備份 + 還原 + 備份與還原 + 建立群組聊天 + 加入公用頻道 + 建立私人群組聊天 + 建立公用頻道 + 頻道名稱 + XMPP 位址 + 活動 + 開啟備份 + 本機伺服器 + 關於 忙碌 + 說明 + 釘選 + 取消釘選 + 離開 + 播放音訊 + 更多選項 From 5a8d70a1f0bbad1d7b0ea89be4fc7573bfbbb27c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 11 Aug 2022 13:27:25 +0200 Subject: [PATCH 144/394] pulled translations from transifex --- src/main/res/values-gl/strings.xml | 1 + src/main/res/values-pl/strings.xml | 1 + 2 files changed, 2 insertions(+) diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 338ff69b8..3c7a0606c 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -417,6 +417,7 @@ video imaxe gráfico de vector + ficheiro multimedia documento PDF App Android Contacto diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 30f28a9ad..d5d494124 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -423,6 +423,7 @@ plik wideo obraz grafika wektorowa + plik multimediów Dokument PDF Aplikacja Androida Kontakt From 150f8313a0d97cf71980a380c1d8e8070a23ec81 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 11 Aug 2022 14:31:27 +0200 Subject: [PATCH 145/394] make launch conversation and launch tor pending intents immutable --- .../services/NotificationService.java | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index acac26cc0..c9b932415 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -1524,7 +1524,9 @@ private PendingIntent createOpenConversationsIntent() { mXmppConnectionService, 0, new Intent(mXmppConnectionService, ConversationsActivity.class), - 0); + s() + ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } catch (RuntimeException e) { return null; } @@ -1573,13 +1575,25 @@ void updateErrorNotification() { R.drawable.ic_play_circle_filled_white_48dp, mXmppConnectionService.getString(R.string.start_orbot), PendingIntent.getActivity( - mXmppConnectionService, 147, TorServiceUtils.LAUNCH_INTENT, 0)); + mXmppConnectionService, + 147, + TorServiceUtils.LAUNCH_INTENT, + s() + ? PendingIntent.FLAG_IMMUTABLE + | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT)); } else { mBuilder.addAction( R.drawable.ic_file_download_white_24dp, mXmppConnectionService.getString(R.string.install_orbot), PendingIntent.getActivity( - mXmppConnectionService, 146, TorServiceUtils.INSTALL_INTENT, 0)); + mXmppConnectionService, + 146, + TorServiceUtils.INSTALL_INTENT, + s() + ? PendingIntent.FLAG_IMMUTABLE + | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT)); } } mBuilder.setDeleteIntent(createDismissErrorIntent()); From 7cc96e704e5d35637b66d49537d40e7e3c5d03ca Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 12 Aug 2022 09:58:33 +0200 Subject: [PATCH 146/394] do not retrieve media attributes from encrypted files fixes #4353 --- .../crypto/PgpDecryptionService.java | 2 +- .../persistance/FileBackend.java | 137 ++++++++++-------- 2 files changed, 77 insertions(+), 62 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java b/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java index a676e5d5d..db84e0cf4 100644 --- a/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java +++ b/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java @@ -207,8 +207,8 @@ private void executeApi(Message message) { } } final String url = message.getFileParams().url; - mXmppConnectionService.getFileBackend().updateFileParams(message, url); message.setEncryption(Message.ENCRYPTION_DECRYPTED); + mXmppConnectionService.getFileBackend().updateFileParams(message, url); mXmppConnectionService.updateMessage(message); if (!inputFile.delete()) { Log.w(Config.LOGTAG,"unable to delete pgp encrypted source file "+inputFile.getAbsolutePath()); diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index c3093f2cf..0d1c03fcb 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -66,7 +66,6 @@ import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.adapter.MediaAdapter; import eu.siacs.conversations.ui.util.Attachment; -import eu.siacs.conversations.utils.Compatibility; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.FileUtils; import eu.siacs.conversations.utils.FileWriterException; @@ -400,25 +399,23 @@ private static boolean weOwnFileLollipop(final Uri uri) { public static Uri getMediaUri(Context context, File file) { final String filePath = file.getAbsolutePath(); - final Cursor cursor; - try { - cursor = - context.getContentResolver() - .query( - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - new String[] {MediaStore.Images.Media._ID}, - MediaStore.Images.Media.DATA + "=? ", - new String[] {filePath}, - null); - } catch (SecurityException e) { - return null; - } - if (cursor != null && cursor.moveToFirst()) { - final int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID)); - cursor.close(); - return Uri.withAppendedPath( - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, String.valueOf(id)); - } else { + try (final Cursor cursor = + context.getContentResolver() + .query( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + new String[] {MediaStore.Images.Media._ID}, + MediaStore.Images.Media.DATA + "=? ", + new String[] {filePath}, + null)) { + if (cursor != null && cursor.moveToFirst()) { + final int id = + cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)); + return Uri.withAppendedPath( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, String.valueOf(id)); + } else { + return null; + } + } catch (final Exception e) { return null; } } @@ -1492,57 +1489,73 @@ public void updateFileParams(Message message) { updateFileParams(message, null); } - public void updateFileParams(Message message, String url) { - DownloadableFile file = getFile(message); + public void updateFileParams(final Message message, final String url) { + final boolean encrypted = + message.getEncryption() == Message.ENCRYPTION_PGP + || message.getEncryption() == Message.ENCRYPTION_DECRYPTED; + final DownloadableFile file = getFile(message); final String mime = file.getMimeType(); - final boolean privateMessage = message.isPrivateMessage(); - final boolean ambiguous = MimeUtils.AMBIGUOUS_CONTAINER_FORMATS.contains(mime); final boolean image = message.getType() == Message.TYPE_IMAGE || (mime != null && mime.startsWith("image/")); - final boolean video = mime != null && mime.startsWith("video/"); - final boolean audio = mime != null && mime.startsWith("audio/"); - final boolean pdf = "application/pdf".equals(mime); + final boolean privateMessage = message.isPrivateMessage(); final StringBuilder body = new StringBuilder(); if (url != null) { body.append(url); } - body.append('|').append(file.getSize()); - if (ambiguous) { - try { - final Dimensions dimensions = getVideoDimensions(file); - if (dimensions.valid()) { - Log.d(Config.LOGTAG,"ambiguous file "+mime+" is video"); - body.append('|').append(dimensions.width).append('|').append(dimensions.height); - } else { - Log.d(Config.LOGTAG,"ambiguous file "+mime+" is audio"); + if (encrypted && !file.exists()) { + Log.d(Config.LOGTAG, "skipping updateFileParams because file is encrypted"); + final DownloadableFile encryptedFile = getFile(message, false); + body.append('|').append(encryptedFile.getSize()); + } else { + Log.d(Config.LOGTAG, "running updateFileParams"); + final boolean ambiguous = MimeUtils.AMBIGUOUS_CONTAINER_FORMATS.contains(mime); + final boolean video = mime != null && mime.startsWith("video/"); + final boolean audio = mime != null && mime.startsWith("audio/"); + final boolean pdf = "application/pdf".equals(mime); + body.append('|').append(file.getSize()); + if (ambiguous) { + try { + final Dimensions dimensions = getVideoDimensions(file); + if (dimensions.valid()) { + Log.d(Config.LOGTAG, "ambiguous file " + mime + " is video"); + body.append('|') + .append(dimensions.width) + .append('|') + .append(dimensions.height); + } else { + Log.d(Config.LOGTAG, "ambiguous file " + mime + " is audio"); + body.append("|0|0|").append(getMediaRuntime(file)); + } + } catch (final NotAVideoFile e) { + Log.d(Config.LOGTAG, "ambiguous file " + mime + " is audio"); body.append("|0|0|").append(getMediaRuntime(file)); } - } catch (final NotAVideoFile e) { - Log.d(Config.LOGTAG,"ambiguous file "+mime+" is audio"); - body.append("|0|0|").append(getMediaRuntime(file)); - } - } else if (image || video || pdf) { - try { - final Dimensions dimensions; - if (video) { - dimensions = getVideoDimensions(file); - } else if (pdf) { - dimensions = getPdfDocumentDimensions(file); - } else { - dimensions = getImageDimensions(file); - } - if (dimensions.valid()) { - body.append('|').append(dimensions.width).append('|').append(dimensions.height); + } else if (image || video || pdf) { + try { + final Dimensions dimensions; + if (video) { + dimensions = getVideoDimensions(file); + } else if (pdf) { + dimensions = getPdfDocumentDimensions(file); + } else { + dimensions = getImageDimensions(file); + } + if (dimensions.valid()) { + body.append('|') + .append(dimensions.width) + .append('|') + .append(dimensions.height); + } + } catch (NotAVideoFile notAVideoFile) { + Log.d( + Config.LOGTAG, + "file with mime type " + file.getMimeType() + " was not a video file"); + // fall threw } - } catch (NotAVideoFile notAVideoFile) { - Log.d( - Config.LOGTAG, - "file with mime type " + file.getMimeType() + " was not a video file"); - // fall threw + } else if (audio) { + body.append("|0|0|").append(getMediaRuntime(file)); } - } else if (audio) { - body.append("|0|0|").append(getMediaRuntime(file)); } message.setBody(body.toString()); message.setDeleted(false); @@ -1556,12 +1569,14 @@ private int getMediaRuntime(final File file) { try { final MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); mediaMetadataRetriever.setDataSource(file.toString()); - final String value = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + final String value = + mediaMetadataRetriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_DURATION); if (Strings.isNullOrEmpty(value)) { return 0; } return Integer.parseInt(value); - } catch (NumberFormatException e) { + } catch (final IllegalArgumentException e) { return 0; } } From e9816a7f9092c29c4e7b37615dc939b43f0dbe1a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 12 Aug 2022 10:02:07 +0200 Subject: [PATCH 147/394] pulled translations from transifex --- src/conversations/res/values-zh-rTW/strings.xml | 8 ++++++++ src/quicksy/res/values-zh-rTW/strings.xml | 8 ++++++++ 2 files changed, 16 insertions(+) create mode 100644 src/conversations/res/values-zh-rTW/strings.xml create mode 100644 src/quicksy/res/values-zh-rTW/strings.xml diff --git a/src/conversations/res/values-zh-rTW/strings.xml b/src/conversations/res/values-zh-rTW/strings.xml new file mode 100644 index 000000000..a5ba73d42 --- /dev/null +++ b/src/conversations/res/values-zh-rTW/strings.xml @@ -0,0 +1,8 @@ + + + 挑選您的 XMPP 提供者 + 使用 conversations.im + 建立新帳戶 + 您的伺服器邀請 + 分享邀請至… + \ No newline at end of file diff --git a/src/quicksy/res/values-zh-rTW/strings.xml b/src/quicksy/res/values-zh-rTW/strings.xml new file mode 100644 index 000000000..d4b37b792 --- /dev/null +++ b/src/quicksy/res/values-zh-rTW/strings.xml @@ -0,0 +1,8 @@ + + + Quicksy 設定檔圖片 + Quicksy 在您的國家無法使用。 + 無法驗證伺服器身分。 + 未知安全性錯誤。 + 連線伺服器逾時。 + From 4fbe2deffc94d087d123dd2b74cd54e1f3558216 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 12 Aug 2022 10:22:45 +0200 Subject: [PATCH 148/394] skip empty uris on attach --- src/main/java/eu/siacs/conversations/ui/util/Attachment.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/ui/util/Attachment.java b/src/main/java/eu/siacs/conversations/ui/util/Attachment.java index f994955d0..9c6849ce6 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/Attachment.java +++ b/src/main/java/eu/siacs/conversations/ui/util/Attachment.java @@ -139,6 +139,9 @@ public static List of(final Context context, Uri uri, Type type) { public static List of(final Context context, List uris, final String type) { final List attachments = new ArrayList<>(); for (final Uri uri : uris) { + if (uri == null) { + continue; + } final String mime = MimeUtils.guessMimeTypeFromUriAndMime(context, uri, type); attachments.add(new Attachment(uri, mime != null && isImage(mime) ? Type.IMAGE : Type.FILE, mime)); } From 41d98da17d584289767ab8186ad16d4cf8846db3 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 12 Aug 2022 11:02:18 +0200 Subject: [PATCH 149/394] set immutable flags for backup notifications --- .../conversations/services/ImportBackupService.java | 6 +++++- src/main/AndroidManifest.xml | 4 ++++ .../conversations/services/ExportBackupService.java | 12 +++++++++--- .../eu/siacs/conversations/ui/util/Attachment.java | 3 +++ 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java b/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java index a1b5f9e77..c118d7375 100644 --- a/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java +++ b/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.services; +import static eu.siacs.conversations.utils.Compatibility.s; + import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; @@ -304,7 +306,9 @@ private void notifySuccess() { mBuilder.setContentTitle(getString(R.string.notification_restored_backup_title)) .setContentText(getString(R.string.notification_restored_backup_subtitle)) .setAutoCancel(true) - .setContentIntent(PendingIntent.getActivity(this, 145, new Intent(this, ManageAccountActivity.class), PendingIntent.FLAG_UPDATE_CURRENT)) + .setContentIntent(PendingIntent.getActivity(this, 145, new Intent(this, ManageAccountActivity.class), s() + ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT)) .setSmallIcon(R.drawable.ic_unarchive_white_24dp); notificationManager.notify(NOTIFICATION_ID, mBuilder.build()); } diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 8cb375870..e37b5ab36 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -60,6 +60,10 @@ + + + + diff --git a/src/main/java/eu/siacs/conversations/services/ExportBackupService.java b/src/main/java/eu/siacs/conversations/services/ExportBackupService.java index 6cbb26ad1..4e144f223 100644 --- a/src/main/java/eu/siacs/conversations/services/ExportBackupService.java +++ b/src/main/java/eu/siacs/conversations/services/ExportBackupService.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.services; +import static eu.siacs.conversations.utils.Compatibility.s; + import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; @@ -344,9 +346,11 @@ private void notifySuccess(final List files) { PendingIntent openFolderIntent = null; - for (Intent intent : getPossibleFileOpenIntents(this, path)) { + for (final Intent intent : getPossibleFileOpenIntents(this, path)) { if (intent.resolveActivityInfo(getPackageManager(), 0) != null) { - openFolderIntent = PendingIntent.getActivity(this, 189, intent, PendingIntent.FLAG_UPDATE_CURRENT); + openFolderIntent = PendingIntent.getActivity(this, 189, intent, s() + ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); break; } } @@ -362,7 +366,9 @@ private void notifySuccess(final List files) { intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.setType(MIME_TYPE); final Intent chooser = Intent.createChooser(intent, getString(R.string.share_backup_files)); - shareFilesIntent = PendingIntent.getActivity(this, 190, chooser, PendingIntent.FLAG_UPDATE_CURRENT); + shareFilesIntent = PendingIntent.getActivity(this, 190, chooser, s() + ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup"); diff --git a/src/main/java/eu/siacs/conversations/ui/util/Attachment.java b/src/main/java/eu/siacs/conversations/ui/util/Attachment.java index 9c6849ce6..e68bcc534 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/Attachment.java +++ b/src/main/java/eu/siacs/conversations/ui/util/Attachment.java @@ -38,6 +38,8 @@ import com.google.common.base.MoreObjects; +import org.jetbrains.annotations.NotNull; + import java.io.File; import java.util.ArrayList; import java.util.Collections; @@ -89,6 +91,7 @@ public Type getType() { return type; } + @NotNull @Override public String toString() { return MoreObjects.toStringHelper(this) From 83d258f90ff16b21af238d7ca96d04bd96aa795a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 15 Aug 2022 11:16:27 +0200 Subject: [PATCH 150/394] version bump to 2.10.9 + changelog --- CHANGELOG.md | 5 +++++ build.gradle | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90365d20e..ac29b5f88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +### Version 2.10.9 + +* Ask for Bluetooth permissions when making A/V calls (You can reject this if you don’t use Bluetooth headsets) +* Fix bug when calling Movim + ### Version 2.10.8 * Fix wrong avatar being shown for group chats diff --git a/build.gradle b/build.gradle index 9e1366ce3..7678d888b 100644 --- a/build.gradle +++ b/build.gradle @@ -91,8 +91,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 32 - versionCode 42034 - versionName "2.10.8" + versionCode 42037 + versionName "2.10.9" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId From aea1e80900c5ac70ebe01fb736ed3a43df63d1dd Mon Sep 17 00:00:00 2001 From: Licaon_Kter Date: Mon, 15 Aug 2022 11:56:05 +0000 Subject: [PATCH 151/394] Add fastlane changelog (#4354) --- fastlane/metadata/android/en-US/changelogs/42037.txt | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 fastlane/metadata/android/en-US/changelogs/42037.txt diff --git a/fastlane/metadata/android/en-US/changelogs/42037.txt b/fastlane/metadata/android/en-US/changelogs/42037.txt new file mode 100644 index 000000000..375905aa8 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/42037.txt @@ -0,0 +1,11 @@ +Version 2.10.9 +* Ask for Bluetooth permissions when making A/V calls (You can reject this if you don’t use Bluetooth headsets) +* Fix bug when calling Movim +* Fix wrong avatar being shown for group chats +* Always ask for battery optimizations opt-out +* Set local only flag on 'x connected accounts' notifications +* Fix interaction with Google Maps Share Location Plugin +* Remove footnote with regards to server fee +* Store files in location appropriate for Android 11 +* Attempt to reconnect call after network switch +* Show caller JID and account JID in incoming call screen From 9fdbd64d024d5493b5ebc0aa8c8c40a856f38894 Mon Sep 17 00:00:00 2001 From: linkmauve Date: Tue, 16 Aug 2022 09:02:57 +0200 Subject: [PATCH 152/394] Update recommendations for Gajim (#4355) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two of the recommended plugins got merged in 1.4, and don’t exist any longer, and when using flatpak the plugins must be installed that way. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6e7833282..5f62f6195 100644 --- a/README.md +++ b/README.md @@ -365,7 +365,7 @@ OTR was removed because it was highly unreliable. It didn’t work with multiple ### What clients do I use on other platforms There are XMPP Clients available for all major platforms. #### Windows / Linux -For your desktop computer we recommend that you use [Gajim](https://gajim.org). You need to install the plugins `OMEMO`, `HTTP Upload` and `URL image preview` to get the best compatibility with Conversations. Plugins can be installed from within the app. +For your desktop computer we recommend that you use [Gajim](https://gajim.org). You need to install the `OMEMO` plugin to get the best compatibility with Conversations. Plugins can be installed from within the app, from your distribution, or from flatpak if you installed it from there. #### iOS Unfortunately we don‘t have a recommendation for iPhones right now. There are three clients available [Siskin](https://siskin.im/), [ChatSecure](https://chatsecure.org/) and [Monal](https://monal.im/). Each with their own pros and cons. From 56a6b17e7e4d04405f907c0a4f25ffe2628b20ea Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 22 Aug 2022 02:50:26 -0500 Subject: [PATCH 153/394] Use the same mechanism for link copying and linkification (#4357) Prevents copying something different from what was linked, such as in the message "fine.gif https://example.com" --- .../conversations/ui/util/ShareUtil.java | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java b/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java index 2470a428f..31c239b47 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java +++ b/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java @@ -31,6 +31,9 @@ import android.content.ActivityNotFoundException; import android.content.Intent; +import android.net.Uri; +import android.text.SpannableStringBuilder; +import android.text.style.URLSpan; import android.widget.Toast; import java.util.regex.Matcher; @@ -106,25 +109,25 @@ public static void copyUrlToClipboard(XmppActivity activity, Message message) { } public static void copyLinkToClipboard(XmppActivity activity, Message message) { - String body = message.getMergedBody().toString(); - Matcher xmppPatternMatcher = Patterns.XMPP_PATTERN.matcher(body); - if (xmppPatternMatcher.find()) { - try { - Jid jid = new XmppUri(body.substring(xmppPatternMatcher.start(), xmppPatternMatcher.end())).getJid(); - if (activity.copyTextToClipboard(jid.asBareJid().toString(), R.string.account_settings_jabber_id)) { - Toast.makeText(activity,R.string.jabber_id_copied_to_clipboard, Toast.LENGTH_SHORT).show(); + SpannableStringBuilder body = message.getMergedBody(); + MyLinkify.addLinks(body, true); + for (final URLSpan urlspan : body.getSpans(0, body.length() - 1, URLSpan.class)) { + Uri uri = Uri.parse(urlspan.getURL()); + if ("xmpp".equals(uri.getScheme())) { + try { + Jid jid = new XmppUri(uri).getJid(); + if (activity.copyTextToClipboard(jid.asBareJid().toString(), R.string.account_settings_jabber_id)) { + Toast.makeText(activity,R.string.jabber_id_copied_to_clipboard, Toast.LENGTH_SHORT).show(); + } + return; + } catch (Exception e) { + e.printStackTrace(); + return; + } + } else { + if (activity.copyTextToClipboard(urlspan.getURL(),R.string.web_address)) { + Toast.makeText(activity,R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT).show(); } - return; - } catch (Exception e) { - e.printStackTrace(); - return; - } - } - Matcher webUrlPatternMatcher = Patterns.AUTOLINK_WEB_URL.matcher(body); - if (webUrlPatternMatcher.find()) { - String url = body.substring(webUrlPatternMatcher.start(),webUrlPatternMatcher.end()); - if (activity.copyTextToClipboard(url,R.string.web_address)) { - Toast.makeText(activity,R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT).show(); } } } From 8111460913921ffb6193879ddd77e249f4e70a7e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 22 Aug 2022 10:01:15 +0200 Subject: [PATCH 154/394] minor code clean up --- .../eu/siacs/conversations/ui/util/ShareUtil.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java b/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java index 31c239b47..8ff81a203 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java +++ b/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java @@ -108,20 +108,19 @@ public static void copyUrlToClipboard(XmppActivity activity, Message message) { } } - public static void copyLinkToClipboard(XmppActivity activity, Message message) { - SpannableStringBuilder body = message.getMergedBody(); + public static void copyLinkToClipboard(final XmppActivity activity, final Message message) { + final SpannableStringBuilder body = message.getMergedBody(); MyLinkify.addLinks(body, true); for (final URLSpan urlspan : body.getSpans(0, body.length() - 1, URLSpan.class)) { - Uri uri = Uri.parse(urlspan.getURL()); + final Uri uri = Uri.parse(urlspan.getURL()); if ("xmpp".equals(uri.getScheme())) { try { - Jid jid = new XmppUri(uri).getJid(); + final Jid jid = new XmppUri(uri).getJid(); if (activity.copyTextToClipboard(jid.asBareJid().toString(), R.string.account_settings_jabber_id)) { Toast.makeText(activity,R.string.jabber_id_copied_to_clipboard, Toast.LENGTH_SHORT).show(); } return; - } catch (Exception e) { - e.printStackTrace(); + } catch (final Exception e) { return; } } else { From c2d37f43591ecc94cf6b9aa542b8589f9cea960c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 22 Aug 2022 11:17:30 +0200 Subject: [PATCH 155/394] use custom libwebrtc (m104) for playstore release --- build.gradle | 11 ++++++----- src/conversations/AndroidManifest.xml | 9 ++++----- src/playstore/AndroidManifest.xml | 24 ++++++++++++++---------- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/build.gradle b/build.gradle index 7678d888b..06c5f0030 100644 --- a/build.gradle +++ b/build.gradle @@ -33,7 +33,7 @@ configurations { dependencies { implementation 'androidx.viewpager:viewpager:1.0.0' - playstoreImplementation('com.google.firebase:firebase-messaging:23.0.6') { + playstoreImplementation('com.google.firebase:firebase-messaging:23.0.7') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' @@ -42,14 +42,14 @@ dependencies { quicksyPlaystoreImplementation 'com.google.android.gms:play-services-auth-api-phone:18.0.1' implementation 'org.sufficientlysecure:openpgp-api:10.0' implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0' - implementation 'androidx.appcompat:appcompat:1.4.2' + implementation 'androidx.appcompat:appcompat:1.5.0' implementation 'androidx.exifinterface:exifinterface:1.3.3' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'com.google.android.material:material:1.4.0' - implementation "androidx.emoji2:emoji2:1.1.0" - freeImplementation "androidx.emoji2:emoji2-bundled:1.1.0" + implementation "androidx.emoji2:emoji2:1.2.0" + freeImplementation "androidx.emoji2:emoji2-bundled:1.2.0" implementation 'org.bouncycastle:bcmail-jdk15on:1.64' //zxing stopped supporting Java 7 so we have to stick with 3.3.3 @@ -75,7 +75,8 @@ dependencies { implementation 'com.google.guava:guava:30.1.1-android' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.49' - implementation 'ch.threema:webrtc-android:100.0.0' + freeImplementation 'ch.threema:webrtc-android:100.0.0' + playstoreImplementation fileTree(include: ['libwebrtc-m104.aar'], dir: 'libs') } ext { diff --git a/src/conversations/AndroidManifest.xml b/src/conversations/AndroidManifest.xml index d573b32d3..c79e4e265 100644 --- a/src/conversations/AndroidManifest.xml +++ b/src/conversations/AndroidManifest.xml @@ -1,8 +1,7 @@ - + - + + android:launchMode="singleTask"> diff --git a/src/playstore/AndroidManifest.xml b/src/playstore/AndroidManifest.xml index 6deb7d2a4..402d957f4 100644 --- a/src/playstore/AndroidManifest.xml +++ b/src/playstore/AndroidManifest.xml @@ -1,23 +1,27 @@ - + - + - - + + - + - + + android:exported="false"> From e8736d5f1ba73a83ce110791ecea27c364544575 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 22 Aug 2022 11:29:04 +0200 Subject: [PATCH 156/394] bump guava library --- build.gradle | 4 ++-- .../conversations/http/HttpUploadConnection.java | 14 +++++++------- .../siacs/conversations/ui/RtpSessionActivity.java | 4 ++-- .../conversations/xmpp/jingle/RtpContentMap.java | 11 +---------- 4 files changed, 12 insertions(+), 21 deletions(-) diff --git a/build.gradle b/build.gradle index 06c5f0030..c102d9e39 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.2.1' + classpath 'com.android.tools.build:gradle:7.2.2' } } @@ -73,7 +73,7 @@ dependencies { implementation "com.squareup.retrofit2:converter-gson:2.9.0" implementation "com.squareup.okhttp3:okhttp:4.10.0" - implementation 'com.google.guava:guava:30.1.1-android' + implementation 'com.google.guava:guava:31.1-android' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.49' freeImplementation 'ch.threema:webrtc-android:100.0.0' playstoreImplementation fileTree(include: ['libwebrtc-m104.aar'], dir: 'libs') diff --git a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java index 20db7bfbd..3e478dd0f 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java @@ -2,14 +2,14 @@ import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; -import org.checkerframework.checker.nullness.compatqual.NullableDecl; -import org.jetbrains.annotations.NotNull; - import java.io.IOException; import java.util.Arrays; import java.util.List; @@ -132,7 +132,7 @@ public void init(boolean delay) { this.slotFuture = new SlotRequester(mXmppConnectionService).request(method, account, file, mime); Futures.addCallback(this.slotFuture, new FutureCallback() { @Override - public void onSuccess(@NullableDecl SlotRequester.Slot result) { + public void onSuccess(@Nullable SlotRequester.Slot result) { HttpUploadConnection.this.slot = result; try { HttpUploadConnection.this.upload(); @@ -142,7 +142,7 @@ public void onSuccess(@NullableDecl SlotRequester.Slot result) { } @Override - public void onFailure(@NotNull final Throwable throwable) { + public void onFailure(@NonNull final Throwable throwable) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to request slot", throwable); // TODO consider fall back to jingle in 1-on-1 chats with exactly one online presence fail(throwable.getMessage()); @@ -169,13 +169,13 @@ private void upload() { this.mostRecentCall = client.newCall(request); this.mostRecentCall.enqueue(new Callback() { @Override - public void onFailure(@NotNull Call call, IOException e) { + public void onFailure(@NonNull Call call, IOException e) { Log.d(Config.LOGTAG, "http upload failed", e); fail(e.getMessage()); } @Override - public void onResponse(@NotNull Call call, @NotNull Response response) { + public void onResponse(@NonNull Call call, @NonNull Response response) { final int code = response.code(); if (code == 200 || code == 201) { Log.d(Config.LOGTAG, "finished uploading file"); diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index fc7b50449..cbf00d04b 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -25,6 +25,7 @@ import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.StringRes; import androidx.databinding.DataBindingUtil; @@ -37,7 +38,6 @@ import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; -import org.checkerframework.checker.nullness.compatqual.NullableDecl; import org.webrtc.RendererCommon; import org.webrtc.SurfaceViewRenderer; import org.webrtc.VideoTrack; @@ -999,7 +999,7 @@ private void switchCamera(final View view) { requireRtpConnection().switchCamera(), new FutureCallback() { @Override - public void onSuccess(@NullableDecl Boolean isFrontCamera) { + public void onSuccess(@Nullable Boolean isFrontCamera) { binding.localVideo.setMirror(isFrontCamera); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java index e7693d6a8..bba44f963 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -1,6 +1,5 @@ package eu.siacs.conversations.xmpp.jingle; -import com.google.common.base.Function; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.Collections2; @@ -11,8 +10,6 @@ import com.google.common.collect.Maps; import com.google.common.collect.Sets; -import org.checkerframework.checker.nullness.compatqual.NullableDecl; - import java.util.Collection; import java.util.List; import java.util.Map; @@ -292,13 +289,7 @@ public static Map of(final Map co return ImmutableMap.copyOf( Maps.transformValues( contents, - new Function() { - @NullableDecl - @Override - public DescriptionTransport apply(@NullableDecl Content content) { - return content == null ? null : of(content); - } - })); + content -> content == null ? null : of(content))); } } From 7b9cf7bb28af3364e19ebd11baa28500925864fb Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 22 Aug 2022 11:33:16 +0200 Subject: [PATCH 157/394] pulled translations from transifex --- src/main/res/values-nl/strings.xml | 46 ++++++++++++++++++++++++++++++ src/main/res/values-pl/strings.xml | 2 +- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-nl/strings.xml b/src/main/res/values-nl/strings.xml index b08726ee8..9b7f72a7c 100644 --- a/src/main/res/values-nl/strings.xml +++ b/src/main/res/values-nl/strings.xml @@ -97,6 +97,7 @@ Verstuur OMEMO-versleuteld bericht Verstuur v\\OMEMO-versleuteld bericht Verstuur OpenPGP-versleuteld bericht + Nieuwe bijnaam in gebruik Verstuur onversleuteld Ontsleutelen mislukt. Misschien heb je niet de juiste private sleutel. OpenKeychain @@ -122,6 +123,7 @@ Verstuur nooit crashrapportages Bevestig berichten Laat je contacten weten wanneer je hun berichten ontvangen en gelezen hebt + Voorkom schermafdrukken Gebruikersomgeving OpenKeychain veroorzaakte een fout. Slechte sleutel voor versleuteling. @@ -150,7 +152,9 @@ Registratie mislukt Gebruikersnaam is al in gebruik Registratie voltooid + Ongeldig registratietoken TLS-onderhandeling mislukt + Domein niet verifieerbaar Beleidsschending Incompatibele server Fout bij stream @@ -174,6 +178,7 @@ gebruikersnaam@voorbeeld.nl Wachtwoord Dit is geen geldig XMPP-adres + Geen geheugen beschikbaar. Afbeelding is te groot Wil je %s toevoegen aan je adresboek? Server-info XEP-0313: MAM @@ -195,9 +200,12 @@ %d uur geleden voor het laatst gezien een dag geleden voor het laatst gezien %d dagen geleden voor het laatst gezien + Nieuw OpenPGP-versleuteld bericht gevonden OpenPGP-sleutel-ID OMEMO-vingerafdruk v\\OMEMO-vingerafdruk + OMEMO-vingerafdruk (herkomst bericht) + v\\OMEMO-vingerafdruk (herkomst bericht) Andere apparaten Vertrouw OMEMO-vingerafdrukken Sleutels ophalen… @@ -232,13 +240,16 @@ Ook toevoegen %s heeft tot hier gelezen %s hebben tot hier gelezen + %1$s en nog %2$d meer hebben tot hier gelezen Iedereen heeft tot hier gelezen Publiceer Tik op avatar om een foto uit de galerij te kiezen Publiceren… De server weigerde de publicatie van je afbeelding + Kon de afbeelding niet converteren Fout bij opslaan van avatar (Of hou lang ingedrukt om de oorspronkelijke terug te zetten) + Je server ondersteunt de publicatie van avatars niet gefluisterd naar %s Privébericht sturen naar %s @@ -266,6 +277,7 @@ Tijdens stille uren worden meldingen onderdrukt Andere Synchroniseren met bladwijzers + OMEMO-vingerafdruk gekopieerd naar klembord Je bent verbannen uit dit groepsgesprek Dit groepsgesprek is enkel voor leden Bronbeperking @@ -779,4 +791,38 @@ Lokale server Over Bezig + Videogesprek + Je microfoon is niet beschikbaar + Je kunt slechts één gesprek tegelijk voeren. + Terug naar lopend gesprek + Kon camera niet wisselen + Bovenaan vastzetten + Bovenaan losmaken + Kon bericht niet corrigeren + Alle gesprekken + Dit gesprek + Je avatar + Avatar voor %s + Versleuteld met OMEMO + Onversleuteld + Speel audio + Pauzeer audio + + Bekijk %1$d deelnemer + Bekijk %1$d deelnemers + + + Een bericht kon niet worden afgeleverd + Sommige berichten konden niet worden afgeleverd + + Mislukte afleveringen + Meer opties + Geen applicatie gevonden + Nodig uit bij Conversations + Kan uitnodiging niet verwerken + Geen actieve accounts ondersteunen deze functie + De backup is gestart. Je krijgt een bericht als het voltooid is. + Kan video niet schakelen. + Onversleuteld document + Accountregistraties zijn niet ondersteund diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index d5d494124..a25411374 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -413,7 +413,7 @@ Ręcznie Odłóż Odpowiedz - Oznacz jako przeczytane + Już przeczytane Ustawienia wprowadzania Enter wysyła Użyj klawisza Enter aby wysłać wiadomość. Możesz zawsze użyć Ctrl+Enter do wysyłania wiadomości, nawet jeśli ta opcja jest wyłączona. From 1aaff18bb5ece9ef51a374523957a76be85be26a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 22 Aug 2022 11:36:29 +0200 Subject: [PATCH 158/394] version bump to 2.10.10 + changelog --- CHANGELOG.md | 5 +++++ build.gradle | 4 ++-- fastlane/metadata/android/en-US/changelogs/42038.txt | 2 ++ 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/42038.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index ac29b5f88..1a10c8088 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +### Version 2.10.10 + +* Minor bug fixes +* Restore ability to call out via JMP and other services (Playstore version) + ### Version 2.10.9 * Ask for Bluetooth permissions when making A/V calls (You can reject this if you don’t use Bluetooth headsets) diff --git a/build.gradle b/build.gradle index c102d9e39..063dac0ce 100644 --- a/build.gradle +++ b/build.gradle @@ -92,8 +92,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 32 - versionCode 42037 - versionName "2.10.9" + versionCode 42038 + versionName "2.10.10" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId diff --git a/fastlane/metadata/android/en-US/changelogs/42038.txt b/fastlane/metadata/android/en-US/changelogs/42038.txt new file mode 100644 index 000000000..da3c42237 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/42038.txt @@ -0,0 +1,2 @@ +* Minor bug fixes +* Restore ability to call out via JMP and other services (Playstore version) From d584ffee7d951212ba379b2425571ec8c398c189 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 25 Aug 2022 07:54:01 +0200 Subject: [PATCH 159/394] try to improve 'sync bookmarks' description --- src/main/res/values/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 8b1fd1de3..d4e2c0d57 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -292,8 +292,8 @@ Enable quiet hours Notifications will be silenced during quiet hours Other - Synchronize with bookmarks - Join group chats automatically if the bookmark says so + Synchronize bookmarks + Set “autojoin” flag when entering or leaving a MUC and react to modifications made by other clients. OMEMO fingerprint copied to clipboard You are banned from this group chat This group chat is members only From ddd08bfe5fb16125306a5d59f43535af80d96383 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 25 Aug 2022 17:12:39 +0200 Subject: [PATCH 160/394] issue self ping + rejoin on muc status code 333 --- .../conversations/entities/MucOptions.java | 1 + .../conversations/generator/IqGenerator.java | 4 ++-- .../conversations/parser/PresenceParser.java | 20 +++++++++++++++---- .../ui/ConversationFragment.java | 3 +++ src/main/res/values/strings.xml | 1 + 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index 34f437e1c..060b1b6f6 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -725,6 +725,7 @@ public enum Error { SHUTDOWN, DESTROYED, INVALID_NICK, + TECHNICAL_PROBLEMS, UNKNOWN, NON_ANONYMOUS } diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 2776aa4f0..6b87cb36d 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -136,8 +136,8 @@ public IqPacket publishNick(String nick) { return publish(Namespace.NICK, item); } - public IqPacket deleteNode(String node) { - IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + public IqPacket deleteNode(final String node) { + final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB_OWNER); pubsub.addChild("delete").setAttribute("node", node); return packet; diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index 341e8d9a0..8ad582b17 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -56,7 +56,8 @@ public void parseConferencePresence(PresencePacket packet, Account account) { } private void processConferencePresence(PresencePacket packet, Conversation conversation) { - MucOptions mucOptions = conversation.getMucOptions(); + final Account account = conversation.getAccount(); + final MucOptions mucOptions = conversation.getMucOptions(); final Jid jid = conversation.getAccount().getJid(); final Jid from = packet.getFrom(); if (!from.isBareJid()) { @@ -93,7 +94,7 @@ private void processConferencePresence(PresencePacket packet, Conversation conve axolotlService.fetchDeviceIds(user.getRealJid()); } if (codes.contains(MucOptions.STATUS_CODE_ROOM_CREATED) && mucOptions.autoPushConfiguration()) { - Log.d(Config.LOGTAG,mucOptions.getAccount().getJid().asBareJid() + Log.d(Config.LOGTAG,account.getJid().asBareJid() +": room '" +mucOptions.getConversation().getJid().asBareJid() +"' created. pushing default configuration"); @@ -138,13 +139,24 @@ private void processConferencePresence(PresencePacket packet, Conversation conve final Jid alternate = destroy == null ? null : InvalidJid.getNullForInvalid(destroy.getAttributeAsJid("jid")); mucOptions.setError(MucOptions.Error.DESTROYED); if (alternate != null) { - Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": muc destroyed. alternate location " + alternate); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": muc destroyed. alternate location " + alternate); } } else if (codes.contains(MucOptions.STATUS_CODE_SHUTDOWN) && fullJidMatches) { mucOptions.setError(MucOptions.Error.SHUTDOWN); } else if (codes.contains(MucOptions.STATUS_CODE_SELF_PRESENCE)) { if (codes.contains(MucOptions.STATUS_CODE_TECHNICAL_REASONS)) { - mucOptions.setError(MucOptions.Error.UNKNOWN); + final boolean wasOnline = mucOptions.online(); + mucOptions.setError(MucOptions.Error.TECHNICAL_PROBLEMS); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": received status code 333 in room " + + mucOptions.getConversation().getJid().asBareJid() + + " online=" + + wasOnline); + if (wasOnline) { + mXmppConnectionService.mucSelfPingAndRejoin(conversation); + } } else if (codes.contains(MucOptions.STATUS_CODE_KICKED)) { mucOptions.setError(MucOptions.Error.KICKED); } else if (codes.contains(MucOptions.STATUS_CODE_BANNED)) { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 0471c014f..3b923adce 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -2721,6 +2721,9 @@ private void updateSnackBar(final Conversation conversation) { case KICKED: showSnackbar(R.string.conference_kicked, R.string.join, joinMuc); break; + case TECHNICAL_PROBLEMS: + showSnackbar(R.string.conference_technical_problems, R.string.try_again, joinMuc); + break; case UNKNOWN: showSnackbar(R.string.conference_unknown_error, R.string.try_again, joinMuc); break; diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index d4e2c0d57..299c57b33 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -301,6 +301,7 @@ You have been kicked from this group chat The group chat was shut down You are no longer in this group chat + You left this group chat due to technical reasons using account %s hosted on %s Checking %s on HTTP host From e439c223ee07e8ee10394db601cad43320c000e9 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 25 Aug 2022 19:22:40 +0200 Subject: [PATCH 161/394] add overflow menu action to delete own avatar --- .../crypto/axolotl/AxolotlService.java | 8 +-- .../conversations/generator/IqGenerator.java | 17 +++-- .../conversations/parser/MessageParser.java | 2 + .../services/XmppConnectionService.java | 72 +++++++++++++++++-- .../ui/PublishProfilePictureActivity.java | 23 +++++- .../eu/siacs/conversations/xml/Namespace.java | 2 + .../menu/activity_publish_profile_picture.xml | 10 +++ src/main/res/values/strings.xml | 1 + 8 files changed, 121 insertions(+), 14 deletions(-) create mode 100644 src/main/res/menu/activity_publish_profile_picture.xml diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 4da07af9f..faef2e098 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -708,11 +708,11 @@ public void onPushFailed() { } public void deleteOmemoIdentity() { - final String node = AxolotlService.PEP_BUNDLES + ":" + getOwnDeviceId(); - final IqPacket deleteBundleNode = mXmppConnectionService.getIqGenerator().deleteNode(node); - mXmppConnectionService.sendIqPacket(account, deleteBundleNode, null); + mXmppConnectionService.deletePepNode( + account, AxolotlService.PEP_BUNDLES + ":" + getOwnDeviceId()); final Set ownDeviceIds = getOwnDeviceIds(); - publishDeviceIdsAndRefineAccessModel(ownDeviceIds == null ? Collections.emptySet() : ownDeviceIds); + publishDeviceIdsAndRefineAccessModel( + ownDeviceIds == null ? Collections.emptySet() : ownDeviceIds); } public List getCryptoTargets(Conversation conversation) { diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 6b87cb36d..52a19eaa4 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -156,9 +156,9 @@ public IqPacket deleteItem(final String node, final String id) { public IqPacket publishAvatar(Avatar avatar, Bundle options) { final Element item = new Element("item"); item.setAttribute("id", avatar.sha1sum); - final Element data = item.addChild("data", "urn:xmpp:avatar:data"); + final Element data = item.addChild("data", Namespace.AVATAR_DATA); data.setContent(avatar.image); - return publish("urn:xmpp:avatar:data", item, options); + return publish(Namespace.AVATAR_DATA, item, options); } public IqPacket publishElement(final String namespace, final Element element, String id, final Bundle options) { @@ -172,20 +172,20 @@ public IqPacket publishAvatarMetadata(final Avatar avatar, final Bundle options) final Element item = new Element("item"); item.setAttribute("id", avatar.sha1sum); final Element metadata = item - .addChild("metadata", "urn:xmpp:avatar:metadata"); + .addChild("metadata", Namespace.AVATAR_METADATA); final Element info = metadata.addChild("info"); info.setAttribute("bytes", avatar.size); info.setAttribute("id", avatar.sha1sum); info.setAttribute("height", avatar.height); info.setAttribute("width", avatar.height); info.setAttribute("type", avatar.type); - return publish("urn:xmpp:avatar:metadata", item, options); + return publish(Namespace.AVATAR_METADATA, item, options); } public IqPacket retrievePepAvatar(final Avatar avatar) { final Element item = new Element("item"); item.setAttribute("id", avatar.sha1sum); - final IqPacket packet = retrieve("urn:xmpp:avatar:data", item); + final IqPacket packet = retrieve(Namespace.AVATAR_DATA, item); packet.setTo(avatar.owner); return packet; } @@ -197,6 +197,13 @@ public IqPacket retrieveVcardAvatar(final Avatar avatar) { return packet; } + public IqPacket retrieveVcardAvatar(final Jid to) { + final IqPacket packet = new IqPacket(IqPacket.TYPE.GET); + packet.setTo(to); + packet.addChild("vCard", "vcard-temp"); + return packet; + } + public IqPacket retrieveAvatarMetaData(final Jid to) { final IqPacket packet = retrieve("urn:xmpp:avatar:metadata", null); if (to != null) { diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 5c66451ce..50743312c 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -279,6 +279,8 @@ private void parseDeleteEvent(final Element event, final Jid from, final Account } else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) { account.setBookmarks(Collections.emptyMap()); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmarks node"); + } else if (Namespace.AVATAR_METADATA.equals(node) && account.getJid().asBareJid().equals(from)) { + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": deleted avatar metadata node"); } } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 43d0e769f..79da6d551 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -38,7 +38,6 @@ import android.provider.ContactsContract; import android.security.KeyChain; import android.telephony.PhoneStateListener; -import android.telephony.TelephonyCallback; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.DisplayMetrics; @@ -48,6 +47,7 @@ import androidx.annotation.BoolRes; import androidx.annotation.IntegerRes; +import androidx.annotation.NonNull; import androidx.core.app.RemoteInput; import androidx.core.content.ContextCompat; @@ -2792,7 +2792,6 @@ public void mucSelfPingAndRejoin(final Conversation conversation) { } }); } - public void joinMuc(Conversation conversation) { joinMuc(conversation, null, false); } @@ -3010,6 +3009,71 @@ public void providePasswordForMuc(Conversation conversation, String password) { } } + public void deleteAvatar(final Account account) { + final AtomicBoolean executed = new AtomicBoolean(false); + final Runnable onDeleted = + () -> { + if (executed.compareAndSet(false, true)) { + account.setAvatar(null); + databaseBackend.updateAccount(account); + getAvatarService().clear(account); + updateAccountUi(); + } + }; + deleteVcardAvatar(account, onDeleted); + deletePepNode(account, Namespace.AVATAR_DATA); + deletePepNode(account, Namespace.AVATAR_METADATA, onDeleted); + } + + public void deletePepNode(final Account account, final String node) { + deletePepNode(account, node, null); + } + + private void deletePepNode(final Account account, final String node, final Runnable runnable) { + final IqPacket request = mIqGenerator.deleteNode(node); + sendIqPacket(account, request, (a, packet) -> { + if (packet.getType() == IqPacket.TYPE.RESULT) { + Log.d(Config.LOGTAG,a.getJid().asBareJid()+": successfully deleted pep node "+node); + if (runnable != null) { + runnable.run(); + } + } else { + Log.d(Config.LOGTAG,a.getJid().asBareJid()+": failed to delete "+ packet); + } + }); + } + + private void deleteVcardAvatar(final Account account, @NonNull final Runnable runnable) { + final IqPacket retrieveVcard = mIqGenerator.retrieveVcardAvatar(account.getJid().asBareJid()); + sendIqPacket(account, retrieveVcard, (a, response) -> { + if (response.getType() != IqPacket.TYPE.RESULT) { + Log.d(Config.LOGTAG,a.getJid().asBareJid()+": no vCard set. nothing to do"); + return; + } + final Element vcard = response.findChild("vCard", "vcard-temp"); + if (vcard == null) { + Log.d(Config.LOGTAG,a.getJid().asBareJid()+": no vCard set. nothing to do"); + return; + } + Element photo = vcard.findChild("PHOTO"); + if (photo == null) { + photo = vcard.addChild("PHOTO"); + } + photo.clearChildren(); + IqPacket publication = new IqPacket(IqPacket.TYPE.SET); + publication.setTo(a.getJid().asBareJid()); + publication.addChild(vcard); + sendIqPacket(account, publication, (a1, publicationResponse) -> { + if (publicationResponse.getType() == IqPacket.TYPE.RESULT) { + Log.d(Config.LOGTAG,a1.getJid().asBareJid()+": successfully deleted vcard avatar"); + runnable.run(); + } else { + Log.d(Config.LOGTAG, "failed to publish vcard " + publicationResponse.getErrorCondition()); + } + }); + }); + } + private boolean hasEnabledAccounts() { if (this.accounts == null) { return false; @@ -3598,7 +3662,7 @@ public void onIqPacketReceived(Account account, IqPacket result) { if (result.getType() == IqPacket.TYPE.RESULT) { publishAvatarMetadata(account, avatar, options, true, callback); } else if (retry && PublishOptions.preconditionNotMet(result)) { - pushNodeConfiguration(account, "urn:xmpp:avatar:data", options, new OnConfigurationPushed() { + pushNodeConfiguration(account, Namespace.AVATAR_DATA, options, new OnConfigurationPushed() { @Override public void onPushSucceeded() { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": changed node configuration for avatar node"); @@ -3638,7 +3702,7 @@ public void onIqPacketReceived(Account account, IqPacket result) { callback.onAvatarPublicationSucceeded(); } } else if (retry && PublishOptions.preconditionNotMet(result)) { - pushNodeConfiguration(account, "urn:xmpp:avatar:metadata", options, new OnConfigurationPushed() { + pushNodeConfiguration(account, Namespace.AVATAR_METADATA, options, new OnConfigurationPushed() { @Override public void onPushSucceeded() { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": changed node configuration for avatar meta data node"); diff --git a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java index cb1d0ad31..0e14fcc8f 100644 --- a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java @@ -7,6 +7,8 @@ import android.os.Build; import android.os.Bundle; import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.view.View.OnLongClickListener; import android.widget.Button; @@ -14,6 +16,7 @@ import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.annotation.StringRes; import com.theartofdev.edmodo.cropper.CropImage; @@ -120,7 +123,25 @@ public void onCreate(Bundle savedInstanceState) { } @Override - public void onSaveInstanceState(Bundle outState) { + public boolean onCreateOptionsMenu(@NonNull final Menu menu) { + getMenuInflater().inflate(R.menu.activity_publish_profile_picture, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(final MenuItem item) { + if (item.getItemId() == R.id.action_delete_avatar) { + if (xmppConnectionService != null && account != null) { + xmppConnectionService.deleteAvatar(account); + } + return true; + } else { + return super.onOptionsItemSelected(item); + } + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { if (this.avatarUri != null) { outState.putParcelable("uri", this.avatarUri); } diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 09bbda4cd..72c35a92f 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -26,6 +26,8 @@ public final class Namespace { public static final String BOOKMARKS_CONVERSION = "urn:xmpp:bookmarks-conversion:0"; public static final String BOOKMARKS = "storage:bookmarks"; public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0"; + public static final String AVATAR_DATA = "urn:xmpp:avatar:data"; + public static final String AVATAR_METADATA = "urn:xmpp:avatar:metadata"; public static final String AVATAR_CONVERSION = "urn:xmpp:pep-vcard-conversion:0"; public static final String JINGLE = "urn:xmpp:jingle:1"; public static final String JINGLE_ERRORS = "urn:xmpp:jingle:errors:1"; diff --git a/src/main/res/menu/activity_publish_profile_picture.xml b/src/main/res/menu/activity_publish_profile_picture.xml new file mode 100644 index 000000000..bcfb99ae8 --- /dev/null +++ b/src/main/res/menu/activity_publish_profile_picture.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 299c57b33..53fb4871d 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -979,5 +979,6 @@ Account registrations are not supported No XMPP address found Temporary authentication failure + Delete avatar From e2612709af160c9001c1c6000fdcd004aa7fd009 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 25 Aug 2022 19:26:18 +0200 Subject: [PATCH 162/394] pulled translations from transifex --- src/main/res/values-ar/strings.xml | 1 - src/main/res/values-bg/strings.xml | 2 -- src/main/res/values-ca/strings.xml | 2 -- src/main/res/values-cs/strings.xml | 2 -- src/main/res/values-da-rDK/strings.xml | 4 ++-- src/main/res/values-de/strings.xml | 5 +---- src/main/res/values-el/strings.xml | 2 -- src/main/res/values-es/strings.xml | 5 +---- src/main/res/values-eu/strings.xml | 1 - src/main/res/values-fi/strings.xml | 2 -- src/main/res/values-fr/strings.xml | 2 -- src/main/res/values-gl/strings.xml | 5 +---- src/main/res/values-hu/strings.xml | 2 -- src/main/res/values-it/strings.xml | 5 +---- src/main/res/values-ja/strings.xml | 5 +---- src/main/res/values-nl/strings.xml | 1 - src/main/res/values-pl/strings.xml | 5 +---- src/main/res/values-pt-rBR/strings.xml | 5 +---- src/main/res/values-ro-rRO/strings.xml | 5 +---- src/main/res/values-ru/strings.xml | 2 -- src/main/res/values-sk/strings.xml | 2 -- src/main/res/values-sr/strings.xml | 2 -- src/main/res/values-sv/strings.xml | 2 -- src/main/res/values-szl/strings.xml | 2 -- src/main/res/values-tr-rTR/strings.xml | 2 -- src/main/res/values-uk/strings.xml | 2 -- src/main/res/values-vi/strings.xml | 2 -- src/main/res/values-zh-rCN/strings.xml | 5 +---- src/main/res/values-zh-rTW/strings.xml | 1 - 29 files changed, 11 insertions(+), 72 deletions(-) diff --git a/src/main/res/values-ar/strings.xml b/src/main/res/values-ar/strings.xml index 197b23a26..6ce130008 100644 --- a/src/main/res/values-ar/strings.xml +++ b/src/main/res/values-ar/strings.xml @@ -239,7 +239,6 @@ تفعيل ساعات السكون سوف تكتم التنبيهات إبان ساعات السكون أخرى - زامِن مع الفواصل المرجعية حسابك محظور للإلتحاق بمجموعة المحادثة هذه هذه المجموعة متاحة للأعضاء المنتمين إليها فقط تم طردك من مجموعة الدردشة هذه diff --git a/src/main/res/values-bg/strings.xml b/src/main/res/values-bg/strings.xml index d4603cc3c..a5018d6ec 100644 --- a/src/main/res/values-bg/strings.xml +++ b/src/main/res/values-bg/strings.xml @@ -294,8 +294,6 @@ Включване на тихите часове Известията ще бъдат заглушени по време на тихите часове Други - Синхронизиране с отметките - Автоматично присъединяване към групови разговори, ако такава е настройката на отметката Отпечатъкът OMEMO е копиран Достъпът Ви до този групов разговор е забранен Този групов разговор е само за членове diff --git a/src/main/res/values-ca/strings.xml b/src/main/res/values-ca/strings.xml index 452c1423d..d5421b742 100644 --- a/src/main/res/values-ca/strings.xml +++ b/src/main/res/values-ca/strings.xml @@ -284,8 +284,6 @@ Habilitar hores de silenci Les notificacions seràn silenciades a les hores de silenci Altres - Sincronitzar als marcadors - Unir-se als xats de grup automàticament si el marcador l\'indica Empremta digital de OMEMO copiada en el portapapers Estàs prohibit en aquest xat de grup Aquest xat en grup només és de membres diff --git a/src/main/res/values-cs/strings.xml b/src/main/res/values-cs/strings.xml index d0ddfa604..d39965222 100644 --- a/src/main/res/values-cs/strings.xml +++ b/src/main/res/values-cs/strings.xml @@ -297,8 +297,6 @@ Povolit tichý režim Upozornění budou během tichého režimu ztlumena Další - Synchronizovat se záložkami - Automaticky se připojovat ke skupinovým chatům, pokud jsou nastaveny v záložkách OMEMO otisk zkopírován do schránky Byl(a) jste blokován(a) v této skupině Tento skupinový chat je pouze pro registrované členy diff --git a/src/main/res/values-da-rDK/strings.xml b/src/main/res/values-da-rDK/strings.xml index c07353a6a..80e81485c 100644 --- a/src/main/res/values-da-rDK/strings.xml +++ b/src/main/res/values-da-rDK/strings.xml @@ -294,8 +294,6 @@ Aktiver stilletid Notifikationer vil være lydløs under stilletid Andre - Synkroniser med bogmærker - Deltag automatisk i gruppechat hvis bogmærket tillader det OMEMO-fingeraftryk kopieret til udklipsholder Du er udelukket fra denne gruppechat Denne gruppechat er kun for medlemmer @@ -417,6 +415,7 @@ video billede vektorgrafik + multimediefil PDF dokument Android App Kontakt @@ -976,4 +975,5 @@ Ren tekstdokument Kontoregistrering er ikke understøttet Ingen XMPP-adresse fundet + Midlertidig godkendelsesfejl diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 6d85f67d9..928c8311f 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -294,8 +294,6 @@ Ruhige Stunden aktivieren Benachrichtigungen sind während der ruhigen Stunden stumm. Sonstiges - Mit Lesezeichen synchronisieren - Gruppenchats automatisch beitreten, wenn das Lesezeichen dies angibt OMEMO-Fingerabdruck in die Zwischenablage kopiert Du wurdest aus diesem Gruppenchat ausgeschlossen Dieser Gruppenchat ist nur für Mitglieder @@ -978,5 +976,4 @@ Kontoregistrierungen werden nicht unterstützt Keine XMPP-Adresse gefunden Temporärer Authentifizierungsfehler - - + diff --git a/src/main/res/values-el/strings.xml b/src/main/res/values-el/strings.xml index 22b188af4..88750efe5 100644 --- a/src/main/res/values-el/strings.xml +++ b/src/main/res/values-el/strings.xml @@ -294,8 +294,6 @@ Ενεργοποίηση ωρών ησυχίας Οι ειδοποιήσεις θα σιγαστούν κατά τις ώρες ησυχίας Άλλο - Συγχρονισμός με σελιδοδείκτες - Συμμετοχή σε ομαδικές συζητήσεις αυτόματα αν ο σελιδοδείκτης αναφέρει αυτόματη συμμετοχή Το αποτύπωμα OMEMO αντιγράφηκε στο πρόχειρο Είστε αποκλεισμένοι από αυτή την ομαδική συζήτηση Αυτή η ομαδική συζήτηση είναι μόνο για μέλη diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index 202d70fc7..fce2d4736 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -297,8 +297,6 @@ Habilitar horario de silencio Las notificaciones serán silenciadas durante el horario de silencio Otros - Sincronizar marcadores - Unirse a conversaciones en grupo automáticamente si el marcador así lo indica Huella digital OMEMO copiada al portapapeles Tu entrada a esta conversación en grupo ha sido prohibida Esta conversación en grupo es solo para miembros @@ -990,5 +988,4 @@ Los registros de cuenta no están soportados Dirección XMPP no encontrada Fallo temporal de autenticación - - + diff --git a/src/main/res/values-eu/strings.xml b/src/main/res/values-eu/strings.xml index 4d2fde773..cfd94fcb3 100644 --- a/src/main/res/values-eu/strings.xml +++ b/src/main/res/values-eu/strings.xml @@ -239,7 +239,6 @@ Ordu lasaiak gaitu Jakinarazpenak isilaraziko dira ordu lasaiak iraun bitartean Besteak - Laster-markekin sinkronizatu Talde honetara sartzea debekatuta duzu Talde hau kideentzat da soilik Baliabide murrizketa diff --git a/src/main/res/values-fi/strings.xml b/src/main/res/values-fi/strings.xml index 9a03e5808..675e11fd4 100644 --- a/src/main/res/values-fi/strings.xml +++ b/src/main/res/values-fi/strings.xml @@ -289,8 +289,6 @@ Ota käyttöön hiljaisuus Ilmoitukset vaimennetaan hiljaisuuden aikana Muut - Synkronoi kirjanmerkkien kanssa - Liity ryhmään automaattisesti jos se on kirjanmerkeissäsi OMEMO-sormenjälki kopioitu leikepöydälle Sinut on estetty tästä ryhmäkeskustelusta Tämä ryhmäkeskustelu on vain jäsenille diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml index 807a45f52..6be70fd0c 100644 --- a/src/main/res/values-fr/strings.xml +++ b/src/main/res/values-fr/strings.xml @@ -295,8 +295,6 @@ Activer les heures tranquilles Les notifications seront muettes pendant les heures tranquilles. Autres - Synchroniser avec les signets - Rejoindre automatiquement les groupes marqués en favoris Empreinte OMEMO copiée dans le presse-papier Vous êtes bannis de ce groupe Ce groupe est réservé aux membres diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 3c7a0606c..2b52435fe 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -294,8 +294,6 @@ Establecer horario sen notificacións As notificacións serán silenciadas durante estas horas Outro - Sincronizar cos marcadores - Unirte as conversas en grupo automáticamente se o marcador así o indica Copiouse a impresión dixital OMEMO ao portapapeis Non podes acceder a esta conversa en grupo Esta conversa en grupo é so para membros @@ -978,5 +976,4 @@ Non está permitido o rexistro de novas contas Non se atopa un enderezo XMPP Fallo temporal da autenticación - - + diff --git a/src/main/res/values-hu/strings.xml b/src/main/res/values-hu/strings.xml index 162212928..5ad16d2e9 100644 --- a/src/main/res/values-hu/strings.xml +++ b/src/main/res/values-hu/strings.xml @@ -289,8 +289,6 @@ Csendes órák engedélyezése Az értesítések el lesznek némítva a csendes órák alatt Egyéb - Szinkronizálás a könyvjelzőkkel - Automatikusan csatlakozzon a csoportos csevegésekhez, ha ez szerepel a könyvjelzőben OMEMO ujjlenyomat a vágólapra lett másolva Ki van tiltva ebből a csoportos csevegésből Ez a csoportos csevegés csak tagoknak szól diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index ffc63ac41..37ef96f79 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -297,8 +297,6 @@ Attiva ore di quiete Le notifiche verranno silenziate durante le ore di quiete Altro - Sincronizza con i segnalibri - Entra nelle chat di gruppo automaticamente se il segnalibro dice così Impronta OMEMO copiata negli appunti Sei stato bandito da questa chat di gruppo Questa chat di gruppo è solo per membri @@ -990,5 +988,4 @@ Le registrazioni di profili non sono supportate Nessun indirizzo XMPP trovato Errore di autenticazione temporaneo - - + diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml index 7fb4f6ae3..94335a359 100644 --- a/src/main/res/values-ja/strings.xml +++ b/src/main/res/values-ja/strings.xml @@ -291,8 +291,6 @@ 消音時間を有効化 消音時間の間、通知は無音になります その他 - ブックマークと同期 - ブックマークに従って、グループチャットに自動で参加します。 OMEMO フィンガープリントをクリップボードにコピーしました このグループチャットから出禁にされています このグループチャットはメンバー制です @@ -958,5 +956,4 @@ アカウント登録はサポートされていません XMPPアドレスがみつかりません 一時的な認証失敗 - - + diff --git a/src/main/res/values-nl/strings.xml b/src/main/res/values-nl/strings.xml index 9b7f72a7c..f67b0c76c 100644 --- a/src/main/res/values-nl/strings.xml +++ b/src/main/res/values-nl/strings.xml @@ -276,7 +276,6 @@ Stille uren inschakelen Tijdens stille uren worden meldingen onderdrukt Andere - Synchroniseren met bladwijzers OMEMO-vingerafdruk gekopieerd naar klembord Je bent verbannen uit dit groepsgesprek Dit groepsgesprek is enkel voor leden diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index a25411374..53158925f 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -300,8 +300,6 @@ Włącz godziny ciszy Powiadomienia będą wyciszone w wybranym przedziale czasu Inne - Synchronizuj z zakładkami - Dołączaj do rozmów grupowych automatycznie jeśli na to wskazuje zakładka Odcisk klucza OMEMO został skopiowany do schowka Zbanowany Konferencja tylko dla użytkowników @@ -1005,5 +1003,4 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Rejestracja kont nie jest wspierana Nie znaleziono adresu XMPP Tymczasowy błąd uwierzytelniania - - + diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index 0847137b3..fd4b29cc2 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -297,8 +297,6 @@ Habilitar horário de sossego As notificações serão silenciadas no horário de sossego. Outras - Sincronizar com os favoritos - Entre nas conversas em grupo automaticamente caso isso esteja definido no favorito Impressão digital OMEMO copiada para a área de transferência Você foi banido desta conversa em grupo Somente membros podem entrar nessa conversa em grupo @@ -991,5 +989,4 @@ O registro de contas não está ativo Não foi encontrado nenhum endereço XMPP Falha temporária na autenticação - - + diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index 17459ba0d..34c5c155f 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -297,8 +297,6 @@ Activează orar de liniște Notificările vor fi reduse la tăcere în timpul orelor de liniște Altele - Sincronizează cu semnele de carte - Alătură-te discuției de grup în mod automat dacă semnul de carte este setat așa Amprentă OMEMO copiată în memorie V-a fost interzis accesul la această discuție de grup Această discuție de grup este rezervată membrilor @@ -991,5 +989,4 @@ Nu este posibilă înregistrarea unui cont Nu a fost găsită o adresă XMPP Eroare temporară de autentificare - - + diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index 4e4880dd6..ddc23d005 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -300,8 +300,6 @@ Включить режим «тихих часов» Уведомления будут отключены во время «тихих часов» Другие - Синхронизировать с закладками - Автоматически заходить в конференции при установленном флаге в настройках закладки OMEMO-отпечаток скопирован в буфер обмена Вы заблокированы в этой конференции Эта конференция — только для участников diff --git a/src/main/res/values-sk/strings.xml b/src/main/res/values-sk/strings.xml index 25e10cd61..e63f51f24 100644 --- a/src/main/res/values-sk/strings.xml +++ b/src/main/res/values-sk/strings.xml @@ -287,8 +287,6 @@ Povoliť tichý režim Upozornenia budú počas tichého režimu stlmené Ďalší - Synchronizovať so záložkami - Automaticky sa pripojiť k skupinovému rozhovoru, ak to hovorí záložka OMEMO odtlačok skopírovaný do schránky Ste zakázaný na tomto skupinovom rozhovore Skupinový rozhovor len pre členov diff --git a/src/main/res/values-sr/strings.xml b/src/main/res/values-sr/strings.xml index cd39b9369..df624e9d8 100644 --- a/src/main/res/values-sr/strings.xml +++ b/src/main/res/values-sr/strings.xml @@ -293,8 +293,6 @@ Укључи тихе сате Обавештења ће бити ућуткана за време тихих сати Остало - Синхронизуј са обележивачима - Аутоматски се придружите групним ћаскањима по поставци обележивача ОМЕМО отисак копиран на клипборд Забрањен вам је приступ овом групном ћаскању Ово групно ћаскање је само за чланове diff --git a/src/main/res/values-sv/strings.xml b/src/main/res/values-sv/strings.xml index 14e91099b..0257dca18 100644 --- a/src/main/res/values-sv/strings.xml +++ b/src/main/res/values-sv/strings.xml @@ -294,8 +294,6 @@ Aktivera tysta timmar Notifieringar kommer vara tysta under tysta timmar Annat - Synkronisera med bokmärken - Gå med i gruppchattar automatiskt om bokmärket säger det OMEMO-fingeravtryck kopierat till urklipp Du är avstängd från denna gruppchatt Denna gruppchatt är endast för medlemmar diff --git a/src/main/res/values-szl/strings.xml b/src/main/res/values-szl/strings.xml index 0ed32a533..b6004932c 100644 --- a/src/main/res/values-szl/strings.xml +++ b/src/main/res/values-szl/strings.xml @@ -316,8 +316,6 @@ Włōncz godziny cisze Powiadōmiynia bydōm wyciszōne we ôbranych godzinach Inksze - Synchrōnizuj ze zokłodkami - Przistympuj do godek grupowych autōmatycznie, jeźli tak pado zokłodka Ôdcisk klucza OMEMO bōł skopiowany do skrytki Ôd tyj grupy mosz wykluczynie Kōnferyncyjo ino dlo czōnkōw diff --git a/src/main/res/values-tr-rTR/strings.xml b/src/main/res/values-tr-rTR/strings.xml index 283fee4d3..683901d9f 100644 --- a/src/main/res/values-tr-rTR/strings.xml +++ b/src/main/res/values-tr-rTR/strings.xml @@ -294,8 +294,6 @@ Sessiz saatleri etkinleştir Bildirimler sessiz saatler boyunca sessize alınacaktır Diğer - Yer imleri ile senkronize et. - Yer imleri öyle belirtmişse grup konuşmalarına otomatik olarak katıl. OMEMO parmak izi panoya kopyalandı Bu grup konuşmasından menedildiniz Bu grup konuşması yalnızca üyeleri içindir diff --git a/src/main/res/values-uk/strings.xml b/src/main/res/values-uk/strings.xml index 4a96a344e..b28336c59 100644 --- a/src/main/res/values-uk/strings.xml +++ b/src/main/res/values-uk/strings.xml @@ -276,8 +276,6 @@ Увімкнути години тиші Сповіщення не звучатимуть під час годин тиші Інше - Синхронізовувати з закладками - Приєднуватися до груп і полишати їх відповідно до опції автоматичного приєднання, вибраної в закладках. Цифровий підпис OMEMO скопійовано Вам заборонили доступ до цієї групи Ця група лише для учасників diff --git a/src/main/res/values-vi/strings.xml b/src/main/res/values-vi/strings.xml index 76eb33c37..afb4b7296 100644 --- a/src/main/res/values-vi/strings.xml +++ b/src/main/res/values-vi/strings.xml @@ -291,8 +291,6 @@ Bật giờ yên lặng Thông báo sẽ được tắt trong giờ yên lặng Khác - Đồng bộ hoá bằng dấu trang - Tự động tham gia các cuộc trò chuyện nhóm nếu dấu trang bảo thế Đã sao chép mã vân tay OMEMO vào bộ nhớ tạm Bạn bị cấm khỏi cuộc trò chuyện nhóm này Cuộc trò chuyện nhóm này chỉ dành cho thành viên diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index 17d185105..ad19591cf 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -291,8 +291,6 @@ 启用静默时间段 在静默时间段内通知将保持静音 其他 - 与书签同步 - 根据书签标记自动加入群聊。 OMEMO指纹已拷贝到剪贴板 您被封禁了 这个群聊只允许成员聊天 @@ -965,5 +963,4 @@ 不支持注册账户 未找到 XMPP 地址 临时认证失败 - - + diff --git a/src/main/res/values-zh-rTW/strings.xml b/src/main/res/values-zh-rTW/strings.xml index 0179f1008..edcacf374 100644 --- a/src/main/res/values-zh-rTW/strings.xml +++ b/src/main/res/values-zh-rTW/strings.xml @@ -248,7 +248,6 @@ 啟用靜默時間段 在靜默時間段內通知將保持靜音 其他 - 同步處理書籤 用帳戶 %s 正在 HTTP 伺服器中檢查 %s 你沒有連接。請稍後重試 From a6b88ba9e95deaeeeff6a4433d1e79a28d74cd25 Mon Sep 17 00:00:00 2001 From: Dmitry Markin Date: Mon, 29 Aug 2022 13:41:35 +0300 Subject: [PATCH 163/394] Add missed call notifications Co-authored-by: Daniel Gultsch --- art/ic_missed_call_notification.svg | 344 ++++++++++++++++++ art/render.rb | 3 +- .../conversations/entities/Conversation.java | 4 +- .../services/NotificationService.java | 241 +++++++++++- .../services/XmppConnectionService.java | 31 +- .../xmpp/jingle/JingleRtpConnection.java | 2 + .../ic_missed_call_notification.png | Bin 0 -> 810 bytes .../ic_missed_call_notification.png | Bin 0 -> 589 bytes .../ic_missed_call_notification.png | Bin 0 -> 1151 bytes .../ic_missed_call_notification.png | Bin 0 -> 1680 bytes .../ic_missed_call_notification.png | Bin 0 -> 2179 bytes src/main/res/values/strings.xml | 5 + 12 files changed, 609 insertions(+), 21 deletions(-) create mode 100644 art/ic_missed_call_notification.svg create mode 100644 src/main/res/drawable-hdpi/ic_missed_call_notification.png create mode 100644 src/main/res/drawable-mdpi/ic_missed_call_notification.png create mode 100644 src/main/res/drawable-xhdpi/ic_missed_call_notification.png create mode 100644 src/main/res/drawable-xxhdpi/ic_missed_call_notification.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_missed_call_notification.png diff --git a/art/ic_missed_call_notification.svg b/art/ic_missed_call_notification.svg new file mode 100644 index 000000000..78f0acead --- /dev/null +++ b/art/ic_missed_call_notification.svg @@ -0,0 +1,344 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/art/render.rb b/art/render.rb index 7fb46d138..7ae4ac8ae 100755 --- a/art/render.rb +++ b/art/render.rb @@ -28,6 +28,7 @@ 'conversations_mono.svg' => ['conversations/ic_notification', 24], 'quicksy_mono.svg' => ['quicksy/ic_notification', 24], 'flip_camera_android-black-24dp.svg' => ['ic_flip_camera_android_black_24dp', 24], + 'ic_missed_call_notification.svg' => ['ic_missed_call_notification', 24], 'ic_send_text_offline.svg' => ['ic_send_text_offline', 36], 'ic_send_text_offline_white.svg' => ['ic_send_text_offline_white', 36], 'ic_send_text_online.svg' => ['ic_send_text_online', 36], @@ -119,7 +120,7 @@ def execute_cmd(cmd) else path = "../src/#{output_parts[0]}/res/drawable-#{resolution}/#{output_parts[1]}.png" end - execute_cmd "#{inkscape} #{source_filename} -C -w #{width} -h #{height} -o #{path}" + execute_cmd "#{inkscape} #{source_filename} -C -w #{width} -h #{height} -e #{path}" top = [] right = [] diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 4a825fbb3..8bb65cc0f 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -241,11 +241,11 @@ public void findWaitingMessages(OnMessageFound onMessageFound) { } } - public void findUnreadMessages(OnMessageFound onMessageFound) { + public void findUnreadMessagesAndCalls(OnMessageFound onMessageFound) { final ArrayList results = new ArrayList<>(); synchronized (this.messages) { for (final Message message : this.messages) { - if (message.isRead() || message.getType() == Message.TYPE_RTP_SESSION) { + if (message.isRead()) { continue; } results.add(message); diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index c9b932415..b6916020d 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -90,17 +90,20 @@ public class NotificationService { private static final long[] CALL_PATTERN = {0, 500, 300, 600}; - private static final String CONVERSATIONS_GROUP = "eu.siacs.conversations"; + private static final String MESSAGES_GROUP = "eu.siacs.conversations.messages"; + private static final String MISSED_CALLS_GROUP = "eu.siacs.conversations.missed_calls"; private static final int NOTIFICATION_ID_MULTIPLIER = 1024 * 1024; static final int FOREGROUND_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 4; private static final int NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 2; private static final int ERROR_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 6; private static final int INCOMING_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 8; public static final int ONGOING_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 10; - private static final int DELIVERY_FAILED_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 12; + public static final int MISSED_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 12; + private static final int DELIVERY_FAILED_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 13; private final XmppConnectionService mXmppConnectionService; private final LinkedHashMap> notifications = new LinkedHashMap<>(); private final HashMap mBacklogMessageCounter = new HashMap<>(); + private final LinkedHashMap mMissedCalls = new LinkedHashMap<>(); private Conversation mOpenConversation; private boolean mIsInForeground; private long mLastNotification; @@ -221,6 +224,16 @@ void initializeChannels() { ongoingCallsChannel.setGroup("calls"); notificationManager.createNotificationChannel(ongoingCallsChannel); + final NotificationChannel missedCallsChannel = new NotificationChannel("missed_calls", + c.getString(R.string.missed_calls_channel_name), + NotificationManager.IMPORTANCE_HIGH); + missedCallsChannel.setShowBadge(true); + missedCallsChannel.setSound(null, null); + missedCallsChannel.setLightColor(LED_COLOR); + missedCallsChannel.enableLights(true); + missedCallsChannel.setGroup("calls"); + notificationManager.createNotificationChannel(missedCallsChannel); + final NotificationChannel messagesChannel = new NotificationChannel( "messages", @@ -284,12 +297,18 @@ void initializeChannels() { notificationManager.createNotificationChannel(deliveryFailedChannel); } - private boolean notify(final Message message) { + private boolean notifyMessage(final Message message) { final Conversation conversation = (Conversation) message.getConversation(); return message.getStatus() == Message.STATUS_RECEIVED && !conversation.isMuted() && (conversation.alwaysNotify() || wasHighlightedOrPrivate(message)) - && (!conversation.isWithStranger() || notificationsFromStrangers()); + && (!conversation.isWithStranger() || notificationsFromStrangers()) + && message.getType() != Message.TYPE_RTP_SESSION; + } + + private boolean notifyMissedCall(final Message message) { + return message.getType() == Message.TYPE_RTP_SESSION + && message.getStatus() == Message.STATUS_RECEIVED; } public boolean notificationsFromStrangers() { @@ -320,12 +339,16 @@ private boolean isQuietHours() { } public void pushFromBacklog(final Message message) { - if (notify(message)) { + if (notifyMessage(message)) { synchronized (notifications) { getBacklogMessageCounter((Conversation) message.getConversation()) .incrementAndGet(); pushToStack(message); } + } else if (notifyMissedCall(message)) { + synchronized (mMissedCalls) { + pushMissedCall(message); + } } } @@ -360,6 +383,9 @@ public void finishBacklog(boolean notify, Account account) { updateNotification(count > 0, conversations); } } + synchronized (mMissedCalls) { + updateMissedCallNotifications(mMissedCalls.keySet()); + } } private List getBacklogConversations(Account account) { @@ -666,7 +692,7 @@ public static void cancelIncomingCallNotification(final Context context) { private void pushNow(final Message message) { mXmppConnectionService.updateUnreadCountBadge(); - if (!notify(message)) { + if (!notifyMessage(message)) { Log.d( Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() @@ -695,7 +721,29 @@ private void pushNow(final Message message) { } } - public void clear() { + private void pushMissedCall(final Message message) { + final Conversational conversation = message.getConversation(); + final MissedCallsInfo info = mMissedCalls.get(conversation); + if (info == null) { + mMissedCalls.put(conversation, new MissedCallsInfo(message.getTimeSent())); + } else { + info.newMissedCall(message.getTimeSent()); + } + } + + public void pushMissedCallNow(final Message message) { + synchronized (mMissedCalls) { + pushMissedCall(message); + updateMissedCallNotifications(Collections.singleton(message.getConversation())); + } + } + + public void clear(final Conversation conversation) { + clearMessages(conversation); + clearMissedCalls(conversation); + } + + public void clearMessages() { synchronized (notifications) { for (ArrayList messages : notifications.values()) { markAsReadIfHasDirectReply(messages); @@ -705,7 +753,7 @@ public void clear() { } } - public void clear(final Conversation conversation) { + public void clearMessages(final Conversation conversation) { synchronized (this.mBacklogMessageCounter) { this.mBacklogMessageCounter.remove(conversation); } @@ -718,6 +766,25 @@ public void clear(final Conversation conversation) { } } + public void clearMissedCalls() { + synchronized (mMissedCalls) { + for (final Conversational conversation : mMissedCalls.keySet()) { + cancel(conversation.getUuid(), MISSED_CALL_NOTIFICATION_ID); + } + mMissedCalls.clear(); + updateMissedCallNotifications(null); + } + } + + public void clearMissedCalls(final Conversation conversation) { + synchronized (mMissedCalls) { + if (mMissedCalls.remove(conversation) != null) { + cancel(conversation.getUuid(), MISSED_CALL_NOTIFICATION_ID); + updateMissedCallNotifications(null); + } + } + } + private void markAsReadIfHasDirectReply(final Conversation conversation) { markAsReadIfHasDirectReply(notifications.get(conversation.getUuid())); } @@ -797,7 +864,7 @@ private void updateNotification( } modifyForSoundVibrationAndLight( singleBuilder, notifyThis, quiteHours, preferences); - singleBuilder.setGroup(CONVERSATIONS_GROUP); + singleBuilder.setGroup(MESSAGES_GROUP); setNotificationColor(singleBuilder); notify(entry.getKey(), NOTIFICATION_ID, singleBuilder.build()); } @@ -807,6 +874,31 @@ private void updateNotification( } } + private void updateMissedCallNotifications(final Set update) { + if (mMissedCalls.isEmpty()) { + cancel(MISSED_CALL_NOTIFICATION_ID); + return; + } + if (mMissedCalls.size() == 1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + final Conversational conversation = mMissedCalls.keySet().iterator().next(); + final MissedCallsInfo info = mMissedCalls.values().iterator().next(); + final Notification notification = missedCall(conversation, info); + notify(MISSED_CALL_NOTIFICATION_ID, notification); + } else { + final Notification summary = missedCallsSummary(); + notify(MISSED_CALL_NOTIFICATION_ID, summary); + if (update != null) { + for (final Conversational conversation : update) { + final MissedCallsInfo info = mMissedCalls.get(conversation); + if (info != null) { + final Notification notification = missedCall(conversation, info); + notify(conversation.getUuid(), MISSED_CALL_NOTIFICATION_ID, notification); + } + } + } + } + } + private void modifyForSoundVibrationAndLight( Builder mBuilder, boolean notify, boolean quietHours, SharedPreferences preferences) { final Resources resources = mXmppConnectionService.getResources(); @@ -867,6 +959,101 @@ private Uri fixRingtoneUri(Uri uri) { } } + private Notification missedCallsSummary() { + final Builder publicBuilder = buildMissedCallsSummary(true); + final Builder builder = buildMissedCallsSummary(false); + builder.setPublicVersion(publicBuilder.build()); + return builder.build(); + } + + private Builder buildMissedCallsSummary(boolean publicVersion) { + final Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "missed_calls"); + int totalCalls = 0; + final StringBuilder names = new StringBuilder(); + long lastTime = 0; + for (Map.Entry entry : mMissedCalls.entrySet()) { + final Conversational conversation = entry.getKey(); + final MissedCallsInfo missedCallsInfo = entry.getValue(); + names.append(conversation.getContact().getDisplayName()); + names.append(", "); + totalCalls += missedCallsInfo.getNumberOfCalls(); + lastTime = Math.max(lastTime, missedCallsInfo.getLastTime()); + } + if (names.length() >= 2) { + names.delete(names.length() - 2, names.length()); + } + final String title = (totalCalls == 1) ? mXmppConnectionService.getString(R.string.missed_call) : + (mMissedCalls.size() == 1) ? mXmppConnectionService.getString(R.string.n_missed_calls, totalCalls) : + mXmppConnectionService.getString(R.string.n_missed_calls_from_m_contacts, totalCalls, mMissedCalls.size()); + builder.setContentTitle(title); + builder.setTicker(title); + if (!publicVersion) { + builder.setContentText(names.toString()); + } + builder.setSmallIcon(R.drawable.ic_missed_call_notification); + builder.setGroupSummary(true); + builder.setGroup(MISSED_CALLS_GROUP); + builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN); + builder.setCategory(NotificationCompat.CATEGORY_CALL); + builder.setWhen(lastTime); + if (!mMissedCalls.isEmpty()) { + final Conversational firstConversation = mMissedCalls.keySet().iterator().next(); + builder.setContentIntent(createContentIntent(firstConversation)); + } + builder.setDeleteIntent(createMissedCallsDeleteIntent(null)); + modifyMissedCall(builder); + return builder; + } + + private Notification missedCall(final Conversational conversation, final MissedCallsInfo info) { + final Builder publicBuilder = buildMissedCall(conversation, info, true); + final Builder builder = buildMissedCall(conversation, info, false); + builder.setPublicVersion(publicBuilder.build()); + return builder.build(); + } + + private Builder buildMissedCall(final Conversational conversation, final MissedCallsInfo info, boolean publicVersion) { + final Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "missed_calls"); + final String title = (info.getNumberOfCalls() == 1) ? mXmppConnectionService.getString(R.string.missed_call) : + mXmppConnectionService.getString(R.string.n_missed_calls, info.getNumberOfCalls()); + builder.setContentTitle(title); + final String name = conversation.getContact().getDisplayName(); + if (publicVersion) { + builder.setTicker(title); + } else { + if (info.getNumberOfCalls() == 1) { + builder.setTicker(mXmppConnectionService.getString(R.string.missed_call_from_x, name)); + } else { + builder.setTicker(mXmppConnectionService.getString(R.string.n_missed_calls_from_x, info.getNumberOfCalls(), name)); + } + builder.setContentText(name); + } + builder.setSmallIcon(R.drawable.ic_missed_call_notification); + builder.setGroup(MISSED_CALLS_GROUP); + builder.setCategory(NotificationCompat.CATEGORY_CALL); + builder.setWhen(info.getLastTime()); + builder.setContentIntent(createContentIntent(conversation)); + builder.setDeleteIntent(createMissedCallsDeleteIntent(conversation)); + if (!publicVersion && conversation instanceof Conversation) { + builder.setLargeIcon(mXmppConnectionService.getAvatarService() + .get((Conversation) conversation, AvatarService.getSystemUiAvatarSize(mXmppConnectionService))); + } + modifyMissedCall(builder); + return builder; + } + + private void modifyMissedCall(final Builder builder) { + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService); + final Resources resources = mXmppConnectionService.getResources(); + final boolean led = preferences.getBoolean("led", resources.getBoolean(R.bool.led)); + if (led) { + builder.setLights(LED_COLOR, 2000, 3000); + } + builder.setPriority(NotificationCompat.PRIORITY_HIGH); + builder.setSound(null); + setNotificationColor(builder); + } + private Builder buildMultipleConversation(final boolean notify, final boolean quietHours) { final Builder mBuilder = new NotificationCompat.Builder( @@ -932,7 +1119,7 @@ private Builder buildMultipleConversation(final boolean notify, final boolean qu mBuilder.setContentIntent(createContentIntent(conversation)); } mBuilder.setGroupSummary(true); - mBuilder.setGroup(CONVERSATIONS_GROUP); + mBuilder.setGroup(MESSAGES_GROUP); mBuilder.setDeleteIntent(createDeleteIntent(null)); mBuilder.setSmallIcon(R.drawable.ic_notification); return mBuilder; @@ -1336,7 +1523,7 @@ private PendingIntent createContentIntent(final Conversational conversation) { private PendingIntent createDeleteIntent(Conversation conversation) { final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); - intent.setAction(XmppConnectionService.ACTION_CLEAR_NOTIFICATION); + intent.setAction(XmppConnectionService.ACTION_CLEAR_MESSAGE_NOTIFICATION); if (conversation != null) { intent.putExtra("uuid", conversation.getUuid()); return PendingIntent.getService( @@ -1356,6 +1543,16 @@ private PendingIntent createDeleteIntent(Conversation conversation) { : PendingIntent.FLAG_UPDATE_CURRENT); } + private PendingIntent createMissedCallsDeleteIntent(final Conversational conversation) { + final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); + intent.setAction(XmppConnectionService.ACTION_CLEAR_MISSED_CALL_NOTIFICATION); + if (conversation != null) { + intent.putExtra("uuid", conversation.getUuid()); + return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 21), intent, 0); + } + return PendingIntent.getService(mXmppConnectionService, 1, intent, 0); + } + private PendingIntent createReplyIntent( final Conversation conversation, final String lastMessageUuid, @@ -1677,6 +1874,28 @@ private void cancel(String tag, int id) { } } + private static class MissedCallsInfo { + private int numberOfCalls; + private long lastTime; + + MissedCallsInfo(final long time) { + numberOfCalls = 1; + lastTime = time; + } + + public void newMissedCall(final long time) { + ++numberOfCalls; + lastTime = time; + } + + public int getNumberOfCalls() { + return numberOfCalls; + } + + public long getLastTime() { + return lastTime; + } + } private class VibrationRunnable implements Runnable { @Override diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 79da6d551..245454247 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -175,7 +175,8 @@ public class XmppConnectionService extends Service { public static final String ACTION_REPLY_TO_CONVERSATION = "reply_to_conversations"; public static final String ACTION_MARK_AS_READ = "mark_as_read"; public static final String ACTION_SNOOZE = "snooze"; - public static final String ACTION_CLEAR_NOTIFICATION = "clear_notification"; + public static final String ACTION_CLEAR_MESSAGE_NOTIFICATION = "clear_message_notification"; + public static final String ACTION_CLEAR_MISSED_CALL_NOTIFICATION = "clear_missed_call_notification"; public static final String ACTION_DISMISS_ERROR_NOTIFICATIONS = "dismiss_error"; public static final String ACTION_TRY_AGAIN = "try_again"; public static final String ACTION_IDLE_PING = "idle_ping"; @@ -670,19 +671,35 @@ public int onStartCommand(Intent intent, int flags, int startId) { case Intent.ACTION_SHUTDOWN: logoutAndSave(true); return START_NOT_STICKY; - case ACTION_CLEAR_NOTIFICATION: + case ACTION_CLEAR_MESSAGE_NOTIFICATION: mNotificationExecutor.execute(() -> { try { final Conversation c = findConversationByUuid(uuid); if (c != null) { - mNotificationService.clear(c); + mNotificationService.clearMessages(c); } else { - mNotificationService.clear(); + mNotificationService.clearMessages(); } restoredFromDatabaseLatch.await(); } catch (InterruptedException e) { - Log.d(Config.LOGTAG, "unable to process clear notification"); + Log.d(Config.LOGTAG, "unable to process clear message notification"); + } + }); + break; + case ACTION_CLEAR_MISSED_CALL_NOTIFICATION: + mNotificationExecutor.execute(() -> { + try { + final Conversation c = findConversationByUuid(uuid); + if (c != null) { + mNotificationService.clearMissedCalls(c); + } else { + mNotificationService.clearMissedCalls(); + } + restoredFromDatabaseLatch.await(); + + } catch (InterruptedException e) { + Log.d(Config.LOGTAG, "unable to process clear missed call notification"); } }); break; @@ -769,7 +786,7 @@ public int onStartCommand(Intent intent, int flags, int startId) { return; } c.setMutedTill(System.currentTimeMillis() + 30 * 60 * 1000); - mNotificationService.clear(c); + mNotificationService.clearMessages(c); updateConversation(c); }); case AudioManager.RINGER_MODE_CHANGED_ACTION: @@ -1954,7 +1971,7 @@ private void restoreFromDatabase() { private void restoreMessages(Conversation conversation) { conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE)); conversation.findUnsentTextMessages(message -> markMessage(message, Message.STATUS_WAITING)); - conversation.findUnreadMessages(mNotificationService::pushFromBacklog); + conversation.findUnreadMessagesAndCalls(mNotificationService::pushFromBacklog); } public void loadPhoneContacts() { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 353851c37..c69fc6b02 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -1110,6 +1110,7 @@ private synchronized void ringingTimeout() { rejectCallFromSessionInitiate(); break; } + xmppConnectionService.getNotificationService().pushMissedCallNow(message); } private void cancelRingingTimeout() { @@ -1187,6 +1188,7 @@ private void receiveRetract(final Jid from, final String serverMsgId, final long this.state == State.PROCEED ? State.RETRACTED_RACED : State.RETRACTED; if (transition(target)) { xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); + xmppConnectionService.getNotificationService().pushMissedCallNow(message); Log.d( Config.LOGTAG, id.account.getJid().asBareJid() diff --git a/src/main/res/drawable-hdpi/ic_missed_call_notification.png b/src/main/res/drawable-hdpi/ic_missed_call_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..3608ebd92478ee32c52ad4feba3b261218ebc50d GIT binary patch literal 810 zcmV+_1J(SAP)oxe((33 z_x;c1Kk%Os%1%%NuoBn+tN`W%GXR12KtJ#lxF16JP(RhNBJJui^_9i=soqhy)?1`S zJ+6K$D3Zkkb$vZ#7OKxCAd+TSJ*Q5uD$}LDtCLKcyXwrcGOL|fi}JXyPAe+YsSZt| zOv2S7GR^9f2FN7r%@aDg=>bQ%a@8{Iq-9HsW>?#QYd}Zpa8@fu$f!-gWnfiC+{pFSJ>!)7 zOdQi>^y-1STzy@nO{uw8#)Pg?w6pvuX$-yaYA_Z%R~V_c*zmNni15Pk{=x z#rwTEl`2!2gzuwUwzh=Ocv4klR0l>2wHo!Ys8E*FePfy!C(PnPEVC$M?2l9` zPgxn&(^*1?OD0LU)$V&>2*xt5men4R43~u;iKlQXi?eanvk(!Uty+Z`#3n@)3yp#bDn2DYi`|9EFzXxg-f5P-bMHC# z+}W886hhqui~@gvk0FG=rM3SfJ+9tY-@6(s>YRE*omdO;LG`I+_^sYmx0aI6sNV|7 zV=Sl#`jJo99jz4~)Uh`5ed^Z%$Q!&+*X0Cehd|!oLc<%HO$FXG??b>6us?+0`Z{nn zAzuU@WeRr#=QHs+U;@}yzwZT(0j~g1yNSiexwh1!Ei>P)o=)P|02&8-+Ptb$z$~yC z*pMv01WwlRo5|XEO!6t`Ks{AIPpCgyI^Sh=22wl#7^yXuTMS%MJ0XPUzzpy+(|H$| z%GCaf1(w=I1YAu-x&`C_@8S_%PVCLA7uDBMtP|z8jU~pJSYS-;^(j#86q0{ex2*D` z-YyVO{Zf!~ui+huG_LpAiv~Liaz?XJ^>A%(P%NwYYezkzE)GHDP&y(2LJ0Fz_sAWf zS1^t?O!v)D$JHz9Gpqk{&+0_KRi9XW-jnJ%tNUyva`13~=2{z?RBx%D)rFx8L_!!cl58m?00000NkvXXu0mjfUpoA) literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_missed_call_notification.png b/src/main/res/drawable-xhdpi/ic_missed_call_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..80cd15819fcca113e91cd6a57468a3062926bfed GIT binary patch literal 1151 zcmV-_1c3XAP)dAf*T*3p6S`T|i9B zAdVfPHxWXzpo}6asy`rl&@Mz52ASp^8>>}brSqec?BB4RTxkz&Ie0o@W+3fAU zthM*r``8CItToL1=KGzQH8Xn#{^vy10o6-^<-h`9F>o4~0geJkB4W16`g%5?UI9D+ z+zH$YT+$}{EN~Fm1AGR29T8`G)leEW)b;9@>KVJ|nEJAM*&xN=t{&(?bTejZMlSBB z@DcSbb*^IZDM!@X`XPRWdZ>@WQ)bm?suaIU{c}L#Q{GUAdli3!dU77b=e(zOH&d$1 z)D!b4KIf%w#9yd>GvCT<%&8k&X&q{j{ycC?Y3KbRfH&18g>swCIDgM?qc`QXR_+=N zdho5a3B*X^GzxCB#hu_W<8@$hS6qxgdESD^SU)h&WkPk$S3N)+u0J zMC?`f0=t0KE%M%xzBKm0d~jAgKtxOeZ@0-mYO*GzJ+Pvi+RZjxuK?FItE@xuI`Or5 z4+5`uD7PH=zEQ}*0*lEbElgal{#bHhnpW2|#2Gi+PIqE`UM3wvBOP2fie6Tmxrm4p zz}>*l1&dDsYa?P$5^nJLQQF$P%^y+&2f7H9(yAx+QC-VJ){}qaFKK|U0O{?t zZfi~8qY+_599LIIgn5RV_-k@iDNY--^0mO=CiOESpyF^;`A0_1lL@^*42w`l`CgCU?W?kh)I&$o6}u zrzD71VTUS@8SJlk)??!*n{0mB(A6Sh= RVebF{002ovPDHLkV1lNL8CC!Q literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_missed_call_notification.png b/src/main/res/drawable-xxhdpi/ic_missed_call_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..0072d2ef0d9253358efbc88d9ed7882b77e27be1 GIT binary patch literal 1680 zcmV;B25K~#90?VMe#9aR;_f2)*Iga|Dp7*dgvSQQ@l?1O;P!h?iB zga_ck05%vQKmq}R7-Fo65QO+Ze9Qry|-H(hO$9jHp+cATISWIz=Oc4qjWwYHUsBG z#PdxW*A_@!4m<*Uwn>K_u?09kBG%V!SW6)FNMHl7ylzKRz&7CQi1|NqO(&1})1$i6Z=QilzKGK(mQTY<^ zg%<2=08X#j<~-mBLu;uoHQ1OMX;W&T@1KmkMW%Q3Y^|-?Zm0UmtWOIv4jus<*T&6u z#BC9A6F@|)1-@6K?Ltxn??!eGI*|^)wvNbc5pm1lg`P;a*06C>_Ci*mHpYJ!+?f>z zKwShJU&GGlvKO+G^op8}RYE-rxTr$aIU!zwIg!)rb20FH;OrWsPQ2=3 z_2Ch17biogFE%uq73diC=Z;6Daf+j9ldw>rryCm06G(N&c10S;uo~ne%Qn!|M;jV0 z5=h$n2*V+9I4VMX|<-_e473`0DSoImYirw1K@Gyb2xk!@|NA=ZV0$tkB za20{H{&1Vbuo|RrZ@S;{%7%uk3Z&H|X_ArC=gw?@*){(;U@I`^wZBEy0HIzU5wrb^ z5izT-0R-MDdAIrv^KC>O&2buM}dCojGRK>X*qAuzG zfI}*@Sp)nmE689}^|9HDXI=ga{C#LG^+Mj1C*<59Gp6N! zQp2jxJw;lSB`;hCtgEbQd#nRK#ds&=K?F{Th-dQ(%LH0T$_*;thTQY{LquFqR9Lur zM8q!O3X;O}nK#M`OB1Lf;wj+XR^6FCN^>uVegiXQ)jvZz88{!=1uTt-_wovdCUHi@ zyTAp&o`1qGpM}6_MTM2pfP3d0u5HVsDP&bqLDf8IBI2jOciQk~3i)zTL6gHQKLoBH z$(`wACutsfKU+{ag8D?n4ZwBGujPM#3XXQw76>3B?gB0cc8uWEw2`~qHxdXSB4$Xc zzuDBrl#wgY$hknKtgIgam$YH8Lv{d5B4XR%#l{iTCnB~+#AT$V^2Jf@b;u&1(w1u@ zdaorX1K(iC`^^-yDtnoTX(yee{nI45OTz+l?R zcHn*BKfphMSAdPA?{?2e#77nTJB_qub0M&}qMK(%#1j?kP7c#n>k`NC-xAcN>UHWX z6{2Wu+x_oRsAs6Z>$5^1lsM)#-99O*9xYLvel%6zE2mO8{5Y| an(;C5E_<216#l;e0000X6P)W!p2+^2O#IMAcNF>G&N{Z0Lchpd60SQo$(n^|e5fFSsG}7AkQY^H+ ze?QDQk$XG4JG(P`cJDd+`?xzh^PJ~7^UUir105Y59gSg43s8illYu3`Qs5L|40ttg z9B?e~0&oDB0`>#{03HLLFtgoF$Xte289>qkUH*gp5doz0} zPRb!e(wL;nCH+>?e^oCBB>hU#XC=L=>EGTcl2%Ikk)#)DR(cjwl5Uc8ZqvFL7?S2m zx=7N44Jkc~hb3JmXk@R@8dM;#_q>nYNtE5O;Drs}WiY?0)NoO>zyC@_r zkaWGIL(S^4G6yAHC+QV+>Zz7)sHA1Ut-#yr$bZ0W2QD$QC+o;Od7~rgGMv!EpnQO{ zfE|)9tt0bf29Pvg(ha~5aii2>Vhq?Q>03(NQ%d53B)t;23HVfs{94XNU~Q@qb+iE_ zodDbhoE;NqBqhlsV3)>r+wbQ>UP0@wn~uUhC3 zatN3(v-`__SjqsB76UtRD^nda`5W*KGkdPgM`c~m_vnPyi6mK zj%Um)a}1TWm~a;H%AW*28kt8CzXaY~Q{LNwYk*sD+u^Hns-$IR_L%QwmI2O>D6r3q zX7*Hqd@_;rVoh1^1Xh{Z9!VGAmR)0va6T?(;V81&fD{IptLy|mXl8o=%xoKQ0WSEX zDw+B`$^gCP#&<^)Hka8BoYz-=p9DsDZ`KQ64LFVA+gKICcHC65cmAelgfCajd$A;M zq>s-4b9+~CHTph3bMj2ma^UL`Wi0hQ^fkiXgmu!JbNUkH*O^aEP<-w`A3)LyU^8(I z@iO3n*$;gNctb=Xl`ym2k|u#4FxbWZK`9?HW)*xcsFA^!nQa6v1r86W$WXWQ8{(Bu zRvtbBj3p?n5@xmuSVR3rn9qReLitdl2GrgKm9!ym zPi4;tR{%#67wzrm&ysBgZsToA47i3|4eY2cOuBcyH<2JQ1_dXZ&-yGTC|Qx^*0`NMKQ*(=ZhfW!dad@)Y7r@p+=f}rH@LI;N#4t% z2$}Z*_a+r59h-m|ZGzh4^ZWb>SW>A50GQc*xE@g(ncVrIL5kC3t7r2`0Ph*O*$y$bj_uJ7A169J3O?B7`lqKEHhwi$QlSE=I~ zM+Bq54JDaBF|!AH1^qQeenUoXL!4{^%giQm0m<7^>}V@xa}H)h{!DB2yW-v zI^c8UY$J?d9&NnjNYY7?)=RoiZVUIIcuCTolGaIjjYqCCBz;rTo`hw3r}0~Jqqd~e zft9#X^#pK~Vj`Hv?NHu|n@YbYaPj+3-j)h?wTwa!PqNIFx}jgs~U8sNI7Wgbc-jY+yv(vAWH#2k6n5rU+5 zO8UN}=VvK@N^U^fF$g3rlMessages Incoming calls Ongoing calls + Missed calls Silent messages This notification group is used to display notifications that should not trigger any sound. For example when being active on another device (Grace Period). Failed deliveries @@ -934,6 +935,10 @@ Outgoing call Outgoing call · %s Missed call + Missed call from %s + %1$d missed calls from %2$s + %d missed calls + %1$d missed calls from %2$d contacts Audio call Video call Help From f8b9e15634e2ddb21899a009770a9252d4979290 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 29 Aug 2022 13:01:20 +0200 Subject: [PATCH 164/394] fixups for missed call notifications --- .../services/NotificationService.java | 81 ++++++++++++++----- .../services/XmppConnectionService.java | 2 +- 2 files changed, 60 insertions(+), 23 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index b6916020d..92c777fb4 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -103,7 +103,8 @@ public class NotificationService { private final XmppConnectionService mXmppConnectionService; private final LinkedHashMap> notifications = new LinkedHashMap<>(); private final HashMap mBacklogMessageCounter = new HashMap<>(); - private final LinkedHashMap mMissedCalls = new LinkedHashMap<>(); + private final LinkedHashMap mMissedCalls = + new LinkedHashMap<>(); private Conversation mOpenConversation; private boolean mIsInForeground; private long mLastNotification; @@ -224,9 +225,11 @@ void initializeChannels() { ongoingCallsChannel.setGroup("calls"); notificationManager.createNotificationChannel(ongoingCallsChannel); - final NotificationChannel missedCallsChannel = new NotificationChannel("missed_calls", - c.getString(R.string.missed_calls_channel_name), - NotificationManager.IMPORTANCE_HIGH); + final NotificationChannel missedCallsChannel = + new NotificationChannel( + "missed_calls", + c.getString(R.string.missed_calls_channel_name), + NotificationManager.IMPORTANCE_HIGH); missedCallsChannel.setShowBadge(true); missedCallsChannel.setSound(null, null); missedCallsChannel.setLightColor(LED_COLOR); @@ -413,8 +416,8 @@ private int getBacklogMessageCount(Account account) { return count; } - void finishBacklog(boolean notify) { - finishBacklog(notify, null); + void finishBacklog() { + finishBacklog(false, null); } private void pushToStack(final Message message) { @@ -967,7 +970,8 @@ private Notification missedCallsSummary() { } private Builder buildMissedCallsSummary(boolean publicVersion) { - final Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "missed_calls"); + final Builder builder = + new NotificationCompat.Builder(mXmppConnectionService, "missed_calls"); int totalCalls = 0; final StringBuilder names = new StringBuilder(); long lastTime = 0; @@ -982,9 +986,16 @@ private Builder buildMissedCallsSummary(boolean publicVersion) { if (names.length() >= 2) { names.delete(names.length() - 2, names.length()); } - final String title = (totalCalls == 1) ? mXmppConnectionService.getString(R.string.missed_call) : - (mMissedCalls.size() == 1) ? mXmppConnectionService.getString(R.string.n_missed_calls, totalCalls) : - mXmppConnectionService.getString(R.string.n_missed_calls_from_m_contacts, totalCalls, mMissedCalls.size()); + final String title = + (totalCalls == 1) + ? mXmppConnectionService.getString(R.string.missed_call) + : (mMissedCalls.size() == 1) + ? mXmppConnectionService.getString( + R.string.n_missed_calls, totalCalls) + : mXmppConnectionService.getString( + R.string.n_missed_calls_from_m_contacts, + totalCalls, + mMissedCalls.size()); builder.setContentTitle(title); builder.setTicker(title); if (!publicVersion) { @@ -1012,19 +1023,27 @@ private Notification missedCall(final Conversational conversation, final MissedC return builder.build(); } - private Builder buildMissedCall(final Conversational conversation, final MissedCallsInfo info, boolean publicVersion) { - final Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "missed_calls"); - final String title = (info.getNumberOfCalls() == 1) ? mXmppConnectionService.getString(R.string.missed_call) : - mXmppConnectionService.getString(R.string.n_missed_calls, info.getNumberOfCalls()); + private Builder buildMissedCall( + final Conversational conversation, final MissedCallsInfo info, boolean publicVersion) { + final Builder builder = + new NotificationCompat.Builder(mXmppConnectionService, "missed_calls"); + final String title = + (info.getNumberOfCalls() == 1) + ? mXmppConnectionService.getString(R.string.missed_call) + : mXmppConnectionService.getString( + R.string.n_missed_calls, info.getNumberOfCalls()); builder.setContentTitle(title); final String name = conversation.getContact().getDisplayName(); if (publicVersion) { builder.setTicker(title); } else { if (info.getNumberOfCalls() == 1) { - builder.setTicker(mXmppConnectionService.getString(R.string.missed_call_from_x, name)); + builder.setTicker( + mXmppConnectionService.getString(R.string.missed_call_from_x, name)); } else { - builder.setTicker(mXmppConnectionService.getString(R.string.n_missed_calls_from_x, info.getNumberOfCalls(), name)); + builder.setTicker( + mXmppConnectionService.getString( + R.string.n_missed_calls_from_x, info.getNumberOfCalls(), name)); } builder.setContentText(name); } @@ -1035,15 +1054,20 @@ private Builder buildMissedCall(final Conversational conversation, final MissedC builder.setContentIntent(createContentIntent(conversation)); builder.setDeleteIntent(createMissedCallsDeleteIntent(conversation)); if (!publicVersion && conversation instanceof Conversation) { - builder.setLargeIcon(mXmppConnectionService.getAvatarService() - .get((Conversation) conversation, AvatarService.getSystemUiAvatarSize(mXmppConnectionService))); + builder.setLargeIcon( + mXmppConnectionService + .getAvatarService() + .get( + (Conversation) conversation, + AvatarService.getSystemUiAvatarSize(mXmppConnectionService))); } modifyMissedCall(builder); return builder; } private void modifyMissedCall(final Builder builder) { - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService); + final SharedPreferences preferences = + PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService); final Resources resources = mXmppConnectionService.getResources(); final boolean led = preferences.getBoolean("led", resources.getBoolean(R.bool.led)); if (led) { @@ -1521,7 +1545,7 @@ private PendingIntent createContentIntent(final Conversational conversation) { return createContentIntent(conversation.getUuid(), null); } - private PendingIntent createDeleteIntent(Conversation conversation) { + private PendingIntent createDeleteIntent(final Conversation conversation) { final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); intent.setAction(XmppConnectionService.ACTION_CLEAR_MESSAGE_NOTIFICATION); if (conversation != null) { @@ -1548,9 +1572,21 @@ private PendingIntent createMissedCallsDeleteIntent(final Conversational convers intent.setAction(XmppConnectionService.ACTION_CLEAR_MISSED_CALL_NOTIFICATION); if (conversation != null) { intent.putExtra("uuid", conversation.getUuid()); - return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 21), intent, 0); + return PendingIntent.getService( + mXmppConnectionService, + generateRequestCode(conversation, 21), + intent, + s() + ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } - return PendingIntent.getService(mXmppConnectionService, 1, intent, 0); + return PendingIntent.getService( + mXmppConnectionService, + 1, + intent, + s() + ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } private PendingIntent createReplyIntent( @@ -1896,6 +1932,7 @@ public long getLastTime() { return lastTime; } } + private class VibrationRunnable implements Runnable { @Override diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 245454247..517a63a6a 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1958,7 +1958,7 @@ private void restoreFromDatabase() { restoreMessages(conversation); } } - mNotificationService.finishBacklog(false); + mNotificationService.finishBacklog(); restoredFromDatabaseLatch.countDown(); final long diffMessageRestore = SystemClock.elapsedRealtime() - startMessageRestore; Log.d(Config.LOGTAG, "finished restoring messages in " + diffMessageRestore + "ms"); From b792563fad694286ee79ea7205853653502c7c7a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 29 Aug 2022 14:04:33 +0200 Subject: [PATCH 165/394] use non-custom missed called --- art/ic_missed_call_notification.svg | 344 ------------------ .../services/NotificationService.java | 82 ++--- .../ic_missed_call_notification.png | Bin 810 -> 0 bytes .../ic_missed_call_notification.png | Bin 589 -> 0 bytes .../ic_missed_call_notification.png | Bin 1151 -> 0 bytes .../ic_missed_call_notification.png | Bin 1680 -> 0 bytes .../ic_missed_call_notification.png | Bin 2179 -> 0 bytes .../drawable/ic_call_missed_white_24db.xml | 5 + 8 files changed, 43 insertions(+), 388 deletions(-) delete mode 100644 art/ic_missed_call_notification.svg delete mode 100644 src/main/res/drawable-hdpi/ic_missed_call_notification.png delete mode 100644 src/main/res/drawable-mdpi/ic_missed_call_notification.png delete mode 100644 src/main/res/drawable-xhdpi/ic_missed_call_notification.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_missed_call_notification.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_missed_call_notification.png create mode 100644 src/main/res/drawable/ic_call_missed_white_24db.xml diff --git a/art/ic_missed_call_notification.svg b/art/ic_missed_call_notification.svg deleted file mode 100644 index 78f0acead..000000000 --- a/art/ic_missed_call_notification.svg +++ /dev/null @@ -1,344 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index 92c777fb4..55e220f62 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -36,6 +36,7 @@ import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.IconCompat; +import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.common.collect.Iterables; @@ -973,19 +974,15 @@ private Builder buildMissedCallsSummary(boolean publicVersion) { final Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "missed_calls"); int totalCalls = 0; - final StringBuilder names = new StringBuilder(); + final List names = new ArrayList<>(); long lastTime = 0; - for (Map.Entry entry : mMissedCalls.entrySet()) { + for (final Map.Entry entry : mMissedCalls.entrySet()) { final Conversational conversation = entry.getKey(); final MissedCallsInfo missedCallsInfo = entry.getValue(); - names.append(conversation.getContact().getDisplayName()); - names.append(", "); + names.add(conversation.getContact().getDisplayName()); totalCalls += missedCallsInfo.getNumberOfCalls(); lastTime = Math.max(lastTime, missedCallsInfo.getLastTime()); } - if (names.length() >= 2) { - names.delete(names.length() - 2, names.length()); - } final String title = (totalCalls == 1) ? mXmppConnectionService.getString(R.string.missed_call) @@ -999,9 +996,9 @@ private Builder buildMissedCallsSummary(boolean publicVersion) { builder.setContentTitle(title); builder.setTicker(title); if (!publicVersion) { - builder.setContentText(names.toString()); + builder.setContentText(Joiner.on(", ").join(names)); } - builder.setSmallIcon(R.drawable.ic_missed_call_notification); + builder.setSmallIcon(R.drawable.ic_call_missed_white_24db); builder.setGroupSummary(true); builder.setGroup(MISSED_CALLS_GROUP); builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN); @@ -1047,7 +1044,7 @@ private Builder buildMissedCall( } builder.setContentText(name); } - builder.setSmallIcon(R.drawable.ic_missed_call_notification); + builder.setSmallIcon(R.drawable.ic_call_missed_white_24db); builder.setGroup(MISSED_CALLS_GROUP); builder.setCategory(NotificationCompat.CATEGORY_CALL); builder.setWhen(info.getLastTime()); @@ -1091,42 +1088,39 @@ private Builder buildMultipleConversation(final boolean notify, final boolean qu R.plurals.x_unread_conversations, notifications.size(), notifications.size())); - final StringBuilder names = new StringBuilder(); + final List names = new ArrayList<>(); Conversation conversation = null; for (final ArrayList messages : notifications.values()) { - if (messages.size() > 0) { - conversation = (Conversation) messages.get(0).getConversation(); - final String name = conversation.getName().toString(); - SpannableString styledString; - if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) { - int count = messages.size(); - styledString = - new SpannableString( - name - + ": " - + mXmppConnectionService - .getResources() - .getQuantityString( - R.plurals.x_messages, count, count)); - styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0); - style.addLine(styledString); - } else { - styledString = - new SpannableString( - name - + ": " - + UIHelper.getMessagePreview( - mXmppConnectionService, messages.get(0)) - .first); - styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0); - style.addLine(styledString); - } - names.append(name); - names.append(", "); + if (messages.isEmpty()) { + continue; } - } - if (names.length() >= 2) { - names.delete(names.length() - 2, names.length()); + conversation = (Conversation) messages.get(0).getConversation(); + final String name = conversation.getName().toString(); + SpannableString styledString; + if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) { + int count = messages.size(); + styledString = + new SpannableString( + name + + ": " + + mXmppConnectionService + .getResources() + .getQuantityString( + R.plurals.x_messages, count, count)); + styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0); + style.addLine(styledString); + } else { + styledString = + new SpannableString( + name + + ": " + + UIHelper.getMessagePreview( + mXmppConnectionService, messages.get(0)) + .first); + styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0); + style.addLine(styledString); + } + names.add(name); } final String contentTitle = mXmppConnectionService @@ -1137,7 +1131,7 @@ private Builder buildMultipleConversation(final boolean notify, final boolean qu notifications.size()); mBuilder.setContentTitle(contentTitle); mBuilder.setTicker(contentTitle); - mBuilder.setContentText(names.toString()); + mBuilder.setContentText(Joiner.on(", ").join(names)); mBuilder.setStyle(style); if (conversation != null) { mBuilder.setContentIntent(createContentIntent(conversation)); diff --git a/src/main/res/drawable-hdpi/ic_missed_call_notification.png b/src/main/res/drawable-hdpi/ic_missed_call_notification.png deleted file mode 100644 index 3608ebd92478ee32c52ad4feba3b261218ebc50d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 810 zcmV+_1J(SAP)oxe((33 z_x;c1Kk%Os%1%%NuoBn+tN`W%GXR12KtJ#lxF16JP(RhNBJJui^_9i=soqhy)?1`S zJ+6K$D3Zkkb$vZ#7OKxCAd+TSJ*Q5uD$}LDtCLKcyXwrcGOL|fi}JXyPAe+YsSZt| zOv2S7GR^9f2FN7r%@aDg=>bQ%a@8{Iq-9HsW>?#QYd}Zpa8@fu$f!-gWnfiC+{pFSJ>!)7 zOdQi>^y-1STzy@nO{uw8#)Pg?w6pvuX$-yaYA_Z%R~V_c*zmNni15Pk{=x z#rwTEl`2!2gzuwUwzh=Ocv4klR0l>2wHo!Ys8E*FePfy!C(PnPEVC$M?2l9` zPgxn&(^*1?OD0LU)$V&>2*xt5men4R43~u;iKlQXi?eanvk(!Uty+Z`#3n@)3yp#bDn2DYi`|9EFzXxg-f5P-bMHC# z+}W886hhqui~@gvk0FG=rM3SfJ+9tY-@6(s>YRE*omdO;LG`I+_^sYmx0aI6sNV|7 zV=Sl#`jJo99jz4~)Uh`5ed^Z%$Q!&+*X0Cehd|!oLc<%HO$FXG??b>6us?+0`Z{nn zAzuU@WeRr#=QHs+U;@}yzwZT(0j~g1yNSiexwh1!Ei>P)o=)P|02&8-+Ptb$z$~yC z*pMv01WwlRo5|XEO!6t`Ks{AIPpCgyI^Sh=22wl#7^yXuTMS%MJ0XPUzzpy+(|H$| z%GCaf1(w=I1YAu-x&`C_@8S_%PVCLA7uDBMtP|z8jU~pJSYS-;^(j#86q0{ex2*D` z-YyVO{Zf!~ui+huG_LpAiv~Liaz?XJ^>A%(P%NwYYezkzE)GHDP&y(2LJ0Fz_sAWf zS1^t?O!v)D$JHz9Gpqk{&+0_KRi9XW-jnJ%tNUyva`13~=2{z?RBx%D)rFx8L_!!cl58m?00000NkvXXu0mjfUpoA) diff --git a/src/main/res/drawable-xhdpi/ic_missed_call_notification.png b/src/main/res/drawable-xhdpi/ic_missed_call_notification.png deleted file mode 100644 index 80cd15819fcca113e91cd6a57468a3062926bfed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1151 zcmV-_1c3XAP)dAf*T*3p6S`T|i9B zAdVfPHxWXzpo}6asy`rl&@Mz52ASp^8>>}brSqec?BB4RTxkz&Ie0o@W+3fAU zthM*r``8CItToL1=KGzQH8Xn#{^vy10o6-^<-h`9F>o4~0geJkB4W16`g%5?UI9D+ z+zH$YT+$}{EN~Fm1AGR29T8`G)leEW)b;9@>KVJ|nEJAM*&xN=t{&(?bTejZMlSBB z@DcSbb*^IZDM!@X`XPRWdZ>@WQ)bm?suaIU{c}L#Q{GUAdli3!dU77b=e(zOH&d$1 z)D!b4KIf%w#9yd>GvCT<%&8k&X&q{j{ycC?Y3KbRfH&18g>swCIDgM?qc`QXR_+=N zdho5a3B*X^GzxCB#hu_W<8@$hS6qxgdESD^SU)h&WkPk$S3N)+u0J zMC?`f0=t0KE%M%xzBKm0d~jAgKtxOeZ@0-mYO*GzJ+Pvi+RZjxuK?FItE@xuI`Or5 z4+5`uD7PH=zEQ}*0*lEbElgal{#bHhnpW2|#2Gi+PIqE`UM3wvBOP2fie6Tmxrm4p zz}>*l1&dDsYa?P$5^nJLQQF$P%^y+&2f7H9(yAx+QC-VJ){}qaFKK|U0O{?t zZfi~8qY+_599LIIgn5RV_-k@iDNY--^0mO=CiOESpyF^;`A0_1lL@^*42w`l`CgCU?W?kh)I&$o6}u zrzD71VTUS@8SJlk)??!*n{0mB(A6Sh= RVebF{002ovPDHLkV1lNL8CC!Q diff --git a/src/main/res/drawable-xxhdpi/ic_missed_call_notification.png b/src/main/res/drawable-xxhdpi/ic_missed_call_notification.png deleted file mode 100644 index 0072d2ef0d9253358efbc88d9ed7882b77e27be1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1680 zcmV;B25K~#90?VMe#9aR;_f2)*Iga|Dp7*dgvSQQ@l?1O;P!h?iB zga_ck05%vQKmq}R7-Fo65QO+Ze9Qry|-H(hO$9jHp+cATISWIz=Oc4qjWwYHUsBG z#PdxW*A_@!4m<*Uwn>K_u?09kBG%V!SW6)FNMHl7ylzKRz&7CQi1|NqO(&1})1$i6Z=QilzKGK(mQTY<^ zg%<2=08X#j<~-mBLu;uoHQ1OMX;W&T@1KmkMW%Q3Y^|-?Zm0UmtWOIv4jus<*T&6u z#BC9A6F@|)1-@6K?Ltxn??!eGI*|^)wvNbc5pm1lg`P;a*06C>_Ci*mHpYJ!+?f>z zKwShJU&GGlvKO+G^op8}RYE-rxTr$aIU!zwIg!)rb20FH;OrWsPQ2=3 z_2Ch17biogFE%uq73diC=Z;6Daf+j9ldw>rryCm06G(N&c10S;uo~ne%Qn!|M;jV0 z5=h$n2*V+9I4VMX|<-_e473`0DSoImYirw1K@Gyb2xk!@|NA=ZV0$tkB za20{H{&1Vbuo|RrZ@S;{%7%uk3Z&H|X_ArC=gw?@*){(;U@I`^wZBEy0HIzU5wrb^ z5izT-0R-MDdAIrv^KC>O&2buM}dCojGRK>X*qAuzG zfI}*@Sp)nmE689}^|9HDXI=ga{C#LG^+Mj1C*<59Gp6N! zQp2jxJw;lSB`;hCtgEbQd#nRK#ds&=K?F{Th-dQ(%LH0T$_*;thTQY{LquFqR9Lur zM8q!O3X;O}nK#M`OB1Lf;wj+XR^6FCN^>uVegiXQ)jvZz88{!=1uTt-_wovdCUHi@ zyTAp&o`1qGpM}6_MTM2pfP3d0u5HVsDP&bqLDf8IBI2jOciQk~3i)zTL6gHQKLoBH z$(`wACutsfKU+{ag8D?n4ZwBGujPM#3XXQw76>3B?gB0cc8uWEw2`~qHxdXSB4$Xc zzuDBrl#wgY$hknKtgIgam$YH8Lv{d5B4XR%#l{iTCnB~+#AT$V^2Jf@b;u&1(w1u@ zdaorX1K(iC`^^-yDtnoTX(yee{nI45OTz+l?R zcHn*BKfphMSAdPA?{?2e#77nTJB_qub0M&}qMK(%#1j?kP7c#n>k`NC-xAcN>UHWX z6{2Wu+x_oRsAs6Z>$5^1lsM)#-99O*9xYLvel%6zE2mO8{5Y| an(;C5E_<216#l;e0000X6P)W!p2+^2O#IMAcNF>G&N{Z0Lchpd60SQo$(n^|e5fFSsG}7AkQY^H+ ze?QDQk$XG4JG(P`cJDd+`?xzh^PJ~7^UUir105Y59gSg43s8illYu3`Qs5L|40ttg z9B?e~0&oDB0`>#{03HLLFtgoF$Xte289>qkUH*gp5doz0} zPRb!e(wL;nCH+>?e^oCBB>hU#XC=L=>EGTcl2%Ikk)#)DR(cjwl5Uc8ZqvFL7?S2m zx=7N44Jkc~hb3JmXk@R@8dM;#_q>nYNtE5O;Drs}WiY?0)NoO>zyC@_r zkaWGIL(S^4G6yAHC+QV+>Zz7)sHA1Ut-#yr$bZ0W2QD$QC+o;Od7~rgGMv!EpnQO{ zfE|)9tt0bf29Pvg(ha~5aii2>Vhq?Q>03(NQ%d53B)t;23HVfs{94XNU~Q@qb+iE_ zodDbhoE;NqBqhlsV3)>r+wbQ>UP0@wn~uUhC3 zatN3(v-`__SjqsB76UtRD^nda`5W*KGkdPgM`c~m_vnPyi6mK zj%Um)a}1TWm~a;H%AW*28kt8CzXaY~Q{LNwYk*sD+u^Hns-$IR_L%QwmI2O>D6r3q zX7*Hqd@_;rVoh1^1Xh{Z9!VGAmR)0va6T?(;V81&fD{IptLy|mXl8o=%xoKQ0WSEX zDw+B`$^gCP#&<^)Hka8BoYz-=p9DsDZ`KQ64LFVA+gKICcHC65cmAelgfCajd$A;M zq>s-4b9+~CHTph3bMj2ma^UL`Wi0hQ^fkiXgmu!JbNUkH*O^aEP<-w`A3)LyU^8(I z@iO3n*$;gNctb=Xl`ym2k|u#4FxbWZK`9?HW)*xcsFA^!nQa6v1r86W$WXWQ8{(Bu zRvtbBj3p?n5@xmuSVR3rn9qReLitdl2GrgKm9!ym zPi4;tR{%#67wzrm&ysBgZsToA47i3|4eY2cOuBcyH<2JQ1_dXZ&-yGTC|Qx^*0`NMKQ*(=ZhfW!dad@)Y7r@p+=f}rH@LI;N#4t% z2$}Z*_a+r59h-m|ZGzh4^ZWb>SW>A50GQc*xE@g(ncVrIL5kC3t7r2`0Ph*O*$y$bj_uJ7A169J3O?B7`lqKEHhwi$QlSE=I~ zM+Bq54JDaBF|!AH1^qQeenUoXL!4{^%giQm0m<7^>}V@xa}H)h{!DB2yW-v zI^c8UY$J?d9&NnjNYY7?)=RoiZVUIIcuCTolGaIjjYqCCBz;rTo`hw3r}0~Jqqd~e zft9#X^#pK~Vj`Hv?NHu|n@YbYaPj+3-j)h?wTwa!PqNIFx}jgs~U8sNI7Wgbc-jY+yv(vAWH#2k6n5rU+5 zO8UN}=VvK@N^U^fF$g3rl + + From a717917b3de98b8d88bed4145dae15fd968f5f00 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 29 Aug 2022 15:09:53 +0200 Subject: [PATCH 166/394] explicitly search for namespaces when processing stream features --- .../crypto/sasl/SaslMechanism.java | 4 ++ .../eu/siacs/conversations/xml/Namespace.java | 2 + .../conversations/xmpp/XmppConnection.java | 48 ++++++++++++++----- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java index 86fd6524e..f6024210a 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java @@ -66,4 +66,8 @@ public String getClientFirstMessage() { public String getResponse(final String challenge) throws AuthenticationException { return ""; } + + public enum Version { + SASL, SASL_2 + } } diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 72c35a92f..c2a7af607 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -7,6 +7,7 @@ public final class Namespace { public static final String BLOCKING = "urn:xmpp:blocking"; public static final String ROSTER = "jabber:iq:roster"; public static final String REGISTER = "jabber:iq:register"; + public static final String REGISTER_STREAM_FEATURE = "http://jabber.org/features/iq-register"; public static final String BYTE_STREAMS = "http://jabber.org/protocol/bytestreams"; public static final String HTTP_UPLOAD = "urn:xmpp:http:upload:0"; public static final String HTTP_UPLOAD_LEGACY = "urn:xmpp:http:upload"; @@ -15,6 +16,7 @@ public final class Namespace { public static final String DATA = "jabber:x:data"; public static final String OOB = "jabber:x:oob"; public static final String SASL = "urn:ietf:params:xml:ns:xmpp-sasl"; + public static final String SASL_2 = "urn:xmpp:sasl:1"; public static final String TLS = "urn:ietf:params:xml:ns:xmpp-tls"; public static final String PUBSUB = "http://jabber.org/protocol/pubsub"; public static final String PUBSUB_PUBLISH_OPTIONS = PUBSUB + "#publish-options"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 06195aaed..2222da3e2 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -848,40 +848,64 @@ private SSLSocket upgradeSocketToTls(final Socket socket) throws IOException { private void processStreamFeatures(final Tag currentTag) throws IOException { this.streamFeatures = tagReader.readElement(currentTag); - final boolean isSecure = features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion(); + Log.d(Config.LOGTAG, this.streamFeatures.toString()); + final boolean isSecure = + features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion(); final boolean needsBinding = !isBound && !account.isOptionSet(Account.OPTION_REGISTER); - if (this.streamFeatures.hasChild("starttls") && !features.encryptionEnabled) { + if (this.streamFeatures.hasChild("starttls", Namespace.TLS) + && !features.encryptionEnabled) { sendStartTLS(); - } else if (this.streamFeatures.hasChild("register") && account.isOptionSet(Account.OPTION_REGISTER)) { + } else if (this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE) + && account.isOptionSet(Account.OPTION_REGISTER)) { if (isSecure) { register(); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to find STARTTLS for registration process " + XmlHelper.printElementNames(this.streamFeatures)); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": unable to find STARTTLS for registration process " + + XmlHelper.printElementNames(this.streamFeatures)); throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } - } else if (!this.streamFeatures.hasChild("register") && account.isOptionSet(Account.OPTION_REGISTER)) { + } else if (!this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE) + && account.isOptionSet(Account.OPTION_REGISTER)) { throw new StateChangingException(Account.State.REGISTRATION_NOT_SUPPORTED); - } else if (this.streamFeatures.hasChild("mechanisms") && shouldAuthenticate && isSecure) { - authenticate(); - } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:" + smVersion) && streamId != null) { + } else if (this.streamFeatures.hasChild("mechanisms", Namespace.SASL_2) + && shouldAuthenticate + && isSecure) { + authenticate(SaslMechanism.Version.SASL_2); + } else if (this.streamFeatures.hasChild("mechanisms", Namespace.SASL) + && shouldAuthenticate + && isSecure) { + authenticate(SaslMechanism.Version.SASL); + } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:" + smVersion) + && streamId != null) { if (Config.EXTENDED_SM_LOGGING) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resuming after stanza #" + stanzasReceived); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": resuming after stanza #" + + stanzasReceived); } final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived, smVersion); this.mSmCatchupMessageCounter.set(0); this.mWaitingForSmCatchup.set(true); this.tagWriter.writeStanzaAsync(resume); } else if (needsBinding) { - if (this.streamFeatures.hasChild("bind") && isSecure) { + if (this.streamFeatures.hasChild("bind", Namespace.BIND) && isSecure) { sendBindRequest(); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to find bind feature " + XmlHelper.printElementNames(this.streamFeatures)); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": unable to find bind feature " + + XmlHelper.printElementNames(this.streamFeatures)); throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } } } - private void authenticate() throws IOException { + private void authenticate(final SaslMechanism.Version version) throws IOException { final List mechanisms = extractMechanisms(streamFeatures.findChild("mechanisms")); final Element auth = new Element("auth", Namespace.SASL); if (mechanisms.contains(External.MECHANISM) && account.getPrivateKeyAlias() != null) { From 5fc8ff899aa2f81b29a2e19bef52f17ad1f898ef Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 29 Aug 2022 17:09:52 +0200 Subject: [PATCH 167/394] support logging in via SASL 2 --- .../crypto/sasl/SaslMechanism.java | 17 +- .../conversations/xmpp/XmppConnection.java | 160 ++++++++++++------ 2 files changed, 121 insertions(+), 56 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java index f6024210a..b255b6f42 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java @@ -1,8 +1,12 @@ package eu.siacs.conversations.crypto.sasl; +import com.google.common.base.Strings; + import java.security.SecureRandom; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xml.TagWriter; public abstract class SaslMechanism { @@ -68,6 +72,17 @@ public String getResponse(final String challenge) throws AuthenticationException } public enum Version { - SASL, SASL_2 + SASL, SASL_2; + + public static Version of(final Element element) { + switch ( Strings.nullToEmpty(element.getNamespace())) { + case Namespace.SASL: + return SASL; + case Namespace.SASL_2: + return SASL_2; + default: + throw new IllegalArgumentException("Unrecognized SASL namespace"); + } + } } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 2222da3e2..bc77246e8 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -469,63 +469,102 @@ private void processStream() throws XmlPullParserException, IOException { } else if (nextTag.isStart("proceed")) { switchOverToTls(); } else if (nextTag.isStart("success")) { - final String challenge = tagReader.readElement(nextTag).getContent(); + final Element success = tagReader.readElement(nextTag); + final SaslMechanism.Version version; + try { + version = SaslMechanism.Version.of(success); + } catch (final IllegalArgumentException e) { + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } + final String challenge; + if (version == SaslMechanism.Version.SASL) { + challenge = success.getContent(); + } else if (version == SaslMechanism.Version.SASL_2) { + challenge = success.findChildContent("additional-data"); + } else { + throw new AssertionError("Missing implementation for " + version); + } try { saslMechanism.getResponse(challenge); } catch (final SaslMechanism.AuthenticationException e) { Log.e(Config.LOGTAG, String.valueOf(e)); throw new StateChangingException(Account.State.UNAUTHORIZED); } - Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": logged in"); - account.setKey(Account.PINNED_MECHANISM_KEY, - String.valueOf(saslMechanism.getPriority())); - tagReader.reset(); - sendStartStream(); - final Tag tag = tagReader.readTag(); - if (tag != null && tag.isStart("stream")) { - processStream(); - } else { - throw new StateChangingException(Account.State.STREAM_OPENING_ERROR); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid().toString() + + ": logged in (using " + + version + + ")"); + account.setKey( + Account.PINNED_MECHANISM_KEY, String.valueOf(saslMechanism.getPriority())); + if (version == SaslMechanism.Version.SASL) { + tagReader.reset(); + sendStartStream(); + final Tag tag = tagReader.readTag(); + if (tag != null && tag.isStart("stream")) { + processStream(); + } else { + throw new StateChangingException(Account.State.STREAM_OPENING_ERROR); + } + break; } - break; } else if (nextTag.isStart("failure")) { final Element failure = tagReader.readElement(nextTag); - if (Namespace.SASL.equals(failure.getNamespace())) { - if (failure.hasChild("temporary-auth-failure")) { - throw new StateChangingException(Account.State.TEMPORARY_AUTH_FAILURE); - } else if (failure.hasChild("account-disabled")) { - final String text = failure.findChildContent("text"); - if ( Strings.isNullOrEmpty(text)) { + if (Namespace.TLS.equals(failure.getNamespace())) { + throw new StateChangingException(Account.State.TLS_ERROR); + } + final SaslMechanism.Version version; + try { + version = SaslMechanism.Version.of(failure); + } catch (final IllegalArgumentException e) { + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": login failure " + version); + if (failure.hasChild("temporary-auth-failure")) { + throw new StateChangingException(Account.State.TEMPORARY_AUTH_FAILURE); + } else if (failure.hasChild("account-disabled")) { + final String text = failure.findChildContent("text"); + if (Strings.isNullOrEmpty(text)) { + throw new StateChangingException(Account.State.UNAUTHORIZED); + } + final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(text); + if (matcher.find()) { + final HttpUrl url; + try { + url = HttpUrl.get(text.substring(matcher.start(), matcher.end())); + } catch (final IllegalArgumentException e) { throw new StateChangingException(Account.State.UNAUTHORIZED); } - final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(text); - if (matcher.find()) { - final HttpUrl url; - try { - url = HttpUrl.get(text.substring(matcher.start(), matcher.end())); - } catch (final IllegalArgumentException e) { - throw new StateChangingException(Account.State.UNAUTHORIZED); - } - if (url.isHttps()) { - this.redirectionUrl = url; - throw new StateChangingException(Account.State.PAYMENT_REQUIRED); - } + if (url.isHttps()) { + this.redirectionUrl = url; + throw new StateChangingException(Account.State.PAYMENT_REQUIRED); } } - throw new StateChangingException(Account.State.UNAUTHORIZED); - } else if (Namespace.TLS.equals(failure.getNamespace())) { - throw new StateChangingException(Account.State.TLS_ERROR); - } else { - throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } + throw new StateChangingException(Account.State.UNAUTHORIZED); } else if (nextTag.isStart("challenge")) { - final String challenge = tagReader.readElement(nextTag).getContent(); - final Element response = new Element("response", Namespace.SASL); + final Element challenge = tagReader.readElement(nextTag); + final SaslMechanism.Version version; + try { + version = SaslMechanism.Version.of(challenge); + } catch (final IllegalArgumentException e) { + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } + final Element response; + if (version == SaslMechanism.Version.SASL) { + response = new Element("response", Namespace.SASL); + } else if (version == SaslMechanism.Version.SASL_2) { + response = new Element("response", Namespace.SASL_2); + } else { + throw new AssertionError("Missing implementation for " + version); + } try { - response.setContent(saslMechanism.getResponse(challenge)); + response.setContent(saslMechanism.getResponse(challenge.getContent())); } catch (final SaslMechanism.AuthenticationException e) { // TODO: Send auth abort tag. Log.e(Config.LOGTAG, e.toString()); + throw new StateChangingException(Account.State.UNAUTHORIZED); } tagWriter.writeElement(response); } else if (nextTag.isStart("enabled")) { @@ -848,7 +887,6 @@ private SSLSocket upgradeSocketToTls(final Socket socket) throws IOException { private void processStreamFeatures(final Tag currentTag) throws IOException { this.streamFeatures = tagReader.readElement(currentTag); - Log.d(Config.LOGTAG, this.streamFeatures.toString()); final boolean isSecure = features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion(); final boolean needsBinding = !isBound && !account.isOptionSet(Account.OPTION_REGISTER); @@ -907,7 +945,6 @@ private void processStreamFeatures(final Tag currentTag) throws IOException { private void authenticate(final SaslMechanism.Version version) throws IOException { final List mechanisms = extractMechanisms(streamFeatures.findChild("mechanisms")); - final Element auth = new Element("auth", Namespace.SASL); if (mechanisms.contains(External.MECHANISM) && account.getPrivateKeyAlias() != null) { saslMechanism = new External(tagWriter, account, mXmppConnectionService.getRNG()); } else if (mechanisms.contains(ScramSha512.MECHANISM)) { @@ -923,25 +960,38 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio } else if (mechanisms.contains(Anonymous.MECHANISM)) { saslMechanism = new Anonymous(tagWriter, account, mXmppConnectionService.getRNG()); } - if (saslMechanism != null) { - final int pinnedMechanism = account.getKeyAsInt(Account.PINNED_MECHANISM_KEY, -1); - if (pinnedMechanism > saslMechanism.getPriority()) { - Log.e(Config.LOGTAG, "Auth failed. Authentication mechanism " + saslMechanism.getMechanism() + - " has lower priority (" + saslMechanism.getPriority() + - ") than pinned priority (" + pinnedMechanism + - "). Possible downgrade attack?"); - throw new StateChangingException(Account.State.DOWNGRADE_ATTACK); + if (saslMechanism == null) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to find supported SASL mechanism in " + mechanisms); + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } + final int pinnedMechanism = account.getKeyAsInt(Account.PINNED_MECHANISM_KEY, -1); + if (pinnedMechanism > saslMechanism.getPriority()) { + Log.e(Config.LOGTAG, "Auth failed. Authentication mechanism " + saslMechanism.getMechanism() + + " has lower priority (" + saslMechanism.getPriority() + + ") than pinned priority (" + pinnedMechanism + + "). Possible downgrade attack?"); + throw new StateChangingException(Account.State.DOWNGRADE_ATTACK); + } + final String firstMessage = saslMechanism.getClientFirstMessage(); + final Element authenticate; + if (version == SaslMechanism.Version.SASL) { + authenticate = new Element("auth", Namespace.SASL); + if (!Strings.isNullOrEmpty(firstMessage)) { + authenticate.setContent(firstMessage); } - Log.d(Config.LOGTAG, account.getJid().toString() + ": Authenticating with " + saslMechanism.getMechanism()); - auth.setAttribute("mechanism", saslMechanism.getMechanism()); - if (!saslMechanism.getClientFirstMessage().isEmpty()) { - auth.setContent(saslMechanism.getClientFirstMessage()); + } else if (version == SaslMechanism.Version.SASL_2) { + authenticate = new Element("authenticate", Namespace.SASL_2); + if (!Strings.isNullOrEmpty(firstMessage)) { + authenticate.addChild("initial-response").setContent(firstMessage); } - tagWriter.writeElement(auth); + // TODO place to add extensions } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to find supported SASL mechanism in " + mechanisms); - throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + throw new AssertionError("Missing implementation for " + version); } + + Log.d(Config.LOGTAG, account.getJid().toString() + ": Authenticating with "+version+ "/" + saslMechanism.getMechanism()); + authenticate.setAttribute("mechanism", saslMechanism.getMechanism()); + tagWriter.writeElement(authenticate); } private List extractMechanisms(final Element stream) { From 6202cbe26b086f9febdc5a9aa3515ccf643b2301 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 29 Aug 2022 18:40:49 +0200 Subject: [PATCH 168/394] minor code clean up for tag and element --- .../eu/siacs/conversations/xml/Element.java | 405 +++++++++--------- .../java/eu/siacs/conversations/xml/Tag.java | 185 ++++---- .../conversations/xmpp/XmppConnection.java | 14 +- 3 files changed, 302 insertions(+), 302 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xml/Element.java b/src/main/java/eu/siacs/conversations/xml/Element.java index 4d53a17b7..9c570df93 100644 --- a/src/main/java/eu/siacs/conversations/xml/Element.java +++ b/src/main/java/eu/siacs/conversations/xml/Element.java @@ -12,207 +12,206 @@ import eu.siacs.conversations.xmpp.stanzas.MessagePacket; public class Element { - private final String name; - private Hashtable attributes = new Hashtable<>(); - private String content; - protected List children = new ArrayList<>(); - - public Element(String name) { - this.name = name; - } - - public Element(String name, String xmlns) { - this.name = name; - this.setAttribute("xmlns", xmlns); - } - - public Element addChild(Element child) { - this.content = null; - children.add(child); - return child; - } - - public Element addChild(String name) { - this.content = null; - Element child = new Element(name); - children.add(child); - return child; - } - - public Element addChild(String name, String xmlns) { - this.content = null; - Element child = new Element(name); - child.setAttribute("xmlns", xmlns); - children.add(child); - return child; - } - - public Element setContent(String content) { - this.content = content; - this.children.clear(); - return this; - } - - public Element findChild(String name) { - for (Element child : this.children) { - if (child.getName().equals(name)) { - return child; - } - } - return null; - } - - public String findChildContent(String name) { - Element element = findChild(name); - return element == null ? null : element.getContent(); - } - - public LocalizedContent findInternationalizedChildContentInDefaultNamespace(String name) { - return LocalizedContent.get(this, name); - } - - public Element findChild(String name, String xmlns) { - for (Element child : this.children) { - if (name.equals(child.getName()) && xmlns.equals(child.getAttribute("xmlns"))) { - return child; - } - } - return null; - } - - public Element findChildEnsureSingle(String name, String xmlns) { - final List results = new ArrayList<>(); - for (Element child : this.children) { - if (name.equals(child.getName()) && xmlns.equals(child.getAttribute("xmlns"))) { - results.add(child); - } - } - if (results.size() == 1) { - return results.get(0); - } - return null; - } - - public String findChildContent(String name, String xmlns) { - Element element = findChild(name,xmlns); - return element == null ? null : element.getContent(); - } - - public boolean hasChild(final String name) { - return findChild(name) != null; - } - - public boolean hasChild(final String name, final String xmlns) { - return findChild(name, xmlns) != null; - } - - public List getChildren() { - return this.children; - } - - public Element setChildren(List children) { - this.children = children; - return this; - } - - public final String getContent() { - return content; - } - - public Element setAttribute(String name, String value) { - if (name != null && value != null) { - this.attributes.put(name, value); - } - return this; - } - - public Element setAttribute(String name, Jid value) { - if (name != null && value != null) { - this.attributes.put(name, value.toEscapedString()); - } - return this; - } - - public Element removeAttribute(String name) { - this.attributes.remove(name); - return this; - } - - public Element setAttributes(Hashtable attributes) { - this.attributes = attributes; - return this; - } - - public String getAttribute(String name) { - if (this.attributes.containsKey(name)) { - return this.attributes.get(name); - } else { - return null; - } - } - - public Jid getAttributeAsJid(String name) { - final String jid = this.getAttribute(name); - if (jid != null && !jid.isEmpty()) { - try { - return Jid.ofEscaped(jid); - } catch (final IllegalArgumentException e) { - return InvalidJid.of(jid, this instanceof MessagePacket); - } - } - return null; - } - - public Hashtable getAttributes() { - return this.attributes; - } - - @NotNull - public String toString() { - final StringBuilder elementOutput = new StringBuilder(); - if ((content == null) && (children.size() == 0)) { - Tag emptyTag = Tag.empty(name); - emptyTag.setAtttributes(this.attributes); - elementOutput.append(emptyTag.toString()); - } else { - Tag startTag = Tag.start(name); - startTag.setAtttributes(this.attributes); - elementOutput.append(startTag); - if (content != null) { - elementOutput.append(XmlHelper.encodeEntities(content)); - } else { - for (Element child : children) { - elementOutput.append(child.toString()); - } - } - Tag endTag = Tag.end(name); - elementOutput.append(endTag); - } - return elementOutput.toString(); - } - - public final String getName() { - return name; - } - - public void clearChildren() { - this.children.clear(); - } - - public void setAttribute(String name, long value) { - this.setAttribute(name, Long.toString(value)); - } - - public void setAttribute(String name, int value) { - this.setAttribute(name, Integer.toString(value)); - } - - public boolean getAttributeAsBoolean(String name) { - String attr = getAttribute(name); - return (attr != null && (attr.equalsIgnoreCase("true") || attr.equalsIgnoreCase("1"))); - } - - public String getNamespace() { - return getAttribute("xmlns"); - } + private final String name; + private Hashtable attributes = new Hashtable<>(); + private String content; + protected List children = new ArrayList<>(); + + public Element(String name) { + this.name = name; + } + + public Element(String name, String xmlns) { + this.name = name; + this.setAttribute("xmlns", xmlns); + } + + public Element addChild(Element child) { + this.content = null; + children.add(child); + return child; + } + + public Element addChild(String name) { + this.content = null; + Element child = new Element(name); + children.add(child); + return child; + } + + public Element addChild(String name, String xmlns) { + this.content = null; + Element child = new Element(name); + child.setAttribute("xmlns", xmlns); + children.add(child); + return child; + } + + public Element setContent(String content) { + this.content = content; + this.children.clear(); + return this; + } + + public Element findChild(String name) { + for (Element child : this.children) { + if (child.getName().equals(name)) { + return child; + } + } + return null; + } + + public String findChildContent(String name) { + Element element = findChild(name); + return element == null ? null : element.getContent(); + } + + public LocalizedContent findInternationalizedChildContentInDefaultNamespace(String name) { + return LocalizedContent.get(this, name); + } + + public Element findChild(String name, String xmlns) { + for (Element child : this.children) { + if (name.equals(child.getName()) && xmlns.equals(child.getAttribute("xmlns"))) { + return child; + } + } + return null; + } + + public Element findChildEnsureSingle(String name, String xmlns) { + final List results = new ArrayList<>(); + for (Element child : this.children) { + if (name.equals(child.getName()) && xmlns.equals(child.getAttribute("xmlns"))) { + results.add(child); + } + } + if (results.size() == 1) { + return results.get(0); + } + return null; + } + + public String findChildContent(String name, String xmlns) { + Element element = findChild(name, xmlns); + return element == null ? null : element.getContent(); + } + + public boolean hasChild(final String name) { + return findChild(name) != null; + } + + public boolean hasChild(final String name, final String xmlns) { + return findChild(name, xmlns) != null; + } + + public List getChildren() { + return this.children; + } + + public Element setChildren(List children) { + this.children = children; + return this; + } + + public final String getContent() { + return content; + } + + public Element setAttribute(String name, String value) { + if (name != null && value != null) { + this.attributes.put(name, value); + } + return this; + } + + public Element setAttribute(String name, Jid value) { + if (name != null && value != null) { + this.attributes.put(name, value.toEscapedString()); + } + return this; + } + + public void removeAttribute(final String name) { + this.attributes.remove(name); + } + + public Element setAttributes(Hashtable attributes) { + this.attributes = attributes; + return this; + } + + public String getAttribute(String name) { + if (this.attributes.containsKey(name)) { + return this.attributes.get(name); + } else { + return null; + } + } + + public Jid getAttributeAsJid(String name) { + final String jid = this.getAttribute(name); + if (jid != null && !jid.isEmpty()) { + try { + return Jid.ofEscaped(jid); + } catch (final IllegalArgumentException e) { + return InvalidJid.of(jid, this instanceof MessagePacket); + } + } + return null; + } + + public Hashtable getAttributes() { + return this.attributes; + } + + @NotNull + public String toString() { + final StringBuilder elementOutput = new StringBuilder(); + if ((content == null) && (children.size() == 0)) { + final Tag emptyTag = Tag.empty(name); + emptyTag.setAttributes(this.attributes); + elementOutput.append(emptyTag); + } else { + final Tag startTag = Tag.start(name); + startTag.setAttributes(this.attributes); + elementOutput.append(startTag); + if (content != null) { + elementOutput.append(XmlHelper.encodeEntities(content)); + } else { + for (final Element child : children) { + elementOutput.append(child.toString()); + } + } + final Tag endTag = Tag.end(name); + elementOutput.append(endTag); + } + return elementOutput.toString(); + } + + public final String getName() { + return name; + } + + public void clearChildren() { + this.children.clear(); + } + + public void setAttribute(String name, long value) { + this.setAttribute(name, Long.toString(value)); + } + + public void setAttribute(String name, int value) { + this.setAttribute(name, Integer.toString(value)); + } + + public boolean getAttributeAsBoolean(String name) { + String attr = getAttribute(name); + return (attr != null && (attr.equalsIgnoreCase("true") || attr.equalsIgnoreCase("1"))); + } + + public String getNamespace() { + return getAttribute("xmlns"); + } } diff --git a/src/main/java/eu/siacs/conversations/xml/Tag.java b/src/main/java/eu/siacs/conversations/xml/Tag.java index b9ef979ff..2e6098522 100644 --- a/src/main/java/eu/siacs/conversations/xml/Tag.java +++ b/src/main/java/eu/siacs/conversations/xml/Tag.java @@ -1,104 +1,101 @@ package eu.siacs.conversations.xml; +import org.jetbrains.annotations.NotNull; + import java.util.Hashtable; -import java.util.Iterator; import java.util.Map.Entry; import java.util.Set; import eu.siacs.conversations.utils.XmlHelper; public class Tag { - public static final int NO = -1; - public static final int START = 0; - public static final int END = 1; - public static final int EMPTY = 2; - - protected int type; - protected String name; - protected Hashtable attributes = new Hashtable(); - - protected Tag(int type, String name) { - this.type = type; - this.name = name; - } - - public static Tag no(String text) { - return new Tag(NO, text); - } - - public static Tag start(String name) { - return new Tag(START, name); - } - - public static Tag end(String name) { - return new Tag(END, name); - } - - public static Tag empty(String name) { - return new Tag(EMPTY, name); - } - - public String getName() { - return name; - } - - public String getAttribute(String attrName) { - return this.attributes.get(attrName); - } - - public Tag setAttribute(String attrName, String attrValue) { - this.attributes.put(attrName, attrValue); - return this; - } - - public Tag setAtttributes(Hashtable attributes) { - this.attributes = attributes; - return this; - } - - public boolean isStart(String needle) { - if (needle == null) - return false; - return (this.type == START) && (needle.equals(this.name)); - } - - public boolean isEnd(String needle) { - if (needle == null) - return false; - return (this.type == END) && (needle.equals(this.name)); - } - - public boolean isNo() { - return (this.type == NO); - } - - public String toString() { - StringBuilder tagOutput = new StringBuilder(); - tagOutput.append('<'); - if (type == END) { - tagOutput.append('/'); - } - tagOutput.append(name); - if (type != END) { - Set> attributeSet = attributes.entrySet(); - Iterator> it = attributeSet.iterator(); - while (it.hasNext()) { - Entry entry = it.next(); - tagOutput.append(' '); - tagOutput.append(entry.getKey()); - tagOutput.append("=\""); - tagOutput.append(XmlHelper.encodeEntities(entry.getValue())); - tagOutput.append('"'); - } - } - if (type == EMPTY) { - tagOutput.append('/'); - } - tagOutput.append('>'); - return tagOutput.toString(); - } - - public Hashtable getAttributes() { - return this.attributes; - } + public static final int NO = -1; + public static final int START = 0; + public static final int END = 1; + public static final int EMPTY = 2; + + protected int type; + protected String name; + protected Hashtable attributes = new Hashtable(); + + protected Tag(int type, String name) { + this.type = type; + this.name = name; + } + + public static Tag no(String text) { + return new Tag(NO, text); + } + + public static Tag start(String name) { + return new Tag(START, name); + } + + public static Tag end(String name) { + return new Tag(END, name); + } + + public static Tag empty(String name) { + return new Tag(EMPTY, name); + } + + public String getName() { + return name; + } + + public String getAttribute(final String attrName) { + return this.attributes.get(attrName); + } + + public Tag setAttribute(final String attrName, final String attrValue) { + this.attributes.put(attrName, attrValue); + return this; + } + + public void setAttributes(final Hashtable attributes) { + this.attributes = attributes; + } + + public boolean isStart(String needle) { + if (needle == null) return false; + return (this.type == START) && (needle.equals(this.name)); + } + + public boolean isEnd(String needle) { + if (needle == null) return false; + return (this.type == END) && (needle.equals(this.name)); + } + + public boolean isNo() { + return (this.type == NO); + } + + @NotNull + public String toString() { + final StringBuilder tagOutput = new StringBuilder(); + tagOutput.append('<'); + if (type == END) { + tagOutput.append('/'); + } + tagOutput.append(name); + if (type != END) { + final Set> attributeSet = attributes.entrySet(); + for (final Entry entry : attributeSet) { + tagOutput.append(' '); + tagOutput.append(entry.getKey()); + tagOutput.append("=\""); + tagOutput.append(XmlHelper.encodeEntities(entry.getValue())); + tagOutput.append('"'); + } + } + if (type == EMPTY) { + tagOutput.append('/'); + } + tagOutput.append('>'); + return tagOutput.toString(); + } + + public Hashtable getAttributes() { + return this.attributes; + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index bc77246e8..bd11fcbe2 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -498,6 +498,10 @@ private void processStream() throws XmlPullParserException, IOException { + ")"); account.setKey( Account.PINNED_MECHANISM_KEY, String.valueOf(saslMechanism.getPriority())); + if (version == SaslMechanism.Version.SASL_2) { + final String authorizationIdentifier = success.findChildContent("authorization-identifier"); + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": SASL 2.0 authorization identifier was "+authorizationIdentifier); + } if (version == SaslMechanism.Version.SASL) { tagReader.reset(); sendStartStream(); @@ -1179,7 +1183,7 @@ private void sendBindRequest() { Log.d(Config.LOGTAG, account.getJid() + ": disconnecting because of bind failure. (no jid)"); } } else { - Log.d(Config.LOGTAG, account.getJid() + ": disconnecting because of bind failure (" + packet.toString()); + Log.d(Config.LOGTAG, account.getJid() + ": disconnecting because of bind failure (" + packet); } final Element error = packet.findChild("error"); if (packet.getType() == IqPacket.TYPE.ERROR && error != null && error.hasChild("conflict")) { @@ -1449,7 +1453,7 @@ private void sendEnableCarbons() { features.carbonsEnabled = true; } else { Log.d(Config.LOGTAG, account.getJid().asBareJid() - + ": error enableing carbons " + packet.toString()); + + ": could not enable carbons " + packet); } }); } @@ -1472,7 +1476,7 @@ private void processStreamError(final Tag currentTag) throws IOException { failPendingMessages(text); throw new StateChangingException(Account.State.POLICY_VIOLATION); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream error " + streamError.toString()); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream error " + streamError); throw new StateChangingException(Account.State.STREAM_ERROR); } } @@ -1839,8 +1843,8 @@ public X509Certificate[] getCertificateChain(String alias) { Log.d(Config.LOGTAG, "getting certificate chain"); try { return KeyChain.getCertificateChain(mXmppConnectionService, alias); - } catch (Exception e) { - Log.d(Config.LOGTAG, e.getMessage()); + } catch (final Exception e) { + Log.d(Config.LOGTAG, "could not get certificate chain", e); return new X509Certificate[0]; } } From 928a16d31d215a4eda2ba4e03d84e8651301a2e3 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 29 Aug 2022 18:53:34 +0200 Subject: [PATCH 169/394] abort on 'continue' - no client support --- .../siacs/conversations/entities/Account.java | 3 + .../java/eu/siacs/conversations/xml/Tag.java | 10 ++- .../conversations/xmpp/XmppConnection.java | 84 ++++++++++++++----- src/main/res/values/strings.xml | 1 + 4 files changed, 75 insertions(+), 23 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index dc354adc4..9220cc192 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -640,6 +640,7 @@ public enum State { TLS_ERROR, TLS_ERROR_DOMAIN, INCOMPATIBLE_SERVER, + INCOMPATIBLE_CLIENT, TOR_NOT_AVAILABLE, DOWNGRADE_ATTACK, SESSION_FAILURE, @@ -709,6 +710,8 @@ public int getReadableId() { return R.string.account_status_tls_error_domain; case INCOMPATIBLE_SERVER: return R.string.account_status_incompatible_server; + case INCOMPATIBLE_CLIENT: + return R.string.account_status_incompatible_client; case TOR_NOT_AVAILABLE: return R.string.account_status_tor_unavailable; case BIND_FAILURE: diff --git a/src/main/java/eu/siacs/conversations/xml/Tag.java b/src/main/java/eu/siacs/conversations/xml/Tag.java index 2e6098522..db2b11172 100644 --- a/src/main/java/eu/siacs/conversations/xml/Tag.java +++ b/src/main/java/eu/siacs/conversations/xml/Tag.java @@ -56,11 +56,17 @@ public void setAttributes(final Hashtable attributes) { this.attributes = attributes; } - public boolean isStart(String needle) { - if (needle == null) return false; + public boolean isStart(final String needle) { + if (needle == null) { + return false; + } return (this.type == START) && (needle.equals(this.name)); } + public boolean isStart(final String name, final String namespace) { + return isStart(name) && namespace != null && namespace.equals(this.getAttribute("xmlns")); + } + public boolean isEnd(String needle) { if (needle == null) return false; return (this.type == END) && (needle.equals(this.name)); diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index bd11fcbe2..0fbb85768 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -466,7 +466,7 @@ private void processStream() throws XmlPullParserException, IOException { processStreamError(nextTag); } else if (nextTag.isStart("features")) { processStreamFeatures(nextTag); - } else if (nextTag.isStart("proceed")) { + } else if (nextTag.isStart("proceed", Namespace.TLS)) { switchOverToTls(); } else if (nextTag.isStart("success")) { final Element success = tagReader.readElement(nextTag); @@ -499,8 +499,13 @@ private void processStream() throws XmlPullParserException, IOException { account.setKey( Account.PINNED_MECHANISM_KEY, String.valueOf(saslMechanism.getPriority())); if (version == SaslMechanism.Version.SASL_2) { - final String authorizationIdentifier = success.findChildContent("authorization-identifier"); - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": SASL 2.0 authorization identifier was "+authorizationIdentifier); + final String authorizationIdentifier = + success.findChildContent("authorization-identifier"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": SASL 2.0 authorization identifier was " + + authorizationIdentifier); } if (version == SaslMechanism.Version.SASL) { tagReader.reset(); @@ -513,11 +518,10 @@ private void processStream() throws XmlPullParserException, IOException { } break; } + } else if (nextTag.isStart("failure", Namespace.TLS)) { + throw new StateChangingException(Account.State.TLS_ERROR); } else if (nextTag.isStart("failure")) { final Element failure = tagReader.readElement(nextTag); - if (Namespace.TLS.equals(failure.getNamespace())) { - throw new StateChangingException(Account.State.TLS_ERROR); - } final SaslMechanism.Version version; try { version = SaslMechanism.Version.of(failure); @@ -547,6 +551,8 @@ private void processStream() throws XmlPullParserException, IOException { } } throw new StateChangingException(Account.State.UNAUTHORIZED); + } else if (nextTag.isStart("continue", Namespace.SASL_2)) { + throw new StateChangingException(Account.State.INCOMPATIBLE_CLIENT); } else if (nextTag.isStart("challenge")) { final Element challenge = tagReader.readElement(nextTag); final SaslMechanism.Version version; @@ -575,12 +581,19 @@ private void processStream() throws XmlPullParserException, IOException { final Element enabled = tagReader.readElement(nextTag); if ("true".equals(enabled.getAttribute("resume"))) { this.streamId = enabled.getAttribute("id"); - Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() - + ": stream management(" + smVersion - + ") enabled (resumable)"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid().toString() + + ": stream management(" + + smVersion + + ") enabled (resumable)"); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() - + ": stream management(" + smVersion + ") enabled"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid().toString() + + ": stream management(" + + smVersion + + ") enabled"); } this.stanzasReceived = 0; this.inSmacksSession = true; @@ -599,11 +612,15 @@ private void processStream() throws XmlPullParserException, IOException { synchronized (this.mStanzaQueue) { final int serverCount = Integer.parseInt(h); if (serverCount < stanzasSent) { - Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() - + ": session resumed with lost packages"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid().toString() + + ": session resumed with lost packages"); stanzasSent = serverCount; } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": session resumed"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid().toString() + ": session resumed"); } acknowledgedMessages = acknowledgeStanzaUpTo(serverCount); for (int i = 0; i < this.mStanzaQueue.size(); ++i) { @@ -618,7 +635,8 @@ private void processStream() throws XmlPullParserException, IOException { for (AbstractAcknowledgeableStanza packet : failedStanzas) { if (packet instanceof MessagePacket) { MessagePacket message = (MessagePacket) packet; - mXmppConnectionService.markMessage(account, + mXmppConnectionService.markMessage( + account, message.getTo().asBareJid(), message.getId(), Message.STATUS_UNSEND); @@ -627,12 +645,20 @@ private void processStream() throws XmlPullParserException, IOException { } } catch (final NumberFormatException ignored) { } - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": online with resource " + account.getResource()); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": online with resource " + + account.getResource()); changeStatus(Account.State.ONLINE); } else if (nextTag.isStart("r")) { tagReader.readElement(nextTag); if (Config.EXTENDED_SM_LOGGING) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": acknowledging stanza #" + this.stanzasReceived); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": acknowledging stanza #" + + this.stanzasReceived); } final AckPacket ack = new AckPacket(this.stanzasReceived, smVersion); tagWriter.writeStanzaAsync(ack); @@ -642,10 +668,19 @@ private void processStream() throws XmlPullParserException, IOException { if (mWaitingForSmCatchup.compareAndSet(true, false)) { final int messageCount = mSmCatchupMessageCounter.get(); final int pendingIQs = packetCallbacks.size(); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": SM catchup complete (messages=" + messageCount + ", pending IQs=" + pendingIQs + ")"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": SM catchup complete (messages=" + + messageCount + + ", pending IQs=" + + pendingIQs + + ")"); accountUiNeedsRefresh = true; if (messageCount > 0) { - mXmppConnectionService.getNotificationService().finishBacklog(true, account); + mXmppConnectionService + .getNotificationService() + .finishBacklog(true, account); } } } @@ -664,13 +699,20 @@ private void processStream() throws XmlPullParserException, IOException { mXmppConnectionService.updateConversationUi(); } } catch (NumberFormatException | NullPointerException e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server send ack without sequence number"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": server send ack without sequence number"); } } else if (nextTag.isStart("failed")) { Element failed = tagReader.readElement(nextTag); try { final int serverCount = Integer.parseInt(failed.getAttribute("h")); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resumption failed but server acknowledged stanza #" + serverCount); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": resumption failed but server acknowledged stanza #" + + serverCount); final boolean acknowledgedMessages; synchronized (this.mStanzaQueue) { acknowledgedMessages = acknowledgeStanzaUpTo(serverCount); diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index c51be907e..cd4412588 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -168,6 +168,7 @@ Domain not verifiable Policy violation Incompatible server + Incompatible client Stream error Stream opening error Unencrypted From f6ab3dd068771394a48e11f25a09e30663adcfe3 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 29 Aug 2022 19:22:25 +0200 Subject: [PATCH 170/394] support resume via sasl 2.0 --- .../conversations/xmpp/XmppConnection.java | 125 ++++++++++-------- 1 file changed, 73 insertions(+), 52 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 0fbb85768..bddcb197a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -506,6 +506,10 @@ private void processStream() throws XmlPullParserException, IOException { account.getJid().asBareJid() + ": SASL 2.0 authorization identifier was " + authorizationIdentifier); + final Element resumed = success.findChild("resumed", "urn:xmpp:sm:3"); + if (resumed != null && streamId != null) { + processResumed(resumed); + } } if (version == SaslMechanism.Version.SASL) { tagReader.reset(); @@ -579,7 +583,7 @@ private void processStream() throws XmlPullParserException, IOException { tagWriter.writeElement(response); } else if (nextTag.isStart("enabled")) { final Element enabled = tagReader.readElement(nextTag); - if ("true".equals(enabled.getAttribute("resume"))) { + if (enabled.getAttributeAsBoolean("resume")) { this.streamId = enabled.getAttribute("id"); Log.d( Config.LOGTAG, @@ -600,57 +604,8 @@ private void processStream() throws XmlPullParserException, IOException { final RequestPacket r = new RequestPacket(smVersion); tagWriter.writeStanzaAsync(r); } else if (nextTag.isStart("resumed")) { - this.inSmacksSession = true; - this.isBound = true; - this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion)); - lastPacketReceived = SystemClock.elapsedRealtime(); final Element resumed = tagReader.readElement(nextTag); - final String h = resumed.getAttribute("h"); - try { - ArrayList failedStanzas = new ArrayList<>(); - final boolean acknowledgedMessages; - synchronized (this.mStanzaQueue) { - final int serverCount = Integer.parseInt(h); - if (serverCount < stanzasSent) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid().toString() - + ": session resumed with lost packages"); - stanzasSent = serverCount; - } else { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid().toString() + ": session resumed"); - } - acknowledgedMessages = acknowledgeStanzaUpTo(serverCount); - for (int i = 0; i < this.mStanzaQueue.size(); ++i) { - failedStanzas.add(mStanzaQueue.valueAt(i)); - } - mStanzaQueue.clear(); - } - if (acknowledgedMessages) { - mXmppConnectionService.updateConversationUi(); - } - Log.d(Config.LOGTAG, "resending " + failedStanzas.size() + " stanzas"); - for (AbstractAcknowledgeableStanza packet : failedStanzas) { - if (packet instanceof MessagePacket) { - MessagePacket message = (MessagePacket) packet; - mXmppConnectionService.markMessage( - account, - message.getTo().asBareJid(), - message.getId(), - Message.STATUS_UNSEND); - } - sendPacket(packet); - } - } catch (final NumberFormatException ignored) { - } - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": online with resource " - + account.getResource()); - changeStatus(Account.State.ONLINE); + processResumed(resumed); } else if (nextTag.isStart("r")) { tagReader.readElement(nextTag); if (Config.EXTENDED_SM_LOGGING) { @@ -739,6 +694,59 @@ private void processStream() throws XmlPullParserException, IOException { } } + private void processResumed(final Element resumed) { + this.inSmacksSession = true; + this.isBound = true; + this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion)); + lastPacketReceived = SystemClock.elapsedRealtime(); + final String h = resumed.getAttribute("h"); + try { + ArrayList failedStanzas = new ArrayList<>(); + final boolean acknowledgedMessages; + synchronized (this.mStanzaQueue) { + final int serverCount = Integer.parseInt(h); + if (serverCount < stanzasSent) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": session resumed with lost packages"); + stanzasSent = serverCount; + } else { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": session resumed"); + } + acknowledgedMessages = acknowledgeStanzaUpTo(serverCount); + for (int i = 0; i < this.mStanzaQueue.size(); ++i) { + failedStanzas.add(mStanzaQueue.valueAt(i)); + } + mStanzaQueue.clear(); + } + if (acknowledgedMessages) { + mXmppConnectionService.updateConversationUi(); + } + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": resending " + + failedStanzas.size() + + " stanzas"); + for (AbstractAcknowledgeableStanza packet : failedStanzas) { + if (packet instanceof MessagePacket) { + MessagePacket message = (MessagePacket) packet; + mXmppConnectionService.markMessage( + account, + message.getTo().asBareJid(), + message.getId(), + Message.STATUS_UNSEND); + } + sendPacket(packet); + } + } catch (final NumberFormatException ignored) { + } + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": online with resource " + account.getResource()); + changeStatus(Account.State.ONLINE); + } + private boolean acknowledgeStanzaUpTo(int serverCount) { if (serverCount > stanzasSent) { Log.e(Config.LOGTAG, "server acknowledged more stanzas than we sent. serverCount=" + serverCount + ", ourCount=" + stanzasSent); @@ -986,6 +994,12 @@ private void processStreamFeatures(final Tag currentTag) throws IOException { + XmlHelper.printElementNames(this.streamFeatures)); throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } + } else { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": received NOP stream features" + + this.streamFeatures); } } @@ -1030,7 +1044,14 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio if (!Strings.isNullOrEmpty(firstMessage)) { authenticate.addChild("initial-response").setContent(firstMessage); } - // TODO place to add extensions + final Element inline = this.streamFeatures.findChild("inline", Namespace.SASL_2); + final boolean inlineStreamManagement = inline != null && inline.hasChild("sm", "urn:xmpp:sm:3"); + if (inlineStreamManagement && streamId != null) { + final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived, smVersion); + this.mSmCatchupMessageCounter.set(0); + this.mWaitingForSmCatchup.set(true); + authenticate.addChild(resume); + } } else { throw new AssertionError("Missing implementation for " + version); } From 7ea4f64ce4b157989266840411bf9aa703d57f0e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 29 Aug 2022 19:30:03 +0200 Subject: [PATCH 171/394] code clean up for resumed processing --- .../conversations/xmpp/XmppConnection.java | 80 ++++++++++--------- 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index bddcb197a..37d02884c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -694,52 +694,56 @@ private void processStream() throws XmlPullParserException, IOException { } } - private void processResumed(final Element resumed) { + private void processResumed(final Element resumed) throws StateChangingException { this.inSmacksSession = true; this.isBound = true; this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion)); lastPacketReceived = SystemClock.elapsedRealtime(); final String h = resumed.getAttribute("h"); + if (h == null) { + resetStreamId(); + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } + final int serverCount; try { - ArrayList failedStanzas = new ArrayList<>(); - final boolean acknowledgedMessages; - synchronized (this.mStanzaQueue) { - final int serverCount = Integer.parseInt(h); - if (serverCount < stanzasSent) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": session resumed with lost packages"); - stanzasSent = serverCount; - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": session resumed"); - } - acknowledgedMessages = acknowledgeStanzaUpTo(serverCount); - for (int i = 0; i < this.mStanzaQueue.size(); ++i) { - failedStanzas.add(mStanzaQueue.valueAt(i)); - } - mStanzaQueue.clear(); + serverCount = Integer.parseInt(h); + } catch (final NumberFormatException e) { + resetStreamId(); + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } + final ArrayList failedStanzas = new ArrayList<>(); + final boolean acknowledgedMessages; + synchronized (this.mStanzaQueue) { + if (serverCount < stanzasSent) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": session resumed with lost packages"); + stanzasSent = serverCount; + } else { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": session resumed"); } - if (acknowledgedMessages) { - mXmppConnectionService.updateConversationUi(); + acknowledgedMessages = acknowledgeStanzaUpTo(serverCount); + for (int i = 0; i < this.mStanzaQueue.size(); ++i) { + failedStanzas.add(mStanzaQueue.valueAt(i)); } - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": resending " - + failedStanzas.size() - + " stanzas"); - for (AbstractAcknowledgeableStanza packet : failedStanzas) { - if (packet instanceof MessagePacket) { - MessagePacket message = (MessagePacket) packet; - mXmppConnectionService.markMessage( - account, - message.getTo().asBareJid(), - message.getId(), - Message.STATUS_UNSEND); - } - sendPacket(packet); + mStanzaQueue.clear(); + } + if (acknowledgedMessages) { + mXmppConnectionService.updateConversationUi(); + } + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": resending " + failedStanzas.size() + " stanzas"); + for (final AbstractAcknowledgeableStanza packet : failedStanzas) { + if (packet instanceof MessagePacket) { + MessagePacket message = (MessagePacket) packet; + mXmppConnectionService.markMessage( + account, + message.getTo().asBareJid(), + message.getId(), + Message.STATUS_UNSEND); } - } catch (final NumberFormatException ignored) { + sendPacket(packet); } Log.d( Config.LOGTAG, @@ -998,7 +1002,7 @@ private void processStreamFeatures(final Tag currentTag) throws IOException { Log.d( Config.LOGTAG, account.getJid().asBareJid() - + ": received NOP stream features" + + ": received NOP stream features " + this.streamFeatures); } } From 8f76084a439f6d03a417013f9fc25afbb145275d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 29 Aug 2022 19:44:39 +0200 Subject: [PATCH 172/394] add sm-failed processing --- .../conversations/xmpp/XmppConnection.java | 55 ++++++++++++------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 37d02884c..cf18316ed 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -507,8 +507,11 @@ private void processStream() throws XmlPullParserException, IOException { + ": SASL 2.0 authorization identifier was " + authorizationIdentifier); final Element resumed = success.findChild("resumed", "urn:xmpp:sm:3"); + final Element failed = success.findChild("failed", "urn:xmpp:sm:3"); if (resumed != null && streamId != null) { processResumed(resumed); + } else if (failed != null) { + processFailed(failed, false); // wait for new stream features } } if (version == SaslMechanism.Version.SASL) { @@ -660,26 +663,8 @@ private void processStream() throws XmlPullParserException, IOException { + ": server send ack without sequence number"); } } else if (nextTag.isStart("failed")) { - Element failed = tagReader.readElement(nextTag); - try { - final int serverCount = Integer.parseInt(failed.getAttribute("h")); - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": resumption failed but server acknowledged stanza #" - + serverCount); - final boolean acknowledgedMessages; - synchronized (this.mStanzaQueue) { - acknowledgedMessages = acknowledgeStanzaUpTo(serverCount); - } - if (acknowledgedMessages) { - mXmppConnectionService.updateConversationUi(); - } - } catch (NumberFormatException | NullPointerException e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resumption failed"); - } - resetStreamId(); - sendBindRequest(); + final Element failed = tagReader.readElement(nextTag); + processFailed(failed, true); } else if (nextTag.isStart("iq")) { processIq(nextTag); } else if (nextTag.isStart("message")) { @@ -751,6 +736,36 @@ private void processResumed(final Element resumed) throws StateChangingException changeStatus(Account.State.ONLINE); } + private void processFailed(final Element failed, final boolean sendBindRequest) { + final int serverCount; + try { + serverCount = Integer.parseInt(failed.getAttribute("h")); + } catch (final NumberFormatException | NullPointerException e) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resumption failed"); + resetStreamId(); + if (sendBindRequest) { + sendBindRequest(); + } + return; + } + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": resumption failed but server acknowledged stanza #" + + serverCount); + final boolean acknowledgedMessages; + synchronized (this.mStanzaQueue) { + acknowledgedMessages = acknowledgeStanzaUpTo(serverCount); + } + if (acknowledgedMessages) { + mXmppConnectionService.updateConversationUi(); + } + resetStreamId(); + if (sendBindRequest) { + sendBindRequest(); + } + } + private boolean acknowledgeStanzaUpTo(int serverCount) { if (serverCount > stanzasSent) { Log.e(Config.LOGTAG, "server acknowledged more stanzas than we sent. serverCount=" + serverCount + ", ourCount=" + stanzasSent); From 3fac7d4992a4ebff8f3cabddcbf40920a331e62c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 30 Aug 2022 08:21:32 +0200 Subject: [PATCH 173/394] fix very rare NPE (race condition) --- .../ui/PublishProfilePictureActivity.java | 31 ++++++++++++------- .../conversations/xmpp/XmppConnection.java | 2 +- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java index 0e14fcc8f..16607b81e 100644 --- a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java @@ -102,18 +102,25 @@ public void onCreate(Bundle savedInstanceState) { xmppConnectionService.publishAvatar(account, avatarUri, this); } }); - this.cancelButton.setOnClickListener(v -> { - if (mInitialAccountSetup) { - final Intent intent = new Intent(getApplicationContext(), StartConversationActivity.class); - if (xmppConnectionService != null && xmppConnectionService.getAccounts().size() == 1) { - intent.putExtra("init", true); - } - StartConversationActivity.addInviteUri(intent, getIntent()); - intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString()); - startActivity(intent); - } - finish(); - }); + this.cancelButton.setOnClickListener( + v -> { + if (mInitialAccountSetup) { + final Intent intent = + new Intent( + getApplicationContext(), StartConversationActivity.class); + if (xmppConnectionService != null + && xmppConnectionService.getAccounts().size() == 1) { + intent.putExtra("init", true); + } + StartConversationActivity.addInviteUri(intent, getIntent()); + if (account != null) { + intent.putExtra( + EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString()); + } + startActivity(intent); + } + finish(); + }); this.avatar.setOnClickListener(v -> chooseAvatar(this)); this.defaultUri = PhoneHelper.getProfilePictureUri(getApplicationContext()); if (savedInstanceState != null) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index cf18316ed..c49cb989c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -1080,7 +1080,7 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio tagWriter.writeElement(authenticate); } - private List extractMechanisms(final Element stream) { + private static List extractMechanisms(final Element stream) { final ArrayList mechanisms = new ArrayList<>(stream .getChildren().size()); for (final Element child : stream.getChildren()) { From 4f92ba880bcd6fd94798b2370abd42e341a33181 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 30 Aug 2022 09:31:06 +0200 Subject: [PATCH 174/394] process authorization id in case full jid changes --- .../java/eu/siacs/conversations/xmpp/Jid.java | 3 +- .../conversations/xmpp/XmppConnection.java | 133 +++++++++++------- 2 files changed, 81 insertions(+), 55 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/Jid.java b/src/main/java/eu/siacs/conversations/xmpp/Jid.java index 622a132fb..299c872b3 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/Jid.java +++ b/src/main/java/eu/siacs/conversations/xmpp/Jid.java @@ -118,8 +118,7 @@ static Jid of(CharSequence jid) { static Jid ofEscaped(CharSequence jid) { try { return new WrappedJid(JidCreate.from(jid)); - } catch (XmppStringprepException e) { - e.printStackTrace(); + } catch (final XmppStringprepException e) { throw new IllegalArgumentException(e); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index c49cb989c..48a9f0281 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -470,61 +470,10 @@ private void processStream() throws XmlPullParserException, IOException { switchOverToTls(); } else if (nextTag.isStart("success")) { final Element success = tagReader.readElement(nextTag); - final SaslMechanism.Version version; - try { - version = SaslMechanism.Version.of(success); - } catch (final IllegalArgumentException e) { - throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); - } - final String challenge; - if (version == SaslMechanism.Version.SASL) { - challenge = success.getContent(); - } else if (version == SaslMechanism.Version.SASL_2) { - challenge = success.findChildContent("additional-data"); - } else { - throw new AssertionError("Missing implementation for " + version); - } - try { - saslMechanism.getResponse(challenge); - } catch (final SaslMechanism.AuthenticationException e) { - Log.e(Config.LOGTAG, String.valueOf(e)); - throw new StateChangingException(Account.State.UNAUTHORIZED); - } - Log.d( - Config.LOGTAG, - account.getJid().asBareJid().toString() - + ": logged in (using " - + version - + ")"); - account.setKey( - Account.PINNED_MECHANISM_KEY, String.valueOf(saslMechanism.getPriority())); - if (version == SaslMechanism.Version.SASL_2) { - final String authorizationIdentifier = - success.findChildContent("authorization-identifier"); - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": SASL 2.0 authorization identifier was " - + authorizationIdentifier); - final Element resumed = success.findChild("resumed", "urn:xmpp:sm:3"); - final Element failed = success.findChild("failed", "urn:xmpp:sm:3"); - if (resumed != null && streamId != null) { - processResumed(resumed); - } else if (failed != null) { - processFailed(failed, false); // wait for new stream features - } - } - if (version == SaslMechanism.Version.SASL) { - tagReader.reset(); - sendStartStream(); - final Tag tag = tagReader.readTag(); - if (tag != null && tag.isStart("stream")) { - processStream(); - } else { - throw new StateChangingException(Account.State.STREAM_OPENING_ERROR); - } + if (processSuccess(success)) { break; } + } else if (nextTag.isStart("failure", Namespace.TLS)) { throw new StateChangingException(Account.State.TLS_ERROR); } else if (nextTag.isStart("failure")) { @@ -679,6 +628,84 @@ private void processStream() throws XmlPullParserException, IOException { } } + private boolean processSuccess(final Element success) throws IOException, XmlPullParserException { + final SaslMechanism.Version version; + try { + version = SaslMechanism.Version.of(success); + } catch (final IllegalArgumentException e) { + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } + final String challenge; + if (version == SaslMechanism.Version.SASL) { + challenge = success.getContent(); + } else if (version == SaslMechanism.Version.SASL_2) { + challenge = success.findChildContent("additional-data"); + } else { + throw new AssertionError("Missing implementation for " + version); + } + try { + saslMechanism.getResponse(challenge); + } catch (final SaslMechanism.AuthenticationException e) { + Log.e(Config.LOGTAG, String.valueOf(e)); + throw new StateChangingException(Account.State.UNAUTHORIZED); + } + Log.d( + Config.LOGTAG, + account.getJid().asBareJid().toString() + + ": logged in (using " + + version + + ")"); + account.setKey( + Account.PINNED_MECHANISM_KEY, String.valueOf(saslMechanism.getPriority())); + if (version == SaslMechanism.Version.SASL_2) { + final String authorizationIdentifier = + success.findChildContent("authorization-identifier"); + final Jid authorizationJid; + try { + authorizationJid = Strings.isNullOrEmpty(authorizationIdentifier) ? null : Jid.ofEscaped(authorizationIdentifier); + } catch (final IllegalArgumentException e) { + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": SASL 2.0 authorization identifier was not a valid jid"); + throw new StateChangingException(Account.State.BIND_FAILURE); + } + if (authorizationJid == null) { + throw new StateChangingException(Account.State.BIND_FAILURE); + } + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": SASL 2.0 authorization identifier was " + + authorizationJid); + if (!account.getJid().getDomain().equals(authorizationJid.getDomain())) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server tried to re-assign domain to " + authorizationJid.getDomain()); + throw new StateChangingError(Account.State.BIND_FAILURE); + } + if (authorizationJid.isFullJid() && account.setJid(authorizationJid)) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": jid changed during SASL 2.0. updating database"); + mXmppConnectionService.databaseBackend.updateAccount(account); + } + final Element resumed = success.findChild("resumed", "urn:xmpp:sm:3"); + final Element failed = success.findChild("failed", "urn:xmpp:sm:3"); + if (resumed != null && streamId != null) { + processResumed(resumed); + } else if (failed != null) { + processFailed(failed, false); // wait for new stream features + } + } + if (version == SaslMechanism.Version.SASL) { + tagReader.reset(); + sendStartStream(); + final Tag tag = tagReader.readTag(); + if (tag != null && tag.isStart("stream")) { + processStream(); + return true; + } else { + throw new StateChangingException(Account.State.STREAM_OPENING_ERROR); + } + } else { + return false; + } + } + private void processResumed(final Element resumed) throws StateChangingException { this.inSmacksSession = true; this.isBound = true; From cb1d7c69a19aafb5ea9d0e02b8bc0949dfb592fb Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 3 Sep 2022 11:05:27 +0200 Subject: [PATCH 175/394] remove comment --- .../eu/siacs/conversations/services/XmppConnectionService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 517a63a6a..af68db19c 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -4502,7 +4502,6 @@ private void refreshAllFcmTokens() { for (Account account : getAccounts()) { if (account.isOnlineAndConnected() && mPushManagementService.available(account)) { mPushManagementService.registerPushTokenOnServer(account); - //TODO renew mucs } } } From 00dd9a8058d0cff4964ebc5919ccb670191f509c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 3 Sep 2022 12:16:06 +0200 Subject: [PATCH 176/394] remove support for sm:2 --- .../eu/siacs/conversations/xml/Namespace.java | 3 + .../conversations/xmpp/XmppConnection.java | 1119 +++++++++++------ .../xmpp/stanzas/csi/ActivePacket.java | 3 +- .../xmpp/stanzas/csi/InactivePacket.java | 3 +- .../xmpp/stanzas/streammgmt/AckPacket.java | 5 +- .../xmpp/stanzas/streammgmt/EnablePacket.java | 5 +- .../stanzas/streammgmt/RequestPacket.java | 5 +- .../xmpp/stanzas/streammgmt/ResumePacket.java | 5 +- 8 files changed, 741 insertions(+), 407 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index c2a7af607..e28e69add 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -25,6 +25,9 @@ public final class Namespace { public static final String NICK = "http://jabber.org/protocol/nick"; public static final String FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL = "http://jabber.org/protocol/offline"; public static final String BIND = "urn:ietf:params:xml:ns:xmpp-bind"; + public static final String BIND2 = "urn:xmpp:bind2:0"; + public static final String STREAM_MANAGEMENT = "urn:xmpp:sm:3"; + public static final String CSI = "urn:xmpp:csi:0"; public static final String BOOKMARKS_CONVERSION = "urn:xmpp:bookmarks-conversion:0"; public static final String BOOKMARKS = "storage:bookmarks"; public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 48a9f0281..6efbfbf15 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -109,38 +109,45 @@ public class XmppConnection implements Runnable { private static final int PACKET_IQ = 0; private static final int PACKET_MESSAGE = 1; private static final int PACKET_PRESENCE = 2; - public final OnIqPacketReceived registrationResponseListener = (account, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { - account.setOption(Account.OPTION_REGISTER, false); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": successfully registered new account on server"); - throw new StateChangingError(Account.State.REGISTRATION_SUCCESSFUL); - } else { - final List PASSWORD_TOO_WEAK_MSGS = Arrays.asList( - "The password is too weak", - "Please use a longer password."); - Element error = packet.findChild("error"); - Account.State state = Account.State.REGISTRATION_FAILED; - if (error != null) { - if (error.hasChild("conflict")) { - state = Account.State.REGISTRATION_CONFLICT; - } else if (error.hasChild("resource-constraint") - && "wait".equals(error.getAttribute("type"))) { - state = Account.State.REGISTRATION_PLEASE_WAIT; - } else if (error.hasChild("not-acceptable") - && PASSWORD_TOO_WEAK_MSGS.contains(error.findChildContent("text"))) { - state = Account.State.REGISTRATION_PASSWORD_TOO_WEAK; + public final OnIqPacketReceived registrationResponseListener = + (account, packet) -> { + if (packet.getType() == IqPacket.TYPE.RESULT) { + account.setOption(Account.OPTION_REGISTER, false); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": successfully registered new account on server"); + throw new StateChangingError(Account.State.REGISTRATION_SUCCESSFUL); + } else { + final List PASSWORD_TOO_WEAK_MSGS = + Arrays.asList( + "The password is too weak", "Please use a longer password."); + Element error = packet.findChild("error"); + Account.State state = Account.State.REGISTRATION_FAILED; + if (error != null) { + if (error.hasChild("conflict")) { + state = Account.State.REGISTRATION_CONFLICT; + } else if (error.hasChild("resource-constraint") + && "wait".equals(error.getAttribute("type"))) { + state = Account.State.REGISTRATION_PLEASE_WAIT; + } else if (error.hasChild("not-acceptable") + && PASSWORD_TOO_WEAK_MSGS.contains( + error.findChildContent("text"))) { + state = Account.State.REGISTRATION_PASSWORD_TOO_WEAK; + } + } + throw new StateChangingError(state); } - } - throw new StateChangingError(state); - } - }; + }; protected final Account account; private final Features features = new Features(this); private final HashMap disco = new HashMap<>(); private final HashMap commands = new HashMap<>(); private final SparseArray mStanzaQueue = new SparseArray<>(); - private final Hashtable> packetCallbacks = new Hashtable<>(); - private final Set advancedStreamFeaturesLoadedListeners = new HashSet<>(); + private final Hashtable> packetCallbacks = + new Hashtable<>(); + private final Set advancedStreamFeaturesLoadedListeners = + new HashSet<>(); private final XmppConnectionService mXmppConnectionService; private Socket socket; private XmlReader tagReader; @@ -150,7 +157,6 @@ public class XmppConnection implements Runnable { private boolean isBound = false; private Element streamFeatures; private String streamId = null; - private int smVersion = 3; private int stanzasReceived = 0; private int stanzasSent = 0; private long lastPacketReceived = 0; @@ -178,7 +184,6 @@ public class XmppConnection implements Runnable { private volatile Thread mThread; private CountDownLatch mStreamCountDownLatch; - public XmppConnection(final Account account, final XmppConnectionService service) { this.account = account; this.mXmppConnectionService = service; @@ -186,10 +191,12 @@ public XmppConnection(final Account account, final XmppConnectionService service private static void fixResource(Context context, Account account) { String resource = account.getResource(); - int fixedPartLength = context.getString(R.string.app_name).length() + 1; //include the trailing dot + int fixedPartLength = + context.getString(R.string.app_name).length() + 1; // include the trailing dot int randomPartLength = 4; // 3 bytes if (resource != null && resource.length() > fixedPartLength + randomPartLength) { - if (validBase64(resource.substring(fixedPartLength, fixedPartLength + randomPartLength))) { + if (validBase64( + resource.substring(fixedPartLength, fixedPartLength + randomPartLength))) { account.setResource(resource.substring(0, fixedPartLength + randomPartLength)); } } @@ -206,7 +213,12 @@ private static boolean validBase64(String input) { private void changeStatus(final Account.State nextStatus) { synchronized (this) { if (Thread.currentThread().isInterrupted()) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": not changing status to " + nextStatus + " because thread was interrupted"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": not changing status to " + + nextStatus + + " because thread was interrupted"); return; } if (account.getStatus() != nextStatus) { @@ -260,7 +272,9 @@ protected void connect() { inSmacksSession = false; isBound = false; this.attempt++; - this.verifiedHostname = null; //will be set if user entered hostname is being used or hostname was verified with dnssec + this.verifiedHostname = + null; // will be set if user entered hostname is being used or hostname was verified + // with dnssec try { Socket localSocket; shouldAuthenticate = !account.isOptionSet(Account.OPTION_REGISTER); @@ -279,7 +293,13 @@ protected void connect() { final int port = account.getPort(); final boolean directTls = Resolver.useDirectTls(port); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": connect to " + destination + " via Tor. directTls=" + directTls); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": connect to " + + destination + + " via Tor. directTls=" + + directTls); localSocket = SocksSocketFactory.createSocketOverTor(destination, port); if (directTls) { @@ -290,7 +310,10 @@ protected void connect() { try { startXmpp(localSocket); } catch (InterruptedException e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": thread was interrupted before beginning stream"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": thread was interrupted before beginning stream"); return; } catch (Exception e) { throw new IOException(e.getMessage()); @@ -309,41 +332,70 @@ protected void connect() { return; } if (results.size() == 0) { - Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": Resolver results were empty"); + Log.e( + Config.LOGTAG, + account.getJid().asBareJid() + ": Resolver results were empty"); return; } final Resolver.Result storedBackupResult; if (hardcoded) { storedBackupResult = null; } else { - storedBackupResult = mXmppConnectionService.databaseBackend.findResolverResult(domain); + storedBackupResult = + mXmppConnectionService.databaseBackend.findResolverResult(domain); if (storedBackupResult != null && !results.contains(storedBackupResult)) { results.add(storedBackupResult); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": loaded backup resolver result from db: " + storedBackupResult); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": loaded backup resolver result from db: " + + storedBackupResult); } } - for (Iterator iterator = results.iterator(); iterator.hasNext(); ) { + for (Iterator iterator = results.iterator(); + iterator.hasNext(); ) { final Resolver.Result result = iterator.next(); if (Thread.currentThread().isInterrupted()) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Thread was interrupted"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": Thread was interrupted"); return; } try { // if tls is true, encryption is implied and must not be started features.encryptionEnabled = result.isDirectTls(); - verifiedHostname = result.isAuthenticated() ? result.getHostname().toString() : null; + verifiedHostname = + result.isAuthenticated() ? result.getHostname().toString() : null; Log.d(Config.LOGTAG, "verified hostname " + verifiedHostname); final InetSocketAddress addr; if (result.getIp() != null) { addr = new InetSocketAddress(result.getIp(), result.getPort()); - Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() - + ": using values from resolver " + (result.getHostname() == null ? "" : result.getHostname().toString() - + "/") + result.getIp().getHostAddress() + ":" + result.getPort() + " tls: " + features.encryptionEnabled); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid().toString() + + ": using values from resolver " + + (result.getHostname() == null + ? "" + : result.getHostname().toString() + "/") + + result.getIp().getHostAddress() + + ":" + + result.getPort() + + " tls: " + + features.encryptionEnabled); } else { - addr = new InetSocketAddress(IDN.toASCII(result.getHostname().toString()), result.getPort()); - Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() - + ": using values from resolver " - + result.getHostname().toString() + ":" + result.getPort() + " tls: " + features.encryptionEnabled); + addr = + new InetSocketAddress( + IDN.toASCII(result.getHostname().toString()), + result.getPort()); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid().toString() + + ": using values from resolver " + + result.getHostname().toString() + + ":" + + result.getPort() + + " tls: " + + features.encryptionEnabled); } localSocket = new Socket(); @@ -355,9 +407,12 @@ protected void connect() { localSocket.setSoTimeout(Config.SOCKET_TIMEOUT * 1000); if (startXmpp(localSocket)) { - localSocket.setSoTimeout(0); //reset to 0; once the connection is established we don’t want this + localSocket.setSoTimeout( + 0); // reset to 0; once the connection is established we don’t + // want this if (!hardcoded && !result.equals(storedBackupResult)) { - mXmppConnectionService.databaseBackend.saveResolverResult(domain, result); + mXmppConnectionService.databaseBackend.saveResolverResult( + domain, result); } break; // successfully connected to server that speaks xmpp } else { @@ -369,10 +424,20 @@ protected void connect() { throw e; } } catch (InterruptedException e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": thread was interrupted before beginning stream"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": thread was interrupted before beginning stream"); return; } catch (final Throwable e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": " + e.getMessage() + "(" + e.getClass().getName() + ")"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid().toString() + + ": " + + e.getMessage() + + "(" + + e.getClass().getName() + + ")"); if (!iterator.hasNext()) { throw new UnknownHostException(); } @@ -384,7 +449,9 @@ protected void connect() { this.changeStatus(Account.State.MISSING_INTERNET_PERMISSION); } catch (final StateChangingException e) { this.changeStatus(e.state); - } catch (final UnknownHostException | ConnectException | SocksSocketFactory.HostNotFoundException e) { + } catch (final UnknownHostException + | ConnectException + | SocksSocketFactory.HostNotFoundException e) { this.changeStatus(Account.State.SERVER_NOT_FOUND); } catch (final SocksSocketFactory.SocksProxyNotFoundException e) { this.changeStatus(Account.State.TOR_NOT_AVAILABLE); @@ -396,7 +463,10 @@ protected void connect() { if (!Thread.currentThread().isInterrupted()) { forceCloseSocket(); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": not force closing socket because thread was interrupted"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": not force closing socket because thread was interrupted"); } } } @@ -430,17 +500,26 @@ private boolean startXmpp(Socket socket) throws Exception { return tag != null && tag.isStart("stream"); } - private SSLSocketFactory getSSLSocketFactory() throws NoSuchAlgorithmException, KeyManagementException { + private SSLSocketFactory getSSLSocketFactory() + throws NoSuchAlgorithmException, KeyManagementException { final SSLContext sc = SSLSocketHelper.getSSLContext(); - final MemorizingTrustManager trustManager = this.mXmppConnectionService.getMemorizingTrustManager(); + final MemorizingTrustManager trustManager = + this.mXmppConnectionService.getMemorizingTrustManager(); final KeyManager[] keyManager; if (account.getPrivateKeyAlias() != null) { - keyManager = new KeyManager[]{new MyKeyManager()}; + keyManager = new KeyManager[] {new MyKeyManager()}; } else { keyManager = null; } final String domain = account.getServer(); - sc.init(keyManager, new X509TrustManager[]{mInteractive ? trustManager.getInteractive(domain) : trustManager.getNonInteractive(domain)}, mXmppConnectionService.getRNG()); + sc.init( + keyManager, + new X509TrustManager[] { + mInteractive + ? trustManager.getInteractive(domain) + : trustManager.getNonInteractive(domain) + }, + mXmppConnectionService.getRNG()); return sc.getSocketFactory(); } @@ -449,7 +528,10 @@ public void run() { synchronized (this) { this.mThread = Thread.currentThread(); if (this.mThread.isInterrupted()) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": aborting connect because thread was interrupted"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": aborting connect because thread was interrupted"); return; } forceCloseSocket(); @@ -540,20 +622,16 @@ private void processStream() throws XmlPullParserException, IOException { Log.d( Config.LOGTAG, account.getJid().asBareJid().toString() - + ": stream management(" - + smVersion - + ") enabled (resumable)"); + + ": stream management enabled (resumable)"); } else { Log.d( Config.LOGTAG, account.getJid().asBareJid().toString() - + ": stream management(" - + smVersion - + ") enabled"); + + ": stream management enabled"); } this.stanzasReceived = 0; this.inSmacksSession = true; - final RequestPacket r = new RequestPacket(smVersion); + final RequestPacket r = new RequestPacket(); tagWriter.writeStanzaAsync(r); } else if (nextTag.isStart("resumed")) { final Element resumed = tagReader.readElement(nextTag); @@ -567,7 +645,7 @@ private void processStream() throws XmlPullParserException, IOException { + ": acknowledging stanza #" + this.stanzasReceived); } - final AckPacket ack = new AckPacket(this.stanzasReceived, smVersion); + final AckPacket ack = new AckPacket(this.stanzasReceived); tagWriter.writeStanzaAsync(ack); } else if (nextTag.isStart("a")) { boolean accountUiNeedsRefresh = false; @@ -628,7 +706,8 @@ private void processStream() throws XmlPullParserException, IOException { } } - private boolean processSuccess(final Element success) throws IOException, XmlPullParserException { + private boolean processSuccess(final Element success) + throws IOException, XmlPullParserException { final SaslMechanism.Version version; try { version = SaslMechanism.Version.of(success); @@ -651,20 +730,22 @@ private boolean processSuccess(final Element success) throws IOException, XmlPul } Log.d( Config.LOGTAG, - account.getJid().asBareJid().toString() - + ": logged in (using " - + version - + ")"); - account.setKey( - Account.PINNED_MECHANISM_KEY, String.valueOf(saslMechanism.getPriority())); + account.getJid().asBareJid().toString() + ": logged in (using " + version + ")"); + account.setKey(Account.PINNED_MECHANISM_KEY, String.valueOf(saslMechanism.getPriority())); if (version == SaslMechanism.Version.SASL_2) { final String authorizationIdentifier = success.findChildContent("authorization-identifier"); final Jid authorizationJid; try { - authorizationJid = Strings.isNullOrEmpty(authorizationIdentifier) ? null : Jid.ofEscaped(authorizationIdentifier); + authorizationJid = + Strings.isNullOrEmpty(authorizationIdentifier) + ? null + : Jid.ofEscaped(authorizationIdentifier); } catch (final IllegalArgumentException e) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": SASL 2.0 authorization identifier was not a valid jid"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": SASL 2.0 authorization identifier was not a valid jid"); throw new StateChangingException(Account.State.BIND_FAILURE); } if (authorizationJid == null) { @@ -676,11 +757,18 @@ private boolean processSuccess(final Element success) throws IOException, XmlPul + ": SASL 2.0 authorization identifier was " + authorizationJid); if (!account.getJid().getDomain().equals(authorizationJid.getDomain())) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server tried to re-assign domain to " + authorizationJid.getDomain()); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": server tried to re-assign domain to " + + authorizationJid.getDomain()); throw new StateChangingError(Account.State.BIND_FAILURE); } if (authorizationJid.isFullJid() && account.setJid(authorizationJid)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": jid changed during SASL 2.0. updating database"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": jid changed during SASL 2.0. updating database"); mXmppConnectionService.databaseBackend.updateAccount(account); } final Element resumed = success.findChild("resumed", "urn:xmpp:sm:3"); @@ -709,7 +797,7 @@ private boolean processSuccess(final Element success) throws IOException, XmlPul private void processResumed(final Element resumed) throws StateChangingException { this.inSmacksSession = true; this.isBound = true; - this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion)); + this.tagWriter.writeStanzaAsync(new RequestPacket()); lastPacketReceived = SystemClock.elapsedRealtime(); final String h = resumed.getAttribute("h"); if (h == null) { @@ -795,13 +883,22 @@ private void processFailed(final Element failed, final boolean sendBindRequest) private boolean acknowledgeStanzaUpTo(int serverCount) { if (serverCount > stanzasSent) { - Log.e(Config.LOGTAG, "server acknowledged more stanzas than we sent. serverCount=" + serverCount + ", ourCount=" + stanzasSent); + Log.e( + Config.LOGTAG, + "server acknowledged more stanzas than we sent. serverCount=" + + serverCount + + ", ourCount=" + + stanzasSent); } boolean acknowledgedMessages = false; for (int i = 0; i < mStanzaQueue.size(); ++i) { if (serverCount >= mStanzaQueue.keyAt(i)) { if (Config.EXTENDED_SM_LOGGING) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server acknowledged stanza #" + mStanzaQueue.keyAt(i)); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": server acknowledged stanza #" + + mStanzaQueue.keyAt(i)); } final AbstractAcknowledgeableStanza stanza = mStanzaQueue.valueAt(i); if (stanza instanceof MessagePacket && acknowledgedListener != null) { @@ -809,7 +906,8 @@ private boolean acknowledgeStanzaUpTo(int serverCount) { final String id = packet.getId(); final Jid to = packet.getTo(); if (id != null && to != null) { - acknowledgedMessages |= acknowledgedListener.onMessageAcknowledged(account, to, id); + acknowledgedMessages |= + acknowledgedListener.onMessageAcknowledged(account, to, id); } } mStanzaQueue.removeAt(i); @@ -819,8 +917,8 @@ private boolean acknowledgeStanzaUpTo(int serverCount) { return acknowledgedMessages; } - private @NonNull - Element processPacket(final Tag currentTag, final int packetType) throws IOException { + private @NonNull Element processPacket(final Tag currentTag, final int packetType) + throws IOException { final Element element; switch (packetType) { case PACKET_IQ: @@ -856,7 +954,12 @@ Element processPacket(final Tag currentTag, final int packetType) throws IOExcep if (inSmacksSession) { ++stanzasReceived; } else if (features.sm()) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": not counting stanza(" + element.getClass().getSimpleName() + "). Not in smacks session."); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": not counting stanza(" + + element.getClass().getSimpleName() + + "). Not in smacks session."); } lastPacketReceived = SystemClock.elapsedRealtime(); if (Config.BACKGROUND_STANZA_LOGGING && mXmppConnectionService.checkListeners()) { @@ -874,7 +977,13 @@ Element processPacket(final Tag currentTag, final int packetType) throws IOExcep private void processIq(final Tag currentTag) throws IOException { final IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ); if (!packet.valid()) { - Log.e(Config.LOGTAG, "encountered invalid iq from='" + packet.getFrom() + "' to='" + packet.getTo() + "'"); + Log.e( + Config.LOGTAG, + "encountered invalid iq from='" + + packet.getFrom() + + "' to='" + + packet.getTo() + + "'"); return; } if (packet instanceof JinglePacket) { @@ -884,7 +993,8 @@ private void processIq(final Tag currentTag) throws IOException { } else { OnIqPacketReceived callback = null; synchronized (this.packetCallbacks) { - final Pair packetCallbackDuple = packetCallbacks.get(packet.getId()); + final Pair packetCallbackDuple = + packetCallbacks.get(packet.getId()); if (packetCallbackDuple != null) { // Packets to the server should have responses from the server if (packetCallbackDuple.first.toServer(account)) { @@ -892,17 +1002,25 @@ private void processIq(final Tag currentTag) throws IOException { callback = packetCallbackDuple.second; packetCallbacks.remove(packet.getId()); } else { - Log.e(Config.LOGTAG, account.getJid().asBareJid().toString() + ": ignoring spoofed iq packet"); + Log.e( + Config.LOGTAG, + account.getJid().asBareJid().toString() + + ": ignoring spoofed iq packet"); } } else { - if (packet.getFrom() != null && packet.getFrom().equals(packetCallbackDuple.first.getTo())) { + if (packet.getFrom() != null + && packet.getFrom().equals(packetCallbackDuple.first.getTo())) { callback = packetCallbackDuple.second; packetCallbacks.remove(packet.getId()); } else { - Log.e(Config.LOGTAG, account.getJid().asBareJid().toString() + ": ignoring spoofed iq packet"); + Log.e( + Config.LOGTAG, + account.getJid().asBareJid().toString() + + ": ignoring spoofed iq packet"); } } - } else if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) { + } else if (packet.getType() == IqPacket.TYPE.GET + || packet.getType() == IqPacket.TYPE.SET) { callback = this.unregisteredIqListener; } } @@ -919,7 +1037,13 @@ private void processIq(final Tag currentTag) throws IOException { private void processMessage(final Tag currentTag) throws IOException { final MessagePacket packet = (MessagePacket) processPacket(currentTag, PACKET_MESSAGE); if (!packet.valid()) { - Log.e(Config.LOGTAG, "encountered invalid message from='" + packet.getFrom() + "' to='" + packet.getTo() + "'"); + Log.e( + Config.LOGTAG, + "encountered invalid message from='" + + packet.getFrom() + + "' to='" + + packet.getTo() + + "'"); return; } this.messageListener.onMessagePacketReceived(account, packet); @@ -928,7 +1052,13 @@ private void processMessage(final Tag currentTag) throws IOException { private void processPresence(final Tag currentTag) throws IOException { PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE); if (!packet.valid()) { - Log.e(Config.LOGTAG, "encountered invalid presence from='" + packet.getFrom() + "' to='" + packet.getTo() + "'"); + Log.e( + Config.LOGTAG, + "encountered invalid presence from='" + + packet.getFrom() + + "' to='" + + packet.getTo() + + "'"); return; } this.presenceListener.onPresencePacketReceived(account, packet); @@ -967,14 +1097,21 @@ private SSLSocket upgradeSocketToTls(final Socket socket) throws IOException { throw new StateChangingException(Account.State.TLS_ERROR); } final InetAddress address = socket.getInetAddress(); - final SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(socket, address.getHostAddress(), socket.getPort(), true); + final SSLSocket sslSocket = + (SSLSocket) + sslSocketFactory.createSocket( + socket, address.getHostAddress(), socket.getPort(), true); SSLSocketHelper.setSecurity(sslSocket); SSLSocketHelper.setHostname(sslSocket, IDN.toASCII(account.getServer())); SSLSocketHelper.setApplicationProtocol(sslSocket, "xmpp-client"); final XmppDomainVerifier xmppDomainVerifier = new XmppDomainVerifier(); try { - if (!xmppDomainVerifier.verify(account.getServer(), this.verifiedHostname, sslSocket.getSession())) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS certificate domain verification failed"); + if (!xmppDomainVerifier.verify( + account.getServer(), this.verifiedHostname, sslSocket.getSession())) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": TLS certificate domain verification failed"); FileBackend.close(sslSocket); throw new StateChangingException(Account.State.TLS_ERROR_DOMAIN); } @@ -1016,7 +1153,7 @@ private void processStreamFeatures(final Tag currentTag) throws IOException { && shouldAuthenticate && isSecure) { authenticate(SaslMechanism.Version.SASL); - } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:" + smVersion) + } else if (this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT) && streamId != null) { if (Config.EXTENDED_SM_LOGGING) { Log.d( @@ -1025,7 +1162,7 @@ private void processStreamFeatures(final Tag currentTag) throws IOException { + ": resuming after stanza #" + stanzasReceived); } - final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived, smVersion); + final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived); this.mSmCatchupMessageCounter.set(0); this.mWaitingForSmCatchup.set(true); this.tagWriter.writeStanzaAsync(resume); @@ -1059,7 +1196,8 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio saslMechanism = new ScramSha256(tagWriter, account, mXmppConnectionService.getRNG()); } else if (mechanisms.contains(ScramSha1.MECHANISM)) { saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG()); - } else if (mechanisms.contains(Plain.MECHANISM) && !account.getJid().getDomain().toEscapedString().equals("nimbuzz.com")) { + } else if (mechanisms.contains(Plain.MECHANISM) + && !account.getJid().getDomain().toEscapedString().equals("nimbuzz.com")) { saslMechanism = new Plain(tagWriter, account); } else if (mechanisms.contains(DigestMd5.MECHANISM)) { saslMechanism = new DigestMd5(tagWriter, account, mXmppConnectionService.getRNG()); @@ -1067,15 +1205,24 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio saslMechanism = new Anonymous(tagWriter, account, mXmppConnectionService.getRNG()); } if (saslMechanism == null) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to find supported SASL mechanism in " + mechanisms); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": unable to find supported SASL mechanism in " + + mechanisms); throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } final int pinnedMechanism = account.getKeyAsInt(Account.PINNED_MECHANISM_KEY, -1); if (pinnedMechanism > saslMechanism.getPriority()) { - Log.e(Config.LOGTAG, "Auth failed. Authentication mechanism " + saslMechanism.getMechanism() + - " has lower priority (" + saslMechanism.getPriority() + - ") than pinned priority (" + pinnedMechanism + - "). Possible downgrade attack?"); + Log.e( + Config.LOGTAG, + "Auth failed. Authentication mechanism " + + saslMechanism.getMechanism() + + " has lower priority (" + + saslMechanism.getPriority() + + ") than pinned priority (" + + pinnedMechanism + + "). Possible downgrade attack?"); throw new StateChangingException(Account.State.DOWNGRADE_ATTACK); } final String firstMessage = saslMechanism.getClientFirstMessage(); @@ -1091,9 +1238,11 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio authenticate.addChild("initial-response").setContent(firstMessage); } final Element inline = this.streamFeatures.findChild("inline", Namespace.SASL_2); - final boolean inlineStreamManagement = inline != null && inline.hasChild("sm", "urn:xmpp:sm:3"); + final boolean inlineStreamManagement = + inline != null && inline.hasChild("sm", "urn:xmpp:sm:3"); + final boolean inlineBind2 = inline != null && inline.hasChild("bind", Namespace.BIND2); if (inlineStreamManagement && streamId != null) { - final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived, smVersion); + final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived); this.mSmCatchupMessageCounter.set(0); this.mWaitingForSmCatchup.set(true); authenticate.addChild(resume); @@ -1102,35 +1251,46 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio throw new AssertionError("Missing implementation for " + version); } - Log.d(Config.LOGTAG, account.getJid().toString() + ": Authenticating with "+version+ "/" + saslMechanism.getMechanism()); + Log.d( + Config.LOGTAG, + account.getJid().toString() + + ": Authenticating with " + + version + + "/" + + saslMechanism.getMechanism()); authenticate.setAttribute("mechanism", saslMechanism.getMechanism()); tagWriter.writeElement(authenticate); } private static List extractMechanisms(final Element stream) { - final ArrayList mechanisms = new ArrayList<>(stream - .getChildren().size()); + final ArrayList mechanisms = new ArrayList<>(stream.getChildren().size()); for (final Element child : stream.getChildren()) { mechanisms.add(child.getContent()); } return mechanisms; } - private void register() { final String preAuth = account.getKey(Account.PRE_AUTH_REGISTRATION_TOKEN); if (preAuth != null && features.invite()) { final IqPacket preAuthRequest = new IqPacket(IqPacket.TYPE.SET); preAuthRequest.addChild("preauth", Namespace.PARS).setAttribute("token", preAuth); - sendUnmodifiedIqPacket(preAuthRequest, (account, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { - sendRegistryRequest(); - } else { - final String error = response.getErrorCondition(); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": failed to pre auth. " + error); - throw new StateChangingError(Account.State.REGISTRATION_INVALID_TOKEN); - } - }, true); + sendUnmodifiedIqPacket( + preAuthRequest, + (account, response) -> { + if (response.getType() == IqPacket.TYPE.RESULT) { + sendRegistryRequest(); + } else { + final String error = response.getErrorCondition(); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": failed to pre auth. " + + error); + throw new StateChangingError(Account.State.REGISTRATION_INVALID_TOKEN); + } + }, + true); } else { sendRegistryRequest(); } @@ -1140,79 +1300,91 @@ private void sendRegistryRequest() { final IqPacket register = new IqPacket(IqPacket.TYPE.GET); register.query(Namespace.REGISTER); register.setTo(account.getDomain()); - sendUnmodifiedIqPacket(register, (account, packet) -> { - if (packet.getType() == IqPacket.TYPE.TIMEOUT) { - return; - } - if (packet.getType() == IqPacket.TYPE.ERROR) { - throw new StateChangingError(Account.State.REGISTRATION_FAILED); - } - final Element query = packet.query(Namespace.REGISTER); - if (query.hasChild("username") && (query.hasChild("password"))) { - final IqPacket register1 = new IqPacket(IqPacket.TYPE.SET); - final Element username = new Element("username").setContent(account.getUsername()); - final Element password = new Element("password").setContent(account.getPassword()); - register1.query(Namespace.REGISTER).addChild(username); - register1.query().addChild(password); - register1.setFrom(account.getJid().asBareJid()); - sendUnmodifiedIqPacket(register1, registrationResponseListener, true); - } else if (query.hasChild("x", Namespace.DATA)) { - final Data data = Data.parse(query.findChild("x", Namespace.DATA)); - final Element blob = query.findChild("data", "urn:xmpp:bob"); - final String id = packet.getId(); - InputStream is; - if (blob != null) { - try { - final String base64Blob = blob.getContent(); - final byte[] strBlob = Base64.decode(base64Blob, Base64.DEFAULT); - is = new ByteArrayInputStream(strBlob); - } catch (Exception e) { - is = null; + sendUnmodifiedIqPacket( + register, + (account, packet) -> { + if (packet.getType() == IqPacket.TYPE.TIMEOUT) { + return; } - } else { - final boolean useTor = mXmppConnectionService.useTorToConnect() || account.isOnion(); - try { - final String url = data.getValue("url"); - final String fallbackUrl = data.getValue("captcha-fallback-url"); - if (url != null) { - is = HttpConnectionManager.open(url, useTor); - } else if (fallbackUrl != null) { - is = HttpConnectionManager.open(fallbackUrl, useTor); + if (packet.getType() == IqPacket.TYPE.ERROR) { + throw new StateChangingError(Account.State.REGISTRATION_FAILED); + } + final Element query = packet.query(Namespace.REGISTER); + if (query.hasChild("username") && (query.hasChild("password"))) { + final IqPacket register1 = new IqPacket(IqPacket.TYPE.SET); + final Element username = + new Element("username").setContent(account.getUsername()); + final Element password = + new Element("password").setContent(account.getPassword()); + register1.query(Namespace.REGISTER).addChild(username); + register1.query().addChild(password); + register1.setFrom(account.getJid().asBareJid()); + sendUnmodifiedIqPacket(register1, registrationResponseListener, true); + } else if (query.hasChild("x", Namespace.DATA)) { + final Data data = Data.parse(query.findChild("x", Namespace.DATA)); + final Element blob = query.findChild("data", "urn:xmpp:bob"); + final String id = packet.getId(); + InputStream is; + if (blob != null) { + try { + final String base64Blob = blob.getContent(); + final byte[] strBlob = Base64.decode(base64Blob, Base64.DEFAULT); + is = new ByteArrayInputStream(strBlob); + } catch (Exception e) { + is = null; + } } else { - is = null; + final boolean useTor = + mXmppConnectionService.useTorToConnect() || account.isOnion(); + try { + final String url = data.getValue("url"); + final String fallbackUrl = data.getValue("captcha-fallback-url"); + if (url != null) { + is = HttpConnectionManager.open(url, useTor); + } else if (fallbackUrl != null) { + is = HttpConnectionManager.open(fallbackUrl, useTor); + } else { + is = null; + } + } catch (final IOException e) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": unable to fetch captcha", + e); + is = null; + } } - } catch (final IOException e) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": unable to fetch captcha", e); - is = null; - } - } - if (is != null) { - Bitmap captcha = BitmapFactory.decodeStream(is); - try { - if (mXmppConnectionService.displayCaptchaRequest(account, id, data, captcha)) { - return; + if (is != null) { + Bitmap captcha = BitmapFactory.decodeStream(is); + try { + if (mXmppConnectionService.displayCaptchaRequest( + account, id, data, captcha)) { + return; + } + } catch (Exception e) { + throw new StateChangingError(Account.State.REGISTRATION_FAILED); + } + } + throw new StateChangingError(Account.State.REGISTRATION_FAILED); + } else if (query.hasChild("instructions") + || query.hasChild("x", Namespace.OOB)) { + final String instructions = query.findChildContent("instructions"); + final Element oob = query.findChild("x", Namespace.OOB); + final String url = oob == null ? null : oob.findChildContent("url"); + if (url != null) { + setAccountCreationFailed(url); + } else if (instructions != null) { + final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(instructions); + if (matcher.find()) { + setAccountCreationFailed( + instructions.substring(matcher.start(), matcher.end())); + } } - } catch (Exception e) { throw new StateChangingError(Account.State.REGISTRATION_FAILED); } - } - throw new StateChangingError(Account.State.REGISTRATION_FAILED); - } else if (query.hasChild("instructions") || query.hasChild("x", Namespace.OOB)) { - final String instructions = query.findChildContent("instructions"); - final Element oob = query.findChild("x", Namespace.OOB); - final String url = oob == null ? null : oob.findChildContent("url"); - if (url != null) { - setAccountCreationFailed(url); - } else if (instructions != null) { - final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(instructions); - if (matcher.find()) { - setAccountCreationFailed(instructions.substring(matcher.start(), matcher.end())); - } - } - throw new StateChangingError(Account.State.REGISTRATION_FAILED); - } - }, true); + }, + true); } private void setAccountCreationFailed(final String url) { @@ -1247,7 +1419,10 @@ private void sendBindRequest() { try { mXmppConnectionService.restoredFromDatabaseLatch.await(); } catch (InterruptedException e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": interrupted while waiting for DB restore during bind"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": interrupted while waiting for DB restore during bind"); return; } clearIqCallbacks(); @@ -1257,49 +1432,76 @@ private void sendBindRequest() { fixResource(mXmppConnectionService, account); } final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); - final String resource = Config.USE_RANDOM_RESOURCE_ON_EVERY_BIND ? nextRandomId() : account.getResource(); + final String resource = + Config.USE_RANDOM_RESOURCE_ON_EVERY_BIND ? nextRandomId() : account.getResource(); iq.addChild("bind", Namespace.BIND).addChild("resource").setContent(resource); - this.sendUnmodifiedIqPacket(iq, (account, packet) -> { - if (packet.getType() == IqPacket.TYPE.TIMEOUT) { - return; - } - final Element bind = packet.findChild("bind"); - if (bind != null && packet.getType() == IqPacket.TYPE.RESULT) { - isBound = true; - final Element jid = bind.findChild("jid"); - if (jid != null && jid.getContent() != null) { - try { - Jid assignedJid = Jid.ofEscaped(jid.getContent()); - if (!account.getJid().getDomain().equals(assignedJid.getDomain())) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server tried to re-assign domain to " + assignedJid.getDomain()); - throw new StateChangingError(Account.State.BIND_FAILURE); - } - if (account.setJid(assignedJid)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": jid changed during bind. updating database"); - mXmppConnectionService.databaseBackend.updateAccount(account); - } - if (streamFeatures.hasChild("session") - && !streamFeatures.findChild("session").hasChild("optional")) { - sendStartSession(); + this.sendUnmodifiedIqPacket( + iq, + (account, packet) -> { + if (packet.getType() == IqPacket.TYPE.TIMEOUT) { + return; + } + final Element bind = packet.findChild("bind"); + if (bind != null && packet.getType() == IqPacket.TYPE.RESULT) { + isBound = true; + final Element jid = bind.findChild("jid"); + if (jid != null && jid.getContent() != null) { + try { + Jid assignedJid = Jid.ofEscaped(jid.getContent()); + if (!account.getJid().getDomain().equals(assignedJid.getDomain())) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": server tried to re-assign domain to " + + assignedJid.getDomain()); + throw new StateChangingError(Account.State.BIND_FAILURE); + } + if (account.setJid(assignedJid)) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": jid changed during bind. updating database"); + mXmppConnectionService.databaseBackend.updateAccount(account); + } + if (streamFeatures.hasChild("session") + && !streamFeatures + .findChild("session") + .hasChild("optional")) { + sendStartSession(); + } else { + sendPostBindInitialization(); + } + return; + } catch (final IllegalArgumentException e) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": server reported invalid jid (" + + jid.getContent() + + ") on bind"); + } } else { - sendPostBindInitialization(); + Log.d( + Config.LOGTAG, + account.getJid() + + ": disconnecting because of bind failure. (no jid)"); } - return; - } catch (final IllegalArgumentException e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server reported invalid jid (" + jid.getContent() + ") on bind"); + } else { + Log.d( + Config.LOGTAG, + account.getJid() + + ": disconnecting because of bind failure (" + + packet); } - } else { - Log.d(Config.LOGTAG, account.getJid() + ": disconnecting because of bind failure. (no jid)"); - } - } else { - Log.d(Config.LOGTAG, account.getJid() + ": disconnecting because of bind failure (" + packet); - } - final Element error = packet.findChild("error"); - if (packet.getType() == IqPacket.TYPE.ERROR && error != null && error.hasChild("conflict")) { - account.setResource(createNewResource()); - } - throw new StateChangingError(Account.State.BIND_FAILURE); - }, true); + final Element error = packet.findChild("error"); + if (packet.getType() == IqPacket.TYPE.ERROR + && error != null + && error.hasChild("conflict")) { + account.setResource(createNewResource()); + } + throw new StateChangingError(Account.State.BIND_FAILURE); + }, + true); } private void clearIqCallbacks() { @@ -1309,8 +1511,14 @@ private void clearIqCallbacks() { if (this.packetCallbacks.size() == 0) { return; } - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": clearing " + this.packetCallbacks.size() + " iq callbacks"); - final Iterator> iterator = this.packetCallbacks.values().iterator(); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": clearing " + + this.packetCallbacks.size() + + " iq callbacks"); + final Iterator> iterator = + this.packetCallbacks.values().iterator(); while (iterator.hasNext()) { Pair entry = iterator.next(); callbacks.add(entry.second); @@ -1321,43 +1529,56 @@ private void clearIqCallbacks() { try { callback.onIqPacketReceived(account, failurePacket); } catch (StateChangingError error) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": caught StateChangingError(" + error.state.toString() + ") while clearing callbacks"); - //ignore + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": caught StateChangingError(" + + error.state.toString() + + ") while clearing callbacks"); + // ignore } } - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": done clearing iq callbacks. " + this.packetCallbacks.size() + " left"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": done clearing iq callbacks. " + + this.packetCallbacks.size() + + " left"); } public void sendDiscoTimeout() { if (mWaitForDisco.compareAndSet(true, false)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": finalizing bind after disco timeout"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": finalizing bind after disco timeout"); finalizeBind(); } } private void sendStartSession() { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending legacy session to outdated server"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": sending legacy session to outdated server"); final IqPacket startSession = new IqPacket(IqPacket.TYPE.SET); startSession.addChild("session", "urn:ietf:params:xml:ns:xmpp-session"); - this.sendUnmodifiedIqPacket(startSession, (account, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { - sendPostBindInitialization(); - } else if (packet.getType() != IqPacket.TYPE.TIMEOUT) { - throw new StateChangingError(Account.State.SESSION_FAILURE); - } - }, true); + this.sendUnmodifiedIqPacket( + startSession, + (account, packet) -> { + if (packet.getType() == IqPacket.TYPE.RESULT) { + sendPostBindInitialization(); + } else if (packet.getType() != IqPacket.TYPE.TIMEOUT) { + throw new StateChangingError(Account.State.SESSION_FAILURE); + } + }, + true); } private void sendPostBindInitialization() { - smVersion = 0; - if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) { - smVersion = 3; - } else if (streamFeatures.hasChild("sm", "urn:xmpp:sm:2")) { - smVersion = 2; - } - if (smVersion != 0) { + final boolean streamManagement = + this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT); + if (streamManagement) { synchronized (this.mStanzaQueue) { - final EnablePacket enable = new EnablePacket(smVersion); + final EnablePacket enable = new EnablePacket(); tagWriter.writeStanzaAsync(enable); stanzasSent = 0; mStanzaQueue.clear(); @@ -1370,22 +1591,29 @@ private void sendPostBindInitialization() { } Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": starting service discovery"); mPendingServiceDiscoveries.set(0); - if (smVersion == 0 || Patches.DISCO_EXCEPTIONS.contains(account.getJid().getDomain().toEscapedString())) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": do not wait for service discovery"); + if (!streamManagement + || Patches.DISCO_EXCEPTIONS.contains( + account.getJid().getDomain().toEscapedString())) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": do not wait for service discovery"); mWaitForDisco.set(false); } else { mWaitForDisco.set(true); } lastDiscoStarted = SystemClock.elapsedRealtime(); - mXmppConnectionService.scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode()); + mXmppConnectionService.scheduleWakeUpCall( + Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode()); Element caps = streamFeatures.findChild("c"); final String hash = caps == null ? null : caps.getAttribute("hash"); final String ver = caps == null ? null : caps.getAttribute("ver"); ServiceDiscoveryResult discoveryResult = null; if (hash != null && ver != null) { - discoveryResult = mXmppConnectionService.getCachedServiceDiscoveryResult(new Pair<>(hash, ver)); + discoveryResult = + mXmppConnectionService.getCachedServiceDiscoveryResult(new Pair<>(hash, ver)); } - final boolean requestDiscoItemsFirst = !account.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY); + final boolean requestDiscoItemsFirst = + !account.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY); if (requestDiscoItemsFirst) { sendServiceDiscoveryItems(account.getDomain()); } @@ -1412,84 +1640,109 @@ private void sendServiceDiscoveryInfo(final Jid jid) { final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); iq.setTo(jid); iq.query("http://jabber.org/protocol/disco#info"); - this.sendIqPacket(iq, (account, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { - boolean advancedStreamFeaturesLoaded; - synchronized (XmppConnection.this.disco) { - ServiceDiscoveryResult result = new ServiceDiscoveryResult(packet); - if (jid.equals(account.getDomain())) { - mXmppConnectionService.databaseBackend.insertDiscoveryResult(result); + this.sendIqPacket( + iq, + (account, packet) -> { + if (packet.getType() == IqPacket.TYPE.RESULT) { + boolean advancedStreamFeaturesLoaded; + synchronized (XmppConnection.this.disco) { + ServiceDiscoveryResult result = new ServiceDiscoveryResult(packet); + if (jid.equals(account.getDomain())) { + mXmppConnectionService.databaseBackend.insertDiscoveryResult( + result); + } + disco.put(jid, result); + advancedStreamFeaturesLoaded = + disco.containsKey(account.getDomain()) + && disco.containsKey(account.getJid().asBareJid()); + } + if (advancedStreamFeaturesLoaded + && (jid.equals(account.getDomain()) + || jid.equals(account.getJid().asBareJid()))) { + enableAdvancedStreamFeatures(); + } + } else if (packet.getType() == IqPacket.TYPE.ERROR) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": could not query disco info for " + + jid.toString()); + final boolean serverOrAccount = + jid.equals(account.getDomain()) + || jid.equals(account.getJid().asBareJid()); + final boolean advancedStreamFeaturesLoaded; + if (serverOrAccount) { + synchronized (XmppConnection.this.disco) { + disco.put(jid, ServiceDiscoveryResult.empty()); + advancedStreamFeaturesLoaded = + disco.containsKey(account.getDomain()) + && disco.containsKey(account.getJid().asBareJid()); + } + } else { + advancedStreamFeaturesLoaded = false; + } + if (advancedStreamFeaturesLoaded) { + enableAdvancedStreamFeatures(); + } } - disco.put(jid, result); - advancedStreamFeaturesLoaded = disco.containsKey(account.getDomain()) - && disco.containsKey(account.getJid().asBareJid()); - } - if (advancedStreamFeaturesLoaded && (jid.equals(account.getDomain()) || jid.equals(account.getJid().asBareJid()))) { - enableAdvancedStreamFeatures(); - } - } else if (packet.getType() == IqPacket.TYPE.ERROR) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not query disco info for " + jid.toString()); - final boolean serverOrAccount = jid.equals(account.getDomain()) || jid.equals(account.getJid().asBareJid()); - final boolean advancedStreamFeaturesLoaded; - if (serverOrAccount) { - synchronized (XmppConnection.this.disco) { - disco.put(jid, ServiceDiscoveryResult.empty()); - advancedStreamFeaturesLoaded = disco.containsKey(account.getDomain()) && disco.containsKey(account.getJid().asBareJid()); + if (packet.getType() != IqPacket.TYPE.TIMEOUT) { + if (mPendingServiceDiscoveries.decrementAndGet() == 0 + && mWaitForDisco.compareAndSet(true, false)) { + finalizeBind(); + } } - } else { - advancedStreamFeaturesLoaded = false; - } - if (advancedStreamFeaturesLoaded) { - enableAdvancedStreamFeatures(); - } - } - if (packet.getType() != IqPacket.TYPE.TIMEOUT) { - if (mPendingServiceDiscoveries.decrementAndGet() == 0 - && mWaitForDisco.compareAndSet(true, false)) { - finalizeBind(); - } - } - }); + }); } private void discoverMamPreferences() { IqPacket request = new IqPacket(IqPacket.TYPE.GET); request.addChild("prefs", MessageArchiveService.Version.MAM_2.namespace); - sendIqPacket(request, (account, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { - Element prefs = response.findChild("prefs", MessageArchiveService.Version.MAM_2.namespace); - isMamPreferenceAlways = "always".equals(prefs == null ? null : prefs.getAttribute("default")); - } - }); + sendIqPacket( + request, + (account, response) -> { + if (response.getType() == IqPacket.TYPE.RESULT) { + Element prefs = + response.findChild( + "prefs", MessageArchiveService.Version.MAM_2.namespace); + isMamPreferenceAlways = + "always" + .equals( + prefs == null + ? null + : prefs.getAttribute("default")); + } + }); } private void discoverCommands() { final IqPacket request = new IqPacket(IqPacket.TYPE.GET); request.setTo(account.getDomain()); request.addChild("query", Namespace.DISCO_ITEMS).setAttribute("node", Namespace.COMMANDS); - sendIqPacket(request, (account, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { - final Element query = response.findChild("query", Namespace.DISCO_ITEMS); - if (query == null) { - return; - } - final HashMap commands = new HashMap<>(); - for (final Element child : query.getChildren()) { - if ("item".equals(child.getName())) { - final String node = child.getAttribute("node"); - final Jid jid = child.getAttributeAsJid("jid"); - if (node != null && jid != null) { - commands.put(node, jid); + sendIqPacket( + request, + (account, response) -> { + if (response.getType() == IqPacket.TYPE.RESULT) { + final Element query = response.findChild("query", Namespace.DISCO_ITEMS); + if (query == null) { + return; + } + final HashMap commands = new HashMap<>(); + for (final Element child : query.getChildren()) { + if ("item".equals(child.getName())) { + final String node = child.getAttribute("node"); + final Jid jid = child.getAttributeAsJid("jid"); + if (node != null && jid != null) { + commands.put(node, jid); + } + } + } + Log.d(Config.LOGTAG, commands.toString()); + synchronized (this.commands) { + this.commands.clear(); + this.commands.putAll(commands); } } - } - Log.d(Config.LOGTAG, commands.toString()); - synchronized (this.commands) { - this.commands.clear(); - this.commands.putAll(commands); - } - } - }); + }); } public boolean isMamPreferenceAlways() { @@ -1497,7 +1750,9 @@ public boolean isMamPreferenceAlways() { } private void finalizeBind() { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": online with resource " + account.getResource()); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": online with resource " + account.getResource()); if (bindListener != null) { bindListener.onBind(account); } @@ -1507,9 +1762,11 @@ private void finalizeBind() { private void enableAdvancedStreamFeatures() { if (getFeatures().blocking() && !features.blockListRequested) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Requesting block list"); - this.sendIqPacket(getIqGenerator().generateGetBlockList(), mXmppConnectionService.getIqParser()); + this.sendIqPacket( + getIqGenerator().generateGetBlockList(), mXmppConnectionService.getIqParser()); } - for (final OnAdvancedStreamFeaturesLoaded listener : advancedStreamFeaturesLoadedListeners) { + for (final OnAdvancedStreamFeaturesLoaded listener : + advancedStreamFeaturesLoadedListeners) { listener.onAdvancedStreamFeaturesAvailable(account); } if (getFeatures().carbons() && !features.carbonsEnabled) { @@ -1525,46 +1782,60 @@ private void sendServiceDiscoveryItems(final Jid server) { final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); iq.setTo(server.getDomain()); iq.query("http://jabber.org/protocol/disco#items"); - this.sendIqPacket(iq, (account, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { - final HashSet items = new HashSet<>(); - final List elements = packet.query().getChildren(); - for (final Element element : elements) { - if (element.getName().equals("item")) { - final Jid jid = InvalidJid.getNullForInvalid(element.getAttributeAsJid("jid")); - if (jid != null && !jid.equals(account.getDomain())) { - items.add(jid); + this.sendIqPacket( + iq, + (account, packet) -> { + if (packet.getType() == IqPacket.TYPE.RESULT) { + final HashSet items = new HashSet<>(); + final List elements = packet.query().getChildren(); + for (final Element element : elements) { + if (element.getName().equals("item")) { + final Jid jid = + InvalidJid.getNullForInvalid( + element.getAttributeAsJid("jid")); + if (jid != null && !jid.equals(account.getDomain())) { + items.add(jid); + } + } } + for (Jid jid : items) { + sendServiceDiscoveryInfo(jid); + } + } else { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": could not query disco items of " + + server); } - } - for (Jid jid : items) { - sendServiceDiscoveryInfo(jid); - } - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not query disco items of " + server); - } - if (packet.getType() != IqPacket.TYPE.TIMEOUT) { - if (mPendingServiceDiscoveries.decrementAndGet() == 0 - && mWaitForDisco.compareAndSet(true, false)) { - finalizeBind(); - } - } - }); + if (packet.getType() != IqPacket.TYPE.TIMEOUT) { + if (mPendingServiceDiscoveries.decrementAndGet() == 0 + && mWaitForDisco.compareAndSet(true, false)) { + finalizeBind(); + } + } + }); } private void sendEnableCarbons() { final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); iq.addChild("enable", "urn:xmpp:carbons:2"); - this.sendIqPacket(iq, (account, packet) -> { - if (!packet.hasChild("error")) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() - + ": successfully enabled carbons"); - features.carbonsEnabled = true; - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() - + ": could not enable carbons " + packet); - } - }); + this.sendIqPacket( + iq, + (account, packet) -> { + if (!packet.hasChild("error")) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": successfully enabled carbons"); + features.carbonsEnabled = true; + } else { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": could not enable carbons " + + packet); + } + }); } private void processStreamError(final Tag currentTag) throws IOException { @@ -1574,7 +1845,12 @@ private void processStreamError(final Tag currentTag) throws IOException { } if (streamError.hasChild("conflict")) { account.setResource(createNewResource()); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": switching resource due to conflict (" + account.getResource() + ")"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": switching resource due to conflict (" + + account.getResource() + + ")"); throw new IOException(); } else if (streamError.hasChild("host-unknown")) { throw new StateChangingException(Account.State.HOST_UNKNOWN); @@ -1598,11 +1874,8 @@ private void failPendingMessages(final String error) { final MessagePacket packet = (MessagePacket) stanza; final String id = packet.getId(); final Jid to = packet.getTo(); - mXmppConnectionService.markMessage(account, - to.asBareJid(), - id, - Message.STATUS_SEND_FAILED, - error); + mXmppConnectionService.markMessage( + account, to.asBareJid(), id, Message.STATUS_SEND_FAILED, error); } } } @@ -1635,7 +1908,8 @@ public String sendIqPacket(final IqPacket packet, final OnIqPacketReceived callb return this.sendUnmodifiedIqPacket(packet, callback, false); } - public synchronized String sendUnmodifiedIqPacket(final IqPacket packet, final OnIqPacketReceived callback, boolean force) { + public synchronized String sendUnmodifiedIqPacket( + final IqPacket packet, final OnIqPacketReceived callback, boolean force) { if (packet.getId() == null) { packet.setAttribute("id", nextRandomId()); } @@ -1670,7 +1944,11 @@ private synchronized void sendPacket(final AbstractStanza packet, final boolean if (force || isBound) { tagWriter.writeStanzaAsync(packet); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + " do not write stanza to unbound stream " + packet.toString()); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + " do not write stanza to unbound stream " + + packet.toString()); } if (packet instanceof AbstractAcknowledgeableStanza) { AbstractAcknowledgeableStanza stanza = (AbstractAcknowledgeableStanza) packet; @@ -1686,9 +1964,13 @@ private synchronized void sendPacket(final AbstractStanza packet, final boolean this.mStanzaQueue.append(stanzasSent, stanza); if (stanza instanceof MessagePacket && stanza.getId() != null && inSmacksSession) { if (Config.EXTENDED_SM_LOGGING) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": requesting ack for message stanza #" + stanzasSent); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": requesting ack for message stanza #" + + stanzasSent); } - tagWriter.writeStanzaAsync(new RequestPacket(this.smVersion)); + tagWriter.writeStanzaAsync(new RequestPacket()); } } } @@ -1704,23 +1986,19 @@ public void sendPing() { this.lastPingSent = SystemClock.elapsedRealtime(); } - public void setOnMessagePacketReceivedListener( - final OnMessagePacketReceived listener) { + public void setOnMessagePacketReceivedListener(final OnMessagePacketReceived listener) { this.messageListener = listener; } - public void setOnUnregisteredIqPacketReceivedListener( - final OnIqPacketReceived listener) { + public void setOnUnregisteredIqPacketReceivedListener(final OnIqPacketReceived listener) { this.unregisteredIqListener = listener; } - public void setOnPresencePacketReceivedListener( - final OnPresencePacketReceived listener) { + public void setOnPresencePacketReceivedListener(final OnPresencePacketReceived listener) { this.presenceListener = listener; } - public void setOnJinglePacketReceivedListener( - final OnJinglePacketReceived listener) { + public void setOnJinglePacketReceivedListener(final OnJinglePacketReceived listener) { this.jingleListener = listener; } @@ -1736,7 +2014,8 @@ public void setOnMessageAcknowledgeListener(final OnMessageAcknowledged listener this.acknowledgedListener = listener; } - public void addOnAdvancedStreamFeaturesAvailableListener(final OnAdvancedStreamFeaturesLoaded listener) { + public void addOnAdvancedStreamFeaturesAvailableListener( + final OnAdvancedStreamFeaturesLoaded listener) { this.advancedStreamFeaturesLoadedListeners.add(listener); } @@ -1768,15 +2047,28 @@ public void disconnect(final boolean force) { currentTagWriter.writeTag(Tag.end("stream:stream")); if (streamCountDownLatch != null) { if (streamCountDownLatch.await(1, TimeUnit.SECONDS)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": remote ended stream"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": remote ended stream"); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": remote has not closed socket. force closing"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": remote has not closed socket. force closing"); } } } catch (InterruptedException e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": interrupted while gracefully closing stream"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": interrupted while gracefully closing stream"); } catch (final IOException e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": io exception during disconnect (" + e.getMessage() + ")"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": io exception during disconnect (" + + e.getMessage() + + ")"); } finally { FileBackend.close(currentSocket); } @@ -1812,7 +2104,7 @@ public Jid findDiscoItemByFeature(final String feature) { public boolean r() { if (getFeatures().sm()) { - this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion)); + this.tagWriter.writeStanzaAsync(new RequestPacket()); return true; } else { return false; @@ -1847,9 +2139,11 @@ public String getMucServer() { } public int getTimeToNextAttempt() { - final int additionalTime = account.getLastErrorStatus() == Account.State.POLICY_VIOLATION ? 3 : 0; + final int additionalTime = + account.getLastErrorStatus() == Account.State.POLICY_VIOLATION ? 3 : 0; final int interval = Math.min((int) (25 * Math.pow(1.3, (additionalTime + attempt))), 300); - final int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000); + final int secondsSinceLast = + (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000); return interval - secondsSinceLast; } @@ -1908,7 +2202,9 @@ public Identity getServerIdentity() { return Identity.UNKNOWN; } for (final ServiceDiscoveryResult.Identity id : result.getIdentities()) { - if (id.getType().equals("im") && id.getCategory().equals("server") && id.getName() != null) { + if (id.getType().equals("im") + && id.getCategory().equals("server") + && id.getName() != null) { switch (id.getName()) { case "Prosody": return Identity.PROSODY; @@ -1961,7 +2257,7 @@ public X509Certificate[] getCertificateChain(String alias) { @Override public String[] getClientAliases(String s, Principal[] principals) { final String alias = account.getPrivateKeyAlias(); - return alias != null ? new String[]{alias} : new String[0]; + return alias != null ? new String[] {alias} : new String[0]; } @Override @@ -2007,8 +2303,8 @@ public Features(final XmppConnection connection) { private boolean hasDiscoFeature(final Jid server, final String feature) { synchronized (XmppConnection.this.disco) { - return connection.disco.containsKey(server) && - connection.disco.get(server).getFeatures().contains(feature); + return connection.disco.containsKey(server) + && connection.disco.get(server).getFeatures().contains(feature); } } @@ -2027,11 +2323,13 @@ public boolean easyOnboardingInvites() { } public boolean bookmarksConversion() { - return hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS_CONVERSION) && pepPublishOptions(); + return hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS_CONVERSION) + && pepPublishOptions(); } public boolean avatarConversion() { - return hasDiscoFeature(account.getJid().asBareJid(), Namespace.AVATAR_CONVERSION) && pepPublishOptions(); + return hasDiscoFeature(account.getJid().asBareJid(), Namespace.AVATAR_CONVERSION) + && pepPublishOptions(); } public boolean blocking() { @@ -2043,7 +2341,8 @@ public boolean spamReporting() { } public boolean flexibleOfflineMessageRetrieval() { - return hasDiscoFeature(account.getDomain(), Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL); + return hasDiscoFeature( + account.getDomain(), Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL); } public boolean register() { @@ -2051,16 +2350,19 @@ public boolean register() { } public boolean invite() { - return connection.streamFeatures != null && connection.streamFeatures.hasChild("register", Namespace.INVITE); + return connection.streamFeatures != null + && connection.streamFeatures.hasChild("register", Namespace.INVITE); } public boolean sm() { return streamId != null - || (connection.streamFeatures != null && connection.streamFeatures.hasChild("sm")); + || (connection.streamFeatures != null + && connection.streamFeatures.hasChild("sm")); } public boolean csi() { - return connection.streamFeatures != null && connection.streamFeatures.hasChild("csi", "urn:xmpp:csi:0"); + return connection.streamFeatures != null + && connection.streamFeatures.hasChild("csi", Namespace.CSI); } public boolean pep() { @@ -2073,7 +2375,9 @@ public boolean pep() { public boolean pepPersistent() { synchronized (XmppConnection.this.disco) { ServiceDiscoveryResult info = disco.get(account.getJid().asBareJid()); - return info != null && info.getFeatures().contains("http://jabber.org/protocol/pubsub#persistent-items"); + return info != null + && info.getFeatures() + .contains("http://jabber.org/protocol/pubsub#persistent-items"); } } @@ -2082,7 +2386,8 @@ public boolean pepPublishOptions() { } public boolean pepOmemoWhitelisted() { - return hasDiscoFeature(account.getJid().asBareJid(), AxolotlService.PEP_OMEMO_WHITELISTED); + return hasDiscoFeature( + account.getJid().asBareJid(), AxolotlService.PEP_OMEMO_WHITELISTED); } public boolean mam() { @@ -2111,15 +2416,29 @@ public boolean httpUpload(long filesize) { if (Config.DISABLE_HTTP_UPLOAD) { return false; } else { - for (String namespace : new String[]{Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) { - List> items = findDiscoItemsByFeature(namespace); + for (String namespace : + new String[] {Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) { + List> items = + findDiscoItemsByFeature(namespace); if (items.size() > 0) { try { - long maxsize = Long.parseLong(items.get(0).getValue().getExtendedDiscoInformation(namespace, "max-file-size")); + long maxsize = + Long.parseLong( + items.get(0) + .getValue() + .getExtendedDiscoInformation( + namespace, "max-file-size")); if (filesize <= maxsize) { return true; } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": http upload is not available for files with size " + filesize + " (max is " + maxsize + ")"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": http upload is not available for files with size " + + filesize + + " (max is " + + maxsize + + ")"); return false; } } catch (Exception e) { @@ -2132,17 +2451,22 @@ public boolean httpUpload(long filesize) { } public boolean useLegacyHttpUpload() { - return findDiscoItemByFeature(Namespace.HTTP_UPLOAD) == null && findDiscoItemByFeature(Namespace.HTTP_UPLOAD_LEGACY) != null; + return findDiscoItemByFeature(Namespace.HTTP_UPLOAD) == null + && findDiscoItemByFeature(Namespace.HTTP_UPLOAD_LEGACY) != null; } public long getMaxHttpUploadSize() { - for (String namespace : new String[]{Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) { + for (String namespace : + new String[] {Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) { List> items = findDiscoItemsByFeature(namespace); if (items.size() > 0) { try { - return Long.parseLong(items.get(0).getValue().getExtendedDiscoInformation(namespace, "max-file-size")); + return Long.parseLong( + items.get(0) + .getValue() + .getExtendedDiscoInformation(namespace, "max-file-size")); } catch (Exception e) { - //ignored + // ignored } } } @@ -2154,7 +2478,8 @@ public boolean stanzaIds() { } public boolean bookmarks2() { - return Config.USE_BOOKMARKS2 /* || hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS2_COMPAT)*/; + return Config + .USE_BOOKMARKS2 /* || hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS2_COMPAT)*/; } public boolean externalServiceDiscovery() { diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java index 78ab66d8f..e1c465f72 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java @@ -1,10 +1,11 @@ package eu.siacs.conversations.xmpp.stanzas.csi; +import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; public class ActivePacket extends AbstractStanza { public ActivePacket() { super("active"); - setAttribute("xmlns", "urn:xmpp:csi:0"); + setAttribute("xmlns", Namespace.CSI); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java index f109280f1..1b74de066 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java @@ -1,10 +1,11 @@ package eu.siacs.conversations.xmpp.stanzas.csi; +import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; public class InactivePacket extends AbstractStanza { public InactivePacket() { super("inactive"); - setAttribute("xmlns", "urn:xmpp:csi:0"); + setAttribute("xmlns", Namespace.CSI); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java index f93b5d870..9e7b991a4 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java @@ -1,12 +1,13 @@ package eu.siacs.conversations.xmpp.stanzas.streammgmt; +import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; public class AckPacket extends AbstractStanza { - public AckPacket(int sequence, int smVersion) { + public AckPacket(final int sequence) { super("a"); - this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion); + this.setAttribute("xmlns", Namespace.STREAM_MANAGEMENT); this.setAttribute("h", Integer.toString(sequence)); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java index 78cd81edc..95558b143 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java @@ -1,12 +1,13 @@ package eu.siacs.conversations.xmpp.stanzas.streammgmt; +import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; public class EnablePacket extends AbstractStanza { - public EnablePacket(int smVersion) { + public EnablePacket() { super("enable"); - this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion); + this.setAttribute("xmlns", Namespace.STREAM_MANAGEMENT); this.setAttribute("resume", "true"); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java index 98cfc748b..4e0e0f11a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java @@ -1,12 +1,13 @@ package eu.siacs.conversations.xmpp.stanzas.streammgmt; +import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; public class RequestPacket extends AbstractStanza { - public RequestPacket(int smVersion) { + public RequestPacket() { super("r"); - this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion); + this.setAttribute("xmlns", Namespace.STREAM_MANAGEMENT); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java index 9cdcfa5ec..38681d7c1 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java @@ -1,12 +1,13 @@ package eu.siacs.conversations.xmpp.stanzas.streammgmt; +import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; public class ResumePacket extends AbstractStanza { - public ResumePacket(String id, int sequence, int smVersion) { + public ResumePacket(final String id, final int sequence) { super("resume"); - this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion); + this.setAttribute("xmlns", Namespace.STREAM_MANAGEMENT); this.setAttribute("previd", id); this.setAttribute("h", Integer.toString(sequence)); } From eb49a7f5e57a627b5c4217e6848fc9135db2236a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 3 Sep 2022 12:33:27 +0200 Subject: [PATCH 177/394] fix crash in buggy connection manager. fixes #4368 --- .../services/XmppConnectionService.java | 6 ++++-- .../siacs/conversations/ui/XmppActivity.java | 13 ++---------- .../conversations/utils/Compatibility.java | 21 +++++++++++++++---- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index af68db19c..ba2c8514e 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -945,9 +945,11 @@ public void discoverChannels(String query, ChannelDiscoveryService.Method method public boolean isDataSaverDisabled() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE); + final ConnectivityManager connectivityManager = + (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE); return !connectivityManager.isActiveNetworkMetered() - || connectivityManager.getRestrictBackgroundStatus() == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED; + || Compatibility.getRestrictBackgroundStatus(connectivityManager) + == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED; } else { return true; } diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 6ac8f7279..644bd7ec5 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -80,6 +80,7 @@ import eu.siacs.conversations.ui.util.PresenceSelector; import eu.siacs.conversations.ui.util.SoftKeyboardUtils; import eu.siacs.conversations.utils.AccountUtils; +import eu.siacs.conversations.utils.Compatibility; import eu.siacs.conversations.utils.ExceptionHelper; import eu.siacs.conversations.ui.util.SettingsUtils; import eu.siacs.conversations.utils.ThemeHelper; @@ -448,22 +449,12 @@ protected boolean isAffectedByDataSaver() { final ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); return cm != null && cm.isActiveNetworkMetered() - && getRestrictBackgroundStatus(cm) == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED; + && Compatibility.getRestrictBackgroundStatus(cm) == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED; } else { return false; } } - @RequiresApi(api = Build.VERSION_CODES.N) - private static int getRestrictBackgroundStatus(@NonNull final ConnectivityManager connectivityManager) { - try { - return connectivityManager.getRestrictBackgroundStatus(); - } catch (final Exception e) { - Log.d(Config.LOGTAG,"platform bug detected. Unable to get restrict background status",e); - return -1; - } - } - private boolean usingEnterKey() { return getBooleanPreference("display_enter_key", R.bool.display_enter_key); } diff --git a/src/main/java/eu/siacs/conversations/utils/Compatibility.java b/src/main/java/eu/siacs/conversations/utils/Compatibility.java index c28b8fe29..b1145794d 100644 --- a/src/main/java/eu/siacs/conversations/utils/Compatibility.java +++ b/src/main/java/eu/siacs/conversations/utils/Compatibility.java @@ -8,6 +8,7 @@ import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.net.ConnectivityManager; import android.os.Build; import android.preference.Preference; import android.preference.PreferenceCategory; @@ -15,6 +16,8 @@ import android.util.Log; import androidx.annotation.BoolRes; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; import androidx.core.content.ContextCompat; import java.util.Arrays; @@ -158,10 +161,20 @@ public static void startService(Context context, Intent intent) { @SuppressLint("UnsupportedChromeOsCameraSystemFeature") public static boolean hasFeatureCamera(final Context context) { final PackageManager packageManager = context.getPackageManager(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - return packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); - } else { - return packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA); + return packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + public static int getRestrictBackgroundStatus( + @NonNull final ConnectivityManager connectivityManager) { + try { + return connectivityManager.getRestrictBackgroundStatus(); + } catch (final Exception e) { + Log.d( + Config.LOGTAG, + "platform bug detected. Unable to get restrict background status", + e); + return ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED; } } } From e204457c319b6a4327ad7f4a1babb17a22c6f802 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 3 Sep 2022 15:51:12 +0200 Subject: [PATCH 178/394] show toast warning about unavailable calls when using tor closes #4103 --- .../conversations/ui/EditAccountActivity.java | 3 + .../conversations/ui/SettingsActivity.java | 868 ++++++++++-------- src/main/res/values/strings.xml | 1 + 3 files changed, 472 insertions(+), 400 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index fc21f6296..19424ee2b 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -293,6 +293,9 @@ public void onClick(final View v) { } binding.hostnameLayout.setError(null); binding.portLayout.setError(null); + if (mAccount.isOnion()) { + Toast.makeText(EditAccountActivity.this, R.string.audio_video_disabled_tor, Toast.LENGTH_LONG).show(); + } if (mAccount.isEnabled() && !registerNewAccount && !mInitMode) { diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java index 7073b881d..21d2b956c 100644 --- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java @@ -38,409 +38,477 @@ import eu.siacs.conversations.services.ExportBackupService; import eu.siacs.conversations.services.MemorizingTrustManager; import eu.siacs.conversations.services.QuickConversationsService; +import eu.siacs.conversations.ui.util.SettingsUtils; import eu.siacs.conversations.ui.util.StyledAttributes; import eu.siacs.conversations.utils.GeoHelper; -import eu.siacs.conversations.ui.util.SettingsUtils; import eu.siacs.conversations.utils.TimeFrameUtils; import eu.siacs.conversations.xmpp.Jid; -public class SettingsActivity extends XmppActivity implements - OnSharedPreferenceChangeListener { - - public static final String KEEP_FOREGROUND_SERVICE = "enable_foreground_service"; - public static final String AWAY_WHEN_SCREEN_IS_OFF = "away_when_screen_off"; - public static final String TREAT_VIBRATE_AS_SILENT = "treat_vibrate_as_silent"; - public static final String DND_ON_SILENT_MODE = "dnd_on_silent_mode"; - public static final String MANUALLY_CHANGE_PRESENCE = "manually_change_presence"; - public static final String BLIND_TRUST_BEFORE_VERIFICATION = "btbv"; - public static final String AUTOMATIC_MESSAGE_DELETION = "automatic_message_deletion"; - public static final String BROADCAST_LAST_ACTIVITY = "last_activity"; - public static final String THEME = "theme"; - public static final String SHOW_DYNAMIC_TAGS = "show_dynamic_tags"; - public static final String OMEMO_SETTING = "omemo"; - public static final String PREVENT_SCREENSHOTS = "prevent_screenshots"; - - public static final int REQUEST_CREATE_BACKUP = 0xbf8701; - - private SettingsFragment mSettingsFragment; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_settings); - FragmentManager fm = getFragmentManager(); - mSettingsFragment = (SettingsFragment) fm.findFragmentById(R.id.settings_content); - if (mSettingsFragment == null || !mSettingsFragment.getClass().equals(SettingsFragment.class)) { - mSettingsFragment = new SettingsFragment(); - fm.beginTransaction().replace(R.id.settings_content, mSettingsFragment).commit(); - } - mSettingsFragment.setActivityIntent(getIntent()); - this.mTheme = findTheme(); - setTheme(this.mTheme); - getWindow().getDecorView().setBackgroundColor(StyledAttributes.getColor(this, R.attr.color_background_primary)); - setSupportActionBar(findViewById(R.id.toolbar)); - configureActionBar(getSupportActionBar()); - } - - @Override - void onBackendConnected() { - - } - - @Override - public void onStart() { - super.onStart(); - PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(this); - - changeOmemoSettingSummary(); - - if (QuickConversationsService.isQuicksy()) { - final PreferenceCategory connectionOptions = (PreferenceCategory) mSettingsFragment.findPreference("connection_options"); - final PreferenceCategory groupChats = (PreferenceCategory) mSettingsFragment.findPreference("group_chats"); - final Preference channelDiscoveryMethod = mSettingsFragment.findPreference("channel_discovery_method"); - PreferenceScreen expert = (PreferenceScreen) mSettingsFragment.findPreference("expert"); - if (connectionOptions != null) { - expert.removePreference(connectionOptions); - } - if (groupChats != null && channelDiscoveryMethod != null) { - groupChats.removePreference(channelDiscoveryMethod); - } - } - - PreferenceScreen mainPreferenceScreen = (PreferenceScreen) mSettingsFragment.findPreference("main_screen"); - - PreferenceCategory attachmentsCategory = (PreferenceCategory) mSettingsFragment.findPreference("attachments"); - CheckBoxPreference locationPlugin = (CheckBoxPreference) mSettingsFragment.findPreference("use_share_location_plugin"); - if (attachmentsCategory != null && locationPlugin != null) { - if (!GeoHelper.isLocationPluginInstalled(this)) { - attachmentsCategory.removePreference(locationPlugin); - } - } - - //this feature is only available on Huawei Android 6. - PreferenceScreen huaweiPreferenceScreen = (PreferenceScreen) mSettingsFragment.findPreference("huawei"); - if (huaweiPreferenceScreen != null) { - Intent intent = huaweiPreferenceScreen.getIntent(); - //remove when Api version is above M (Version 6.0) or if the intent is not callable - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M || !isCallable(intent)) { - PreferenceCategory generalCategory = (PreferenceCategory) mSettingsFragment.findPreference("general"); - generalCategory.removePreference(huaweiPreferenceScreen); - if (generalCategory.getPreferenceCount() == 0) { - if (mainPreferenceScreen != null) { - mainPreferenceScreen.removePreference(generalCategory); - } - } - } - } - - ListPreference automaticMessageDeletionList = (ListPreference) mSettingsFragment.findPreference(AUTOMATIC_MESSAGE_DELETION); - if (automaticMessageDeletionList != null) { - final int[] choices = getResources().getIntArray(R.array.automatic_message_deletion_values); - CharSequence[] entries = new CharSequence[choices.length]; - CharSequence[] entryValues = new CharSequence[choices.length]; - for (int i = 0; i < choices.length; ++i) { - entryValues[i] = String.valueOf(choices[i]); - if (choices[i] == 0) { - entries[i] = getString(R.string.never); - } else { - entries[i] = TimeFrameUtils.resolve(this, 1000L * choices[i]); - } - } - automaticMessageDeletionList.setEntries(entries); - automaticMessageDeletionList.setEntryValues(entryValues); - } - - - boolean removeLocation = new Intent("eu.siacs.conversations.location.request").resolveActivity(getPackageManager()) == null; - boolean removeVoice = new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION).resolveActivity(getPackageManager()) == null; - - ListPreference quickAction = (ListPreference) mSettingsFragment.findPreference("quick_action"); - if (quickAction != null && (removeLocation || removeVoice)) { - ArrayList entries = new ArrayList<>(Arrays.asList(quickAction.getEntries())); - ArrayList entryValues = new ArrayList<>(Arrays.asList(quickAction.getEntryValues())); - int index = entryValues.indexOf("location"); - if (index > 0 && removeLocation) { - entries.remove(index); - entryValues.remove(index); - } - index = entryValues.indexOf("voice"); - if (index > 0 && removeVoice) { - entries.remove(index); - entryValues.remove(index); - } - quickAction.setEntries(entries.toArray(new CharSequence[entries.size()])); - quickAction.setEntryValues(entryValues.toArray(new CharSequence[entryValues.size()])); - } - - final Preference removeCertsPreference = mSettingsFragment.findPreference("remove_trusted_certificates"); - if (removeCertsPreference != null) { - removeCertsPreference.setOnPreferenceClickListener(preference -> { - final MemorizingTrustManager mtm = xmppConnectionService.getMemorizingTrustManager(); - final ArrayList aliases = Collections.list(mtm.getCertificates()); - if (aliases.size() == 0) { - displayToast(getString(R.string.toast_no_trusted_certs)); - return true; - } - final ArrayList selectedItems = new ArrayList<>(); - final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(SettingsActivity.this); - dialogBuilder.setTitle(getResources().getString(R.string.dialog_manage_certs_title)); - dialogBuilder.setMultiChoiceItems(aliases.toArray(new CharSequence[aliases.size()]), null, - (dialog, indexSelected, isChecked) -> { - if (isChecked) { - selectedItems.add(indexSelected); - } else if (selectedItems.contains(indexSelected)) { - selectedItems.remove(Integer.valueOf(indexSelected)); - } - ((AlertDialog) dialog).getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(selectedItems.size() > 0); - }); - - dialogBuilder.setPositiveButton( - getResources().getString(R.string.dialog_manage_certs_positivebutton), (dialog, which) -> { - int count = selectedItems.size(); - if (count > 0) { - for (int i = 0; i < count; i++) { - try { - Integer item = Integer.valueOf(selectedItems.get(i).toString()); - String alias = aliases.get(item); - mtm.deleteCertificate(alias); - } catch (KeyStoreException e) { - e.printStackTrace(); - displayToast("Error: " + e.getLocalizedMessage()); - } - } - if (xmppConnectionServiceBound) { - reconnectAccounts(); - } - displayToast(getResources().getQuantityString(R.plurals.toast_delete_certificates, count, count)); - } - }); - dialogBuilder.setNegativeButton(getResources().getString(R.string.dialog_manage_certs_negativebutton), null); - AlertDialog removeCertsDialog = dialogBuilder.create(); - removeCertsDialog.show(); - removeCertsDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); - return true; - }); - } - - final Preference createBackupPreference = mSettingsFragment.findPreference("create_backup"); - if (createBackupPreference != null) { - createBackupPreference.setSummary(getString(R.string.pref_create_backup_summary, FileBackend.getBackupDirectory(this).getAbsolutePath())); - createBackupPreference.setOnPreferenceClickListener(preference -> { - if (hasStoragePermission(REQUEST_CREATE_BACKUP)) { - createBackup(); - } - return true; - }); - } - - if (Config.ONLY_INTERNAL_STORAGE) { - final Preference cleanCachePreference = mSettingsFragment.findPreference("clean_cache"); - if (cleanCachePreference != null) { - cleanCachePreference.setOnPreferenceClickListener(preference -> cleanCache()); - } - - final Preference cleanPrivateStoragePreference = mSettingsFragment.findPreference("clean_private_storage"); - if (cleanPrivateStoragePreference != null) { - cleanPrivateStoragePreference.setOnPreferenceClickListener(preference -> cleanPrivateStorage()); - } - } - - final Preference deleteOmemoPreference = mSettingsFragment.findPreference("delete_omemo_identities"); - if (deleteOmemoPreference != null) { - deleteOmemoPreference.setOnPreferenceClickListener(preference -> deleteOmemoIdentities()); - } - } - - private void changeOmemoSettingSummary() { - ListPreference omemoPreference = (ListPreference) mSettingsFragment.findPreference(OMEMO_SETTING); - if (omemoPreference != null) { - String value = omemoPreference.getValue(); - switch (value) { - case "always": - omemoPreference.setSummary(R.string.pref_omemo_setting_summary_always); - break; - case "default_on": - omemoPreference.setSummary(R.string.pref_omemo_setting_summary_default_on); - break; - case "default_off": - omemoPreference.setSummary(R.string.pref_omemo_setting_summary_default_off); - break; - } - } else { - Log.d(Config.LOGTAG,"unable to find preference named "+OMEMO_SETTING); - } - } - - private boolean isCallable(final Intent i) { - return i != null && getPackageManager().queryIntentActivities(i, PackageManager.MATCH_DEFAULT_ONLY).size() > 0; - } - - - private boolean cleanCache() { - Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - intent.setData(Uri.parse("package:" + getPackageName())); - startActivity(intent); - return true; - } - - private boolean cleanPrivateStorage() { - for(String type : Arrays.asList("Images", "Videos", "Files", "Recordings")) { - cleanPrivateFiles(type); - } - return true; - } - - private void cleanPrivateFiles(final String type) { - try { - File dir = new File(getFilesDir().getAbsolutePath(), "/" + type + "/"); - File[] array = dir.listFiles(); - if (array != null) { - for (int b = 0; b < array.length; b++) { - String name = array[b].getName().toLowerCase(); - if (name.equals(".nomedia")) { - continue; - } - if (array[b].isFile()) { - array[b].delete(); - } - } - } - } catch (Throwable e) { - Log.e("CleanCache", e.toString()); - } - } - - private boolean deleteOmemoIdentities() { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.pref_delete_omemo_identities); - final List accounts = new ArrayList<>(); - for (Account account : xmppConnectionService.getAccounts()) { - if (account.isEnabled()) { - accounts.add(account.getJid().asBareJid().toString()); - } - } - final boolean[] checkedItems = new boolean[accounts.size()]; - builder.setMultiChoiceItems(accounts.toArray(new CharSequence[accounts.size()]), checkedItems, (dialog, which, isChecked) -> { - checkedItems[which] = isChecked; - final AlertDialog alertDialog = (AlertDialog) dialog; - for (boolean item : checkedItems) { - if (item) { - alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(true); - return; - } - } - alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(false); - }); - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.delete_selected_keys, (dialog, which) -> { - for (int i = 0; i < checkedItems.length; ++i) { - if (checkedItems[i]) { - try { - Jid jid = Jid.of(accounts.get(i).toString()); - Account account = xmppConnectionService.findAccountByJid(jid); - if (account != null) { - account.getAxolotlService().regenerateKeys(true); - } - } catch (IllegalArgumentException e) { - // - } - - } - } - }); - AlertDialog dialog = builder.create(); - dialog.show(); - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); - return true; - } - - @Override - public void onStop() { - super.onStop(); - PreferenceManager.getDefaultSharedPreferences(this) - .unregisterOnSharedPreferenceChangeListener(this); - } - - @Override - public void onSharedPreferenceChanged(SharedPreferences preferences, String name) { - final List resendPresence = Arrays.asList( - "confirm_messages", - DND_ON_SILENT_MODE, - AWAY_WHEN_SCREEN_IS_OFF, - "allow_message_correction", - TREAT_VIBRATE_AS_SILENT, - MANUALLY_CHANGE_PRESENCE, - BROADCAST_LAST_ACTIVITY); - if (name.equals(OMEMO_SETTING)) { - OmemoSetting.load(this, preferences); - changeOmemoSettingSummary(); - } else if (name.equals(KEEP_FOREGROUND_SERVICE)) { - xmppConnectionService.toggleForegroundService(); - } else if (resendPresence.contains(name)) { - if (xmppConnectionServiceBound) { - if (name.equals(AWAY_WHEN_SCREEN_IS_OFF) || name.equals(MANUALLY_CHANGE_PRESENCE)) { - xmppConnectionService.toggleScreenEventReceiver(); - } - xmppConnectionService.refreshAllPresences(); - } - } else if (name.equals("dont_trust_system_cas")) { - xmppConnectionService.updateMemorizingTrustmanager(); - reconnectAccounts(); - } else if (name.equals("use_tor")) { - reconnectAccounts(); - xmppConnectionService.reinitializeMuclumbusService(); - } else if (name.equals(AUTOMATIC_MESSAGE_DELETION)) { - xmppConnectionService.expireOldMessages(true); - } else if (name.equals(THEME)) { - final int theme = findTheme(); - if (this.mTheme != theme) { - recreate(); - } - } else if(name.equals(PREVENT_SCREENSHOTS)){ - SettingsUtils.applyScreenshotPreventionSetting(this); - } - } - - @Override - public void onResume(){ - super.onResume(); - SettingsUtils.applyScreenshotPreventionSetting(this); - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - if (grantResults.length > 0) - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - if (requestCode == REQUEST_CREATE_BACKUP) { - createBackup(); - } - } else { - Toast.makeText(this, getString(R.string.no_storage_permission, getString(R.string.app_name)), Toast.LENGTH_SHORT).show(); - } - } - - private void createBackup() { - ContextCompat.startForegroundService(this, new Intent(this, ExportBackupService.class)); - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setMessage(R.string.backup_started_message); - builder.setPositiveButton(R.string.ok, null); - builder.create().show(); - } - - private void displayToast(final String msg) { - runOnUiThread(() -> Toast.makeText(SettingsActivity.this, msg, Toast.LENGTH_LONG).show()); - } - - private void reconnectAccounts() { - for (Account account : xmppConnectionService.getAccounts()) { - if (account.isEnabled()) { - xmppConnectionService.reconnectAccountInBackground(account); - } - } - } - - public void refreshUiReal() { - //nothing to do. This Activity doesn't implement any listeners - } - +public class SettingsActivity extends XmppActivity implements OnSharedPreferenceChangeListener { + + public static final String KEEP_FOREGROUND_SERVICE = "enable_foreground_service"; + public static final String AWAY_WHEN_SCREEN_IS_OFF = "away_when_screen_off"; + public static final String TREAT_VIBRATE_AS_SILENT = "treat_vibrate_as_silent"; + public static final String DND_ON_SILENT_MODE = "dnd_on_silent_mode"; + public static final String MANUALLY_CHANGE_PRESENCE = "manually_change_presence"; + public static final String BLIND_TRUST_BEFORE_VERIFICATION = "btbv"; + public static final String AUTOMATIC_MESSAGE_DELETION = "automatic_message_deletion"; + public static final String BROADCAST_LAST_ACTIVITY = "last_activity"; + public static final String THEME = "theme"; + public static final String SHOW_DYNAMIC_TAGS = "show_dynamic_tags"; + public static final String OMEMO_SETTING = "omemo"; + public static final String PREVENT_SCREENSHOTS = "prevent_screenshots"; + + public static final int REQUEST_CREATE_BACKUP = 0xbf8701; + + private SettingsFragment mSettingsFragment; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_settings); + FragmentManager fm = getFragmentManager(); + mSettingsFragment = (SettingsFragment) fm.findFragmentById(R.id.settings_content); + if (mSettingsFragment == null + || !mSettingsFragment.getClass().equals(SettingsFragment.class)) { + mSettingsFragment = new SettingsFragment(); + fm.beginTransaction().replace(R.id.settings_content, mSettingsFragment).commit(); + } + mSettingsFragment.setActivityIntent(getIntent()); + this.mTheme = findTheme(); + setTheme(this.mTheme); + getWindow() + .getDecorView() + .setBackgroundColor( + StyledAttributes.getColor(this, R.attr.color_background_primary)); + setSupportActionBar(findViewById(R.id.toolbar)); + configureActionBar(getSupportActionBar()); + } + + @Override + void onBackendConnected() {} + + @Override + public void onStart() { + super.onStart(); + PreferenceManager.getDefaultSharedPreferences(this) + .registerOnSharedPreferenceChangeListener(this); + + changeOmemoSettingSummary(); + + if (QuickConversationsService.isQuicksy()) { + final PreferenceCategory connectionOptions = + (PreferenceCategory) mSettingsFragment.findPreference("connection_options"); + final PreferenceCategory groupChats = + (PreferenceCategory) mSettingsFragment.findPreference("group_chats"); + final Preference channelDiscoveryMethod = + mSettingsFragment.findPreference("channel_discovery_method"); + PreferenceScreen expert = (PreferenceScreen) mSettingsFragment.findPreference("expert"); + if (connectionOptions != null) { + expert.removePreference(connectionOptions); + } + if (groupChats != null && channelDiscoveryMethod != null) { + groupChats.removePreference(channelDiscoveryMethod); + } + } + + PreferenceScreen mainPreferenceScreen = + (PreferenceScreen) mSettingsFragment.findPreference("main_screen"); + + PreferenceCategory attachmentsCategory = + (PreferenceCategory) mSettingsFragment.findPreference("attachments"); + CheckBoxPreference locationPlugin = + (CheckBoxPreference) mSettingsFragment.findPreference("use_share_location_plugin"); + if (attachmentsCategory != null && locationPlugin != null) { + if (!GeoHelper.isLocationPluginInstalled(this)) { + attachmentsCategory.removePreference(locationPlugin); + } + } + + // this feature is only available on Huawei Android 6. + PreferenceScreen huaweiPreferenceScreen = + (PreferenceScreen) mSettingsFragment.findPreference("huawei"); + if (huaweiPreferenceScreen != null) { + Intent intent = huaweiPreferenceScreen.getIntent(); + // remove when Api version is above M (Version 6.0) or if the intent is not callable + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M || !isCallable(intent)) { + PreferenceCategory generalCategory = + (PreferenceCategory) mSettingsFragment.findPreference("general"); + generalCategory.removePreference(huaweiPreferenceScreen); + if (generalCategory.getPreferenceCount() == 0) { + if (mainPreferenceScreen != null) { + mainPreferenceScreen.removePreference(generalCategory); + } + } + } + } + + ListPreference automaticMessageDeletionList = + (ListPreference) mSettingsFragment.findPreference(AUTOMATIC_MESSAGE_DELETION); + if (automaticMessageDeletionList != null) { + final int[] choices = + getResources().getIntArray(R.array.automatic_message_deletion_values); + CharSequence[] entries = new CharSequence[choices.length]; + CharSequence[] entryValues = new CharSequence[choices.length]; + for (int i = 0; i < choices.length; ++i) { + entryValues[i] = String.valueOf(choices[i]); + if (choices[i] == 0) { + entries[i] = getString(R.string.never); + } else { + entries[i] = TimeFrameUtils.resolve(this, 1000L * choices[i]); + } + } + automaticMessageDeletionList.setEntries(entries); + automaticMessageDeletionList.setEntryValues(entryValues); + } + + boolean removeLocation = + new Intent("eu.siacs.conversations.location.request") + .resolveActivity(getPackageManager()) + == null; + boolean removeVoice = + new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION) + .resolveActivity(getPackageManager()) + == null; + + ListPreference quickAction = + (ListPreference) mSettingsFragment.findPreference("quick_action"); + if (quickAction != null && (removeLocation || removeVoice)) { + ArrayList entries = + new ArrayList<>(Arrays.asList(quickAction.getEntries())); + ArrayList entryValues = + new ArrayList<>(Arrays.asList(quickAction.getEntryValues())); + int index = entryValues.indexOf("location"); + if (index > 0 && removeLocation) { + entries.remove(index); + entryValues.remove(index); + } + index = entryValues.indexOf("voice"); + if (index > 0 && removeVoice) { + entries.remove(index); + entryValues.remove(index); + } + quickAction.setEntries(entries.toArray(new CharSequence[entries.size()])); + quickAction.setEntryValues(entryValues.toArray(new CharSequence[entryValues.size()])); + } + + final Preference removeCertsPreference = + mSettingsFragment.findPreference("remove_trusted_certificates"); + if (removeCertsPreference != null) { + removeCertsPreference.setOnPreferenceClickListener( + preference -> { + final MemorizingTrustManager mtm = + xmppConnectionService.getMemorizingTrustManager(); + final ArrayList aliases = Collections.list(mtm.getCertificates()); + if (aliases.size() == 0) { + displayToast(getString(R.string.toast_no_trusted_certs)); + return true; + } + final ArrayList selectedItems = new ArrayList<>(); + final AlertDialog.Builder dialogBuilder = + new AlertDialog.Builder(SettingsActivity.this); + dialogBuilder.setTitle( + getResources().getString(R.string.dialog_manage_certs_title)); + dialogBuilder.setMultiChoiceItems( + aliases.toArray(new CharSequence[aliases.size()]), + null, + (dialog, indexSelected, isChecked) -> { + if (isChecked) { + selectedItems.add(indexSelected); + } else if (selectedItems.contains(indexSelected)) { + selectedItems.remove(Integer.valueOf(indexSelected)); + } + ((AlertDialog) dialog) + .getButton(DialogInterface.BUTTON_POSITIVE) + .setEnabled(selectedItems.size() > 0); + }); + + dialogBuilder.setPositiveButton( + getResources() + .getString(R.string.dialog_manage_certs_positivebutton), + (dialog, which) -> { + int count = selectedItems.size(); + if (count > 0) { + for (int i = 0; i < count; i++) { + try { + Integer item = + Integer.valueOf( + selectedItems.get(i).toString()); + String alias = aliases.get(item); + mtm.deleteCertificate(alias); + } catch (KeyStoreException e) { + e.printStackTrace(); + displayToast("Error: " + e.getLocalizedMessage()); + } + } + if (xmppConnectionServiceBound) { + reconnectAccounts(); + } + displayToast( + getResources() + .getQuantityString( + R.plurals.toast_delete_certificates, + count, + count)); + } + }); + dialogBuilder.setNegativeButton( + getResources() + .getString(R.string.dialog_manage_certs_negativebutton), + null); + AlertDialog removeCertsDialog = dialogBuilder.create(); + removeCertsDialog.show(); + removeCertsDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); + return true; + }); + } + + final Preference createBackupPreference = mSettingsFragment.findPreference("create_backup"); + if (createBackupPreference != null) { + createBackupPreference.setSummary( + getString( + R.string.pref_create_backup_summary, + FileBackend.getBackupDirectory(this).getAbsolutePath())); + createBackupPreference.setOnPreferenceClickListener( + preference -> { + if (hasStoragePermission(REQUEST_CREATE_BACKUP)) { + createBackup(); + } + return true; + }); + } + + if (Config.ONLY_INTERNAL_STORAGE) { + final Preference cleanCachePreference = mSettingsFragment.findPreference("clean_cache"); + if (cleanCachePreference != null) { + cleanCachePreference.setOnPreferenceClickListener(preference -> cleanCache()); + } + + final Preference cleanPrivateStoragePreference = + mSettingsFragment.findPreference("clean_private_storage"); + if (cleanPrivateStoragePreference != null) { + cleanPrivateStoragePreference.setOnPreferenceClickListener( + preference -> cleanPrivateStorage()); + } + } + + final Preference deleteOmemoPreference = + mSettingsFragment.findPreference("delete_omemo_identities"); + if (deleteOmemoPreference != null) { + deleteOmemoPreference.setOnPreferenceClickListener( + preference -> deleteOmemoIdentities()); + } + } + + private void changeOmemoSettingSummary() { + ListPreference omemoPreference = + (ListPreference) mSettingsFragment.findPreference(OMEMO_SETTING); + if (omemoPreference != null) { + String value = omemoPreference.getValue(); + switch (value) { + case "always": + omemoPreference.setSummary(R.string.pref_omemo_setting_summary_always); + break; + case "default_on": + omemoPreference.setSummary(R.string.pref_omemo_setting_summary_default_on); + break; + case "default_off": + omemoPreference.setSummary(R.string.pref_omemo_setting_summary_default_off); + break; + } + } else { + Log.d(Config.LOGTAG, "unable to find preference named " + OMEMO_SETTING); + } + } + + private boolean isCallable(final Intent i) { + return i != null + && getPackageManager() + .queryIntentActivities(i, PackageManager.MATCH_DEFAULT_ONLY) + .size() + > 0; + } + + private boolean cleanCache() { + Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(Uri.parse("package:" + getPackageName())); + startActivity(intent); + return true; + } + + private boolean cleanPrivateStorage() { + for (String type : Arrays.asList("Images", "Videos", "Files", "Recordings")) { + cleanPrivateFiles(type); + } + return true; + } + + private void cleanPrivateFiles(final String type) { + try { + File dir = new File(getFilesDir().getAbsolutePath(), "/" + type + "/"); + File[] array = dir.listFiles(); + if (array != null) { + for (int b = 0; b < array.length; b++) { + String name = array[b].getName().toLowerCase(); + if (name.equals(".nomedia")) { + continue; + } + if (array[b].isFile()) { + array[b].delete(); + } + } + } + } catch (Throwable e) { + Log.e("CleanCache", e.toString()); + } + } + + private boolean deleteOmemoIdentities() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.pref_delete_omemo_identities); + final List accounts = new ArrayList<>(); + for (Account account : xmppConnectionService.getAccounts()) { + if (account.isEnabled()) { + accounts.add(account.getJid().asBareJid().toString()); + } + } + final boolean[] checkedItems = new boolean[accounts.size()]; + builder.setMultiChoiceItems( + accounts.toArray(new CharSequence[accounts.size()]), + checkedItems, + (dialog, which, isChecked) -> { + checkedItems[which] = isChecked; + final AlertDialog alertDialog = (AlertDialog) dialog; + for (boolean item : checkedItems) { + if (item) { + alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(true); + return; + } + } + alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(false); + }); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton( + R.string.delete_selected_keys, + (dialog, which) -> { + for (int i = 0; i < checkedItems.length; ++i) { + if (checkedItems[i]) { + try { + Jid jid = Jid.of(accounts.get(i).toString()); + Account account = xmppConnectionService.findAccountByJid(jid); + if (account != null) { + account.getAxolotlService().regenerateKeys(true); + } + } catch (IllegalArgumentException e) { + // + } + } + } + }); + AlertDialog dialog = builder.create(); + dialog.show(); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); + return true; + } + + @Override + public void onStop() { + super.onStop(); + PreferenceManager.getDefaultSharedPreferences(this) + .unregisterOnSharedPreferenceChangeListener(this); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences preferences, String name) { + final List resendPresence = + Arrays.asList( + "confirm_messages", + DND_ON_SILENT_MODE, + AWAY_WHEN_SCREEN_IS_OFF, + "allow_message_correction", + TREAT_VIBRATE_AS_SILENT, + MANUALLY_CHANGE_PRESENCE, + BROADCAST_LAST_ACTIVITY); + if (name.equals(OMEMO_SETTING)) { + OmemoSetting.load(this, preferences); + changeOmemoSettingSummary(); + } else if (name.equals(KEEP_FOREGROUND_SERVICE)) { + xmppConnectionService.toggleForegroundService(); + } else if (resendPresence.contains(name)) { + if (xmppConnectionServiceBound) { + if (name.equals(AWAY_WHEN_SCREEN_IS_OFF) || name.equals(MANUALLY_CHANGE_PRESENCE)) { + xmppConnectionService.toggleScreenEventReceiver(); + } + xmppConnectionService.refreshAllPresences(); + } + } else if (name.equals("dont_trust_system_cas")) { + xmppConnectionService.updateMemorizingTrustmanager(); + reconnectAccounts(); + } else if (name.equals("use_tor")) { + if (preferences.getBoolean(name, false)) { + displayToast(getString(R.string.audio_video_disabled_tor)); + } + reconnectAccounts(); + xmppConnectionService.reinitializeMuclumbusService(); + } else if (name.equals(AUTOMATIC_MESSAGE_DELETION)) { + xmppConnectionService.expireOldMessages(true); + } else if (name.equals(THEME)) { + final int theme = findTheme(); + if (this.mTheme != theme) { + recreate(); + } + } else if (name.equals(PREVENT_SCREENSHOTS)) { + SettingsUtils.applyScreenshotPreventionSetting(this); + } + } + + @Override + public void onResume() { + super.onResume(); + SettingsUtils.applyScreenshotPreventionSetting(this); + } + + @Override + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (grantResults.length > 0) + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if (requestCode == REQUEST_CREATE_BACKUP) { + createBackup(); + } + } else { + Toast.makeText( + this, + getString( + R.string.no_storage_permission, + getString(R.string.app_name)), + Toast.LENGTH_SHORT) + .show(); + } + } + + private void createBackup() { + ContextCompat.startForegroundService(this, new Intent(this, ExportBackupService.class)); + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setMessage(R.string.backup_started_message); + builder.setPositiveButton(R.string.ok, null); + builder.create().show(); + } + + private void displayToast(final String msg) { + runOnUiThread(() -> Toast.makeText(SettingsActivity.this, msg, Toast.LENGTH_LONG).show()); + } + + private void reconnectAccounts() { + for (Account account : xmppConnectionService.getAccounts()) { + if (account.isEnabled()) { + xmppConnectionService.reconnectAccountInBackground(account); + } + } + } + + public void refreshUiReal() { + // nothing to do. This Activity doesn't implement any listeners + } } diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index cd4412588..f668e3f25 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -986,5 +986,6 @@ No XMPP address found Temporary authentication failure Delete avatar + Calls are disabled when using Tor From 052c58f3770e0702077221dbfa1913c15854519a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 3 Sep 2022 20:17:29 +0200 Subject: [PATCH 179/394] rudimentary bind 2 implementation --- .../conversations/parser/MessageParser.java | 6 +- .../eu/siacs/conversations/xml/Namespace.java | 3 +- .../conversations/xmpp/XmppConnection.java | 116 ++++++++++++++---- 3 files changed, 94 insertions(+), 31 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 50743312c..76945c472 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -313,7 +313,7 @@ private void setNick(Account account, Jid user, String nick) { private boolean handleErrorMessage(final Account account, final MessagePacket packet) { if (packet.getType() == MessagePacket.TYPE_ERROR) { if (packet.fromServer(account)) { - final Pair forwarded = packet.getForwardedMessagePacket("received", "urn:xmpp:carbons:2"); + final Pair forwarded = packet.getForwardedMessagePacket("received", Namespace.CARBONS); if (forwarded != null) { return handleErrorMessage(account, forwarded.first); } @@ -389,8 +389,8 @@ public void onMessagePacketReceived(Account account, MessagePacket original) { return; } else if (original.fromServer(account)) { Pair f; - f = original.getForwardedMessagePacket("received", "urn:xmpp:carbons:2"); - f = f == null ? original.getForwardedMessagePacket("sent", "urn:xmpp:carbons:2") : f; + f = original.getForwardedMessagePacket("received", Namespace.CARBONS); + f = f == null ? original.getForwardedMessagePacket("sent", Namespace.CARBONS) : f; packet = f != null ? f.first : original; if (handleErrorMessage(account, packet)) { return; diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index e28e69add..a4fd8c063 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -25,9 +25,10 @@ public final class Namespace { public static final String NICK = "http://jabber.org/protocol/nick"; public static final String FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL = "http://jabber.org/protocol/offline"; public static final String BIND = "urn:ietf:params:xml:ns:xmpp-bind"; - public static final String BIND2 = "urn:xmpp:bind2:0"; + public static final String BIND2 = "urn:xmpp:bind2:1"; public static final String STREAM_MANAGEMENT = "urn:xmpp:sm:3"; public static final String CSI = "urn:xmpp:csi:0"; + public static final String CARBONS = "urn:xmpp:carbons:2"; public static final String BOOKMARKS_CONVERSION = "urn:xmpp:bookmarks-conversion:0"; public static final String BOOKMARKS = "storage:bookmarks"; public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 6efbfbf15..525151d62 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -13,6 +13,7 @@ import androidx.annotation.NonNull; import com.google.common.base.Strings; +import com.google.common.collect.Collections2; import org.xmlpull.v1.XmlPullParserException; @@ -32,6 +33,7 @@ import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -274,7 +276,7 @@ protected void connect() { this.attempt++; this.verifiedHostname = null; // will be set if user entered hostname is being used or hostname was verified - // with dnssec + // with dnssec try { Socket localSocket; shouldAuthenticate = !account.isOptionSet(Account.OPTION_REGISTER); @@ -409,7 +411,7 @@ protected void connect() { if (startXmpp(localSocket)) { localSocket.setSoTimeout( 0); // reset to 0; once the connection is established we don’t - // want this + // want this if (!hardcoded && !result.equals(storedBackupResult)) { mXmppConnectionService.databaseBackend.saveResolverResult( domain, result); @@ -615,24 +617,9 @@ private void processStream() throws XmlPullParserException, IOException { throw new StateChangingException(Account.State.UNAUTHORIZED); } tagWriter.writeElement(response); - } else if (nextTag.isStart("enabled")) { + } else if (nextTag.isStart("enabled", Namespace.STREAM_MANAGEMENT)) { final Element enabled = tagReader.readElement(nextTag); - if (enabled.getAttributeAsBoolean("resume")) { - this.streamId = enabled.getAttribute("id"); - Log.d( - Config.LOGTAG, - account.getJid().asBareJid().toString() - + ": stream management enabled (resumable)"); - } else { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid().toString() - + ": stream management enabled"); - } - this.stanzasReceived = 0; - this.inSmacksSession = true; - final RequestPacket r = new RequestPacket(); - tagWriter.writeStanzaAsync(r); + processEnabled(enabled); } else if (nextTag.isStart("resumed")) { final Element resumed = tagReader.readElement(nextTag); processResumed(resumed); @@ -771,13 +758,31 @@ private boolean processSuccess(final Element success) + ": jid changed during SASL 2.0. updating database"); mXmppConnectionService.databaseBackend.updateAccount(account); } + final Element bound = success.findChild("bound", Namespace.BIND2); final Element resumed = success.findChild("resumed", "urn:xmpp:sm:3"); final Element failed = success.findChild("failed", "urn:xmpp:sm:3"); + // TODO check if resumed and bound exist and throw bind failure if (resumed != null && streamId != null) { processResumed(resumed); } else if (failed != null) { processFailed(failed, false); // wait for new stream features } + if (bound != null) { + this.isBound = true; + final Element streamManagementEnabled = + bound.findChild("enabled", Namespace.STREAM_MANAGEMENT); + final Element carbonsEnabled = bound.findChild("enabled", Namespace.CARBONS); + if (streamManagementEnabled != null) { + processEnabled(streamManagementEnabled); + } + if (carbonsEnabled != null) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": successfully enabled carbons"); + features.carbonsEnabled = true; + } + sendPostBindInitialization(streamManagementEnabled != null, carbonsEnabled != null); + } } if (version == SaslMechanism.Version.SASL) { tagReader.reset(); @@ -794,6 +799,27 @@ private boolean processSuccess(final Element success) } } + private void processEnabled(final Element enabled) { + final String streamId; + if (enabled.getAttributeAsBoolean("resume")) { + streamId = enabled.getAttribute("id"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid().toString() + + ": stream management enabled (resumable)"); + } else { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid().toString() + ": stream management enabled"); + streamId = null; + } + this.streamId = streamId; + this.stanzasReceived = 0; + this.inSmacksSession = true; + final RequestPacket r = new RequestPacket(); + // tagWriter.writeStanzaAsync(r); + } + private void processResumed(final Element resumed) throws StateChangingException { this.inSmacksSession = true; this.isBound = true; @@ -1241,6 +1267,16 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio final boolean inlineStreamManagement = inline != null && inline.hasChild("sm", "urn:xmpp:sm:3"); final boolean inlineBind2 = inline != null && inline.hasChild("bind", Namespace.BIND2); + final Element inlineBindFeatures = + this.streamFeatures.findChild("inline", Namespace.BIND2); + if (inlineBind2 && inlineBindFeatures != null) { + final Element bind = + generateBindRequest( + Collections2.transform( + inlineBindFeatures.getChildren(), + c -> c == null ? null : c.getAttribute("var"))); + authenticate.addChild(bind); + } if (inlineStreamManagement && streamId != null) { final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived); this.mSmCatchupMessageCounter.set(0); @@ -1259,9 +1295,26 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio + "/" + saslMechanism.getMechanism()); authenticate.setAttribute("mechanism", saslMechanism.getMechanism()); + Log.d(Config.LOGTAG, "authenticate " + authenticate); tagWriter.writeElement(authenticate); } + private Element generateBindRequest(final Collection bindFeatures) { + Log.d(Config.LOGTAG, "inline bind features: " + bindFeatures); + final Element bind = new Element("bind", Namespace.BIND2); + final Element clientId = bind.addChild("client-id"); + clientId.setAttribute("tag", mXmppConnectionService.getString(R.string.app_name)); + clientId.setContent(account.getUuid()); + final Element features = bind.addChild("features"); + if (bindFeatures.contains(Namespace.CARBONS)) { + features.addChild("enable", Namespace.CARBONS); + } + if (bindFeatures.contains(Namespace.STREAM_MANAGEMENT)) { + features.addChild("enable", Namespace.STREAM_MANAGEMENT); + } + return bind; + } + private static List extractMechanisms(final Element stream) { final ArrayList mechanisms = new ArrayList<>(stream.getChildren().size()); for (final Element child : stream.getChildren()) { @@ -1469,7 +1522,8 @@ private void sendBindRequest() { .hasChild("optional")) { sendStartSession(); } else { - sendPostBindInitialization(); + final boolean waitForDisco = enableStreamManagement(); + sendPostBindInitialization(waitForDisco, false); } return; } catch (final IllegalArgumentException e) { @@ -1565,7 +1619,8 @@ private void sendStartSession() { startSession, (account, packet) -> { if (packet.getType() == IqPacket.TYPE.RESULT) { - sendPostBindInitialization(); + final boolean waitForDisco = enableStreamManagement(); + sendPostBindInitialization(waitForDisco, false); } else if (packet.getType() != IqPacket.TYPE.TIMEOUT) { throw new StateChangingError(Account.State.SESSION_FAILURE); } @@ -1573,7 +1628,7 @@ private void sendStartSession() { true); } - private void sendPostBindInitialization() { + private boolean enableStreamManagement() { final boolean streamManagement = this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT); if (streamManagement) { @@ -1583,15 +1638,22 @@ private void sendPostBindInitialization() { stanzasSent = 0; mStanzaQueue.clear(); } + return true; + } else { + return false; } - features.carbonsEnabled = false; + } + + private void sendPostBindInitialization( + final boolean waitForDisco, final boolean carbonsEnabled) { + features.carbonsEnabled = carbonsEnabled; features.blockListRequested = false; synchronized (this.disco) { this.disco.clear(); } Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": starting service discovery"); mPendingServiceDiscoveries.set(0); - if (!streamManagement + if (!waitForDisco || Patches.DISCO_EXCEPTIONS.contains( account.getJid().getDomain().toEscapedString())) { Log.d( @@ -1819,11 +1881,11 @@ private void sendServiceDiscoveryItems(final Jid server) { private void sendEnableCarbons() { final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); - iq.addChild("enable", "urn:xmpp:carbons:2"); + iq.addChild("enable", Namespace.CARBONS); this.sendIqPacket( iq, (account, packet) -> { - if (!packet.hasChild("error")) { + if (packet.getType() == IqPacket.TYPE.RESULT) { Log.d( Config.LOGTAG, account.getJid().asBareJid() + ": successfully enabled carbons"); @@ -2309,7 +2371,7 @@ private boolean hasDiscoFeature(final Jid server, final String feature) { } public boolean carbons() { - return hasDiscoFeature(account.getDomain(), "urn:xmpp:carbons:2"); + return hasDiscoFeature(account.getDomain(), Namespace.CARBONS); } public boolean commands() { From e0bd1d168c6937201f4076db0db8b9a6b436c204 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 4 Sep 2022 09:28:00 +0200 Subject: [PATCH 180/394] do not attempt resume when already in smacks session --- .../siacs/conversations/utils/XmlHelper.java | 47 ++++++++++--------- .../conversations/xmpp/XmppConnection.java | 14 +++--- 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/utils/XmlHelper.java b/src/main/java/eu/siacs/conversations/utils/XmlHelper.java index 4964bd5ef..7287297e3 100644 --- a/src/main/java/eu/siacs/conversations/utils/XmlHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/XmlHelper.java @@ -1,30 +1,31 @@ package eu.siacs.conversations.utils; +import com.google.common.base.Joiner; +import com.google.common.collect.Lists; + +import java.util.Collections; +import java.util.List; + import eu.siacs.conversations.xml.Element; public class XmlHelper { - public static String encodeEntities(String content) { - content = content.replace("&", "&"); - content = content.replace("<", "<"); - content = content.replace(">", ">"); - content = content.replace("\"", """); - content = content.replace("'", "'"); - content = content.replaceAll("[\\p{Cntrl}&&[^\n\t\r]]", ""); - return content; - } + public static String encodeEntities(String content) { + content = content.replace("&", "&"); + content = content.replace("<", "<"); + content = content.replace(">", ">"); + content = content.replace("\"", """); + content = content.replace("'", "'"); + content = content.replaceAll("[\\p{Cntrl}&&[^\n\t\r]]", ""); + return content; + } - public static String printElementNames(final Element element) { - final StringBuilder builder = new StringBuilder(); - builder.append('['); - if (element != null) { - for (Element child : element.getChildren()) { - if (builder.length() != 1) { - builder.append(','); - } - builder.append(child.getName()); - } - } - builder.append(']'); - return builder.toString(); - } + public static String printElementNames(final Element element) { + final List features = + element == null + ? Collections.emptyList() + : Lists.transform( + element.getChildren(), + child -> child != null ? child.getName() : null); + return Joiner.on(", ").join(features); + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 525151d62..64ffa4e12 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -817,7 +817,7 @@ private void processEnabled(final Element enabled) { this.stanzasReceived = 0; this.inSmacksSession = true; final RequestPacket r = new RequestPacket(); - // tagWriter.writeStanzaAsync(r); + tagWriter.writeStanzaAsync(r); } private void processResumed(final Element resumed) throws StateChangingException { @@ -1180,7 +1180,8 @@ private void processStreamFeatures(final Tag currentTag) throws IOException { && isSecure) { authenticate(SaslMechanism.Version.SASL); } else if (this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT) - && streamId != null) { + && streamId != null + && !inSmacksSession) { if (Config.EXTENDED_SM_LOGGING) { Log.d( Config.LOGTAG, @@ -1208,7 +1209,7 @@ private void processStreamFeatures(final Tag currentTag) throws IOException { Config.LOGTAG, account.getJid().asBareJid() + ": received NOP stream features " - + this.streamFeatures); + + XmlHelper.printElementNames(this.streamFeatures)); } } @@ -1295,7 +1296,6 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio + "/" + saslMechanism.getMechanism()); authenticate.setAttribute("mechanism", saslMechanism.getMechanism()); - Log.d(Config.LOGTAG, "authenticate " + authenticate); tagWriter.writeElement(authenticate); } @@ -1310,7 +1310,7 @@ private Element generateBindRequest(final Collection bindFeatures) { features.addChild("enable", Namespace.CARBONS); } if (bindFeatures.contains(Namespace.STREAM_MANAGEMENT)) { - features.addChild("enable", Namespace.STREAM_MANAGEMENT); + features.addChild(new EnablePacket()); } return bind; } @@ -2365,8 +2365,8 @@ public Features(final XmppConnection connection) { private boolean hasDiscoFeature(final Jid server, final String feature) { synchronized (XmppConnection.this.disco) { - return connection.disco.containsKey(server) - && connection.disco.get(server).getFeatures().contains(feature); + final ServiceDiscoveryResult sdr = connection.disco.get(server); + return sdr != null && sdr.getFeatures().contains(feature); } } From eee14a822a7f4657ccf4b1c586473ad49667eebe Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 5 Sep 2022 11:07:25 +0200 Subject: [PATCH 181/394] add todos --- src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 64ffa4e12..70bc347b3 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -718,6 +718,7 @@ private boolean processSuccess(final Element success) Log.d( Config.LOGTAG, account.getJid().asBareJid().toString() + ": logged in (using " + version + ")"); + //TODO store mechanism name account.setKey(Account.PINNED_MECHANISM_KEY, String.valueOf(saslMechanism.getPriority())); if (version == SaslMechanism.Version.SASL_2) { final String authorizationIdentifier = @@ -781,6 +782,7 @@ private boolean processSuccess(final Element success) account.getJid().asBareJid() + ": successfully enabled carbons"); features.carbonsEnabled = true; } + //TODO if both are set mark account ready for pipelining sendPostBindInitialization(streamManagementEnabled != null, carbonsEnabled != null); } } From 22f41292620e83c0fbc61621124df96a945a2dd6 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 5 Sep 2022 12:17:13 +0200 Subject: [PATCH 182/394] increase quoting depth to 2 --- src/main/java/eu/siacs/conversations/Config.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index c89ce81bb..377be3ba1 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -205,5 +205,5 @@ public static final class Map { // How deep nested quotes should be displayed. '2' means one quote nested in another. public static final int QUOTE_MAX_DEPTH = 7; // How deep nested quotes should be created on quoting a message. - public static final int QUOTING_MAX_DEPTH = 1; + public static final int QUOTING_MAX_DEPTH = 2; } From 562ffd200332b0537dbc8108d5a18bb34b7eb7bc Mon Sep 17 00:00:00 2001 From: Millesimus <32270710+Millesimus@users.noreply.github.com> Date: Mon, 5 Sep 2022 12:17:57 +0200 Subject: [PATCH 183/394] preserve new lines when quoting. fixes #3876 --- .../java/eu/siacs/conversations/ui/util/QuoteHelper.java | 8 ++++---- .../eu/siacs/conversations/ui/widget/EditMessage.java | 8 +++++++- .../java/eu/siacs/conversations/utils/MessageUtils.java | 6 +----- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java b/src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java index cf49be767..c2a69e607 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java +++ b/src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java @@ -84,13 +84,13 @@ public static boolean isNestedTooDeeply(CharSequence line) { if (isPositionQuoteStart(line, 0)) { int nestingDepth = 1; for (int i = 1; i < line.length(); i++) { - if (isPositionQuoteStart(line, i)) { + if (isPositionQuoteCharacter(line, i)) { nestingDepth++; - } - if (nestingDepth > (Config.QUOTING_MAX_DEPTH - 1)) { - return true; + } else if (line.charAt(i) != ' ') { + break; } } + return nestingDepth >= (Config.QUOTING_MAX_DEPTH); } return false; } diff --git a/src/main/java/eu/siacs/conversations/ui/widget/EditMessage.java b/src/main/java/eu/siacs/conversations/ui/widget/EditMessage.java index e890e5984..455c3ba44 100644 --- a/src/main/java/eu/siacs/conversations/ui/widget/EditMessage.java +++ b/src/main/java/eu/siacs/conversations/ui/widget/EditMessage.java @@ -145,7 +145,13 @@ public void setRichContentListener(String[] mimeTypes, OnCommitContentListener l public void insertAsQuote(String text) { text = QuoteHelper.replaceAltQuoteCharsInText(text); - text = text.replaceAll("(\n *){2,}", "\n").replaceAll("(^|\n)(" + QuoteHelper.QUOTE_CHAR + ")", "$1$2$2").replaceAll("(^|\n)([^" + QuoteHelper.QUOTE_CHAR + "])", "$1> $2").replaceAll("\n$", ""); + text = text + // first replace all '>' at the beginning of the line with nice and tidy '>>' + // for nested quoting + .replaceAll("(^|\n)(" + QuoteHelper.QUOTE_CHAR + ")", "$1$2$2") + // then find all other lines and have them start with a '> ' + .replaceAll("(^|\n)(?!" + QuoteHelper.QUOTE_CHAR + ")(.*)", "$1> $2") + ; Editable editable = getEditableText(); int position = getSelectionEnd(); if (position == -1) position = editable.length(); diff --git a/src/main/java/eu/siacs/conversations/utils/MessageUtils.java b/src/main/java/eu/siacs/conversations/utils/MessageUtils.java index 0a11cd720..9687a7b14 100644 --- a/src/main/java/eu/siacs/conversations/utils/MessageUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/MessageUtils.java @@ -66,11 +66,7 @@ public static String prepareQuote(Message message) { body = message.getMergedBody().toString(); } for (String line : body.split("\n")) { - if (line.length() <= 0) { - continue; - } - final char c = line.charAt(0); - if (QuoteHelper.isNestedTooDeeply(line)) { + if (!(line.length() <= 0) && QuoteHelper.isNestedTooDeeply(line)) { continue; } if (builder.length() != 0) { From 511dfa13c49347d3c5640c5cacebb68057361a2f Mon Sep 17 00:00:00 2001 From: Licaon_Kter Date: Mon, 5 Sep 2022 13:45:49 +0000 Subject: [PATCH 184/394] Fastlane description, remove fee (#4372) --- fastlane/metadata/android/en-US/full_description.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index e3b806b02..e30ca25c1 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -20,7 +20,7 @@ Features: * Multiple accounts / unified inbox * Very low impact on battery life -Conversations makes it very easy to create an account on the conversations.im server. Using that server comes with an annual fee of 8 Euro after a 6 month trial period. However Conversations will work with any other XMPP server as well. A lot of XMPP servers are run by volunteers and are free of charge. +Conversations makes it very easy to create an account on the free conversations.im server. However Conversations will work with any other XMPP server as well. A lot of XMPP servers are run by volunteers and are free of charge. XMPP Features: From a210568a9ce00cbafc36563b0aa70bf10ef90047 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 6 Sep 2022 09:25:09 +0200 Subject: [PATCH 185/394] refactor SASL choice into factory; remove unused TagWriter --- .../crypto/axolotl/AxolotlService.java | 4 +- .../conversations/crypto/sasl/Anonymous.java | 7 +- .../conversations/crypto/sasl/DigestMd5.java | 72 +++++--- .../conversations/crypto/sasl/External.java | 10 +- .../conversations/crypto/sasl/Plain.java | 15 +- .../crypto/sasl/SaslMechanism.java | 102 +++++++---- .../crypto/sasl/ScramMechanism.java | 168 ++++++++++-------- .../conversations/crypto/sasl/ScramSha1.java | 11 +- .../crypto/sasl/ScramSha256.java | 11 +- .../crypto/sasl/ScramSha512.java | 11 +- .../conversations/crypto/sasl/Tokenizer.java | 17 +- .../http/HttpConnectionManager.java | 4 +- .../http/HttpUploadConnection.java | 4 +- .../services/MessageArchiveService.java | 4 +- .../services/XmppConnectionService.java | 11 +- .../ui/CreatePublicChannelDialog.java | 3 +- .../conversations/utils/CryptoHelper.java | 14 +- .../eu/siacs/conversations/utils/Random.java | 13 ++ .../conversations/xmpp/XmppConnection.java | 38 ++-- 19 files changed, 288 insertions(+), 231 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/utils/Random.java diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index faef2e098..3d4f23360 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.crypto.axolotl; +import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; + import android.os.Bundle; import android.security.KeyChain; import android.util.Log; @@ -499,7 +501,7 @@ public void publishDeviceVerificationAndBundle(final SignedPreKeyRecord signedPr PrivateKey x509PrivateKey = KeyChain.getPrivateKey(mXmppConnectionService, account.getPrivateKeyAlias()); X509Certificate[] chain = KeyChain.getCertificateChain(mXmppConnectionService, account.getPrivateKeyAlias()); Signature verifier = Signature.getInstance("sha256WithRSA"); - verifier.initSign(x509PrivateKey, mXmppConnectionService.getRNG()); + verifier.initSign(x509PrivateKey, SECURE_RANDOM); verifier.update(axolotlPublicKey.serialize()); byte[] signature = verifier.sign(); IqPacket packet = mXmppConnectionService.getIqGenerator().publishVerification(signature, chain, getOwnDeviceId()); diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/Anonymous.java b/src/main/java/eu/siacs/conversations/crypto/sasl/Anonymous.java index a9abb2bf8..22cf80e65 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/Anonymous.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/Anonymous.java @@ -1,16 +1,13 @@ package eu.siacs.conversations.crypto.sasl; -import java.security.SecureRandom; - import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.xml.TagWriter; public class Anonymous extends SaslMechanism { public static final String MECHANISM = "ANONYMOUS"; - public Anonymous(TagWriter tagWriter, Account account, SecureRandom rng) { - super(tagWriter, account, rng); + public Anonymous(final Account account) { + super(account); } @Override diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java b/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java index 74d4463d5..7229299ef 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java @@ -5,18 +5,17 @@ import java.nio.charset.Charset; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.xml.TagWriter; public class DigestMd5 extends SaslMechanism { public static final String MECHANISM = "DIGEST-MD5"; + private State state = State.INITIAL; - public DigestMd5(final TagWriter tagWriter, final Account account, final SecureRandom rng) { - super(tagWriter, account, rng); + public DigestMd5(final Account account) { + super(account); } @Override @@ -29,8 +28,6 @@ public String getMechanism() { return MECHANISM; } - private State state = State.INITIAL; - @Override public String getResponse(final String challenge) throws AuthenticationException { switch (state) { @@ -38,7 +35,8 @@ public String getResponse(final String challenge) throws AuthenticationException state = State.RESPONSE_SENT; final String encodedResponse; try { - final Tokenizer tokenizer = new Tokenizer(Base64.decode(challenge, Base64.DEFAULT)); + final Tokenizer tokenizer = + new Tokenizer(Base64.decode(challenge, Base64.DEFAULT)); String nonce = ""; for (final String token : tokenizer) { final String[] parts = token.split("=", 2); @@ -50,29 +48,49 @@ public String getResponse(final String challenge) throws AuthenticationException } final String digestUri = "xmpp/" + account.getServer(); final String nonceCount = "00000001"; - final String x = account.getUsername() + ":" + account.getServer() + ":" - + account.getPassword(); + final String x = + account.getUsername() + + ":" + + account.getServer() + + ":" + + account.getPassword(); final MessageDigest md = MessageDigest.getInstance("MD5"); final byte[] y = md.digest(x.getBytes(Charset.defaultCharset())); - final String cNonce = CryptoHelper.random(100, rng); - final byte[] a1 = CryptoHelper.concatenateByteArrays(y, - (":" + nonce + ":" + cNonce).getBytes(Charset.defaultCharset())); + final String cNonce = CryptoHelper.random(100); + final byte[] a1 = + CryptoHelper.concatenateByteArrays( + y, + (":" + nonce + ":" + cNonce) + .getBytes(Charset.defaultCharset())); final String a2 = "AUTHENTICATE:" + digestUri; final String ha1 = CryptoHelper.bytesToHex(md.digest(a1)); - final String ha2 = CryptoHelper.bytesToHex(md.digest(a2.getBytes(Charset - .defaultCharset()))); - final String kd = ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce - + ":auth:" + ha2; - final String response = CryptoHelper.bytesToHex(md.digest(kd.getBytes(Charset - .defaultCharset()))); - final String saslString = "username=\"" + account.getUsername() - + "\",realm=\"" + account.getServer() + "\",nonce=\"" - + nonce + "\",cnonce=\"" + cNonce + "\",nc=" + nonceCount - + ",qop=auth,digest-uri=\"" + digestUri + "\",response=" - + response + ",charset=utf-8"; - encodedResponse = Base64.encodeToString( - saslString.getBytes(Charset.defaultCharset()), - Base64.NO_WRAP); + final String ha2 = + CryptoHelper.bytesToHex( + md.digest(a2.getBytes(Charset.defaultCharset()))); + final String kd = + ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce + ":auth:" + ha2; + final String response = + CryptoHelper.bytesToHex( + md.digest(kd.getBytes(Charset.defaultCharset()))); + final String saslString = + "username=\"" + + account.getUsername() + + "\",realm=\"" + + account.getServer() + + "\",nonce=\"" + + nonce + + "\",cnonce=\"" + + cNonce + + "\",nc=" + + nonceCount + + ",qop=auth,digest-uri=\"" + + digestUri + + "\",response=" + + response + + ",charset=utf-8"; + encodedResponse = + Base64.encodeToString( + saslString.getBytes(Charset.defaultCharset()), Base64.NO_WRAP); } catch (final NoSuchAlgorithmException e) { throw new AuthenticationException(e); } @@ -83,7 +101,7 @@ public String getResponse(final String challenge) throws AuthenticationException break; case VALID_SERVER_RESPONSE: if (challenge == null) { - return null; //everything is fine + return null; // everything is fine } default: throw new InvalidStateException(state); diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/External.java b/src/main/java/eu/siacs/conversations/crypto/sasl/External.java index 6e0ed4390..06323f039 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/External.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/External.java @@ -2,17 +2,14 @@ import android.util.Base64; -import java.security.SecureRandom; - import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.xml.TagWriter; public class External extends SaslMechanism { public static final String MECHANISM = "EXTERNAL"; - public External(TagWriter tagWriter, Account account, SecureRandom rng) { - super(tagWriter, account, rng); + public External(final Account account) { + super(account); } @Override @@ -27,6 +24,7 @@ public String getMechanism() { @Override public String getClientFirstMessage() { - return Base64.encodeToString(account.getJid().asBareJid().toEscapedString().getBytes(), Base64.NO_WRAP); + return Base64.encodeToString( + account.getJid().asBareJid().toEscapedString().getBytes(), Base64.NO_WRAP); } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java b/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java index d5cc037e1..875538bec 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java @@ -5,14 +5,18 @@ import java.nio.charset.Charset; import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.xml.TagWriter; public class Plain extends SaslMechanism { public static final String MECHANISM = "PLAIN"; - public Plain(final TagWriter tagWriter, final Account account) { - super(tagWriter, account, null); + public Plain(final Account account) { + super(account); + } + + public static String getMessage(String username, String password) { + final String message = '\u0000' + username + '\u0000' + password; + return Base64.encodeToString(message.getBytes(Charset.defaultCharset()), Base64.NO_WRAP); } @Override @@ -29,9 +33,4 @@ public String getMechanism() { public String getClientFirstMessage() { return getMessage(account.getUsername(), account.getPassword()); } - - public static String getMessage(String username, String password) { - final String message = '\u0000' + username + '\u0000' + password; - return Base64.encodeToString(message.getBytes(Charset.defaultCharset()), Base64.NO_WRAP); - } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java index b255b6f42..ce2d5cd6a 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java @@ -2,18 +2,38 @@ import com.google.common.base.Strings; -import java.security.SecureRandom; +import java.util.Collection; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xml.TagWriter; public abstract class SaslMechanism { - final protected TagWriter tagWriter; - final protected Account account; - final protected SecureRandom rng; + protected final Account account; + + protected SaslMechanism(final Account account) { + this.account = account; + } + + /** + * The priority is used to pin the authentication mechanism. If authentication fails, it MAY be + * retried with another mechanism of the same priority, but MUST NOT be tried with a mechanism + * of lower priority (to prevent downgrade attacks). + * + * @return An arbitrary int representing the priority + */ + public abstract int getPriority(); + + public abstract String getMechanism(); + + public String getClientFirstMessage() { + return ""; + } + + public String getResponse(final String challenge) throws AuthenticationException { + return ""; + } protected enum State { INITIAL, @@ -22,6 +42,22 @@ protected enum State { VALID_SERVER_RESPONSE, } + public enum Version { + SASL, + SASL_2; + + public static Version of(final Element element) { + switch (Strings.nullToEmpty(element.getNamespace())) { + case Namespace.SASL: + return SASL; + case Namespace.SASL_2: + return SASL_2; + default: + throw new IllegalArgumentException("Unrecognized SASL namespace"); + } + } + } + public static class AuthenticationException extends Exception { public AuthenticationException(final String message) { super(message); @@ -46,42 +82,32 @@ public InvalidStateException(final State state) { } } - public SaslMechanism(final TagWriter tagWriter, final Account account, final SecureRandom rng) { - this.tagWriter = tagWriter; - this.account = account; - this.rng = rng; - } + public static final class Factory { - /** - * The priority is used to pin the authentication mechanism. If authentication fails, it MAY be retried with another - * mechanism of the same priority, but MUST NOT be tried with a mechanism of lower priority (to prevent downgrade - * attacks). - * - * @return An arbitrary int representing the priority - */ - public abstract int getPriority(); - - public abstract String getMechanism(); + private final Account account; - public String getClientFirstMessage() { - return ""; - } - - public String getResponse(final String challenge) throws AuthenticationException { - return ""; - } - - public enum Version { - SASL, SASL_2; + public Factory(final Account account) { + this.account = account; + } - public static Version of(final Element element) { - switch ( Strings.nullToEmpty(element.getNamespace())) { - case Namespace.SASL: - return SASL; - case Namespace.SASL_2: - return SASL_2; - default: - throw new IllegalArgumentException("Unrecognized SASL namespace"); + public SaslMechanism of(final Collection mechanisms) { + if (mechanisms.contains(External.MECHANISM) && account.getPrivateKeyAlias() != null) { + return new External(account); + } else if (mechanisms.contains(ScramSha512.MECHANISM)) { + return new ScramSha512(account); + } else if (mechanisms.contains(ScramSha256.MECHANISM)) { + return new ScramSha256(account); + } else if (mechanisms.contains(ScramSha1.MECHANISM)) { + return new ScramSha1(account); + } else if (mechanisms.contains(Plain.MECHANISM) + && !account.getServer().equals("nimbuzz.com")) { + return new Plain(account); + } else if (mechanisms.contains(DigestMd5.MECHANISM)) { + return new DigestMd5(account); + } else if (mechanisms.contains(Anonymous.MECHANISM)) { + return new Anonymous(account); + } else { + return null; } } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java index 807056bf8..0fe7434a8 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java @@ -12,78 +12,53 @@ import java.nio.charset.Charset; import java.security.InvalidKeyException; -import java.security.SecureRandom; import java.util.concurrent.ExecutionException; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.xml.TagWriter; abstract class ScramMechanism extends SaslMechanism { - // TODO: When channel binding (SCRAM-SHA1-PLUS) is supported in future, generalize this to indicate support and/or usage. - private final static String GS2_HEADER = "n,,"; + // TODO: When channel binding (SCRAM-SHA1-PLUS) is supported in future, generalize this to + // indicate support and/or usage. + private static final String GS2_HEADER = "n,,"; private static final byte[] CLIENT_KEY_BYTES = "Client Key".getBytes(); private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes(); - - protected abstract HMac getHMAC(); - - protected abstract Digest getDigest(); - - private static final Cache CACHE = CacheBuilder.newBuilder().maximumSize(10).build(); - - private static class CacheKey { - final String algorithm; - final String password; - final String salt; - final int iterations; - - private CacheKey(String algorithm, String password, String salt, int iterations) { - this.algorithm = algorithm; - this.password = password; - this.salt = salt; - this.iterations = iterations; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - CacheKey cacheKey = (CacheKey) o; - return iterations == cacheKey.iterations && - Objects.equal(algorithm, cacheKey.algorithm) && - Objects.equal(password, cacheKey.password) && - Objects.equal(salt, cacheKey.salt); - } - - @Override - public int hashCode() { - return Objects.hashCode(algorithm, password, salt, iterations); - } - } - - private KeyPair getKeyPair(final String password, final String salt, final int iterations) throws ExecutionException { - return CACHE.get(new CacheKey(getHMAC().getAlgorithmName(), password, salt, iterations), () -> { - final byte[] saltedPassword, serverKey, clientKey; - saltedPassword = hi(password.getBytes(), Base64.decode(salt, Base64.DEFAULT), iterations); - serverKey = hmac(saltedPassword, SERVER_KEY_BYTES); - clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES); - return new KeyPair(clientKey, serverKey); - }); - } - + private static final Cache CACHE = + CacheBuilder.newBuilder().maximumSize(10).build(); private final String clientNonce; protected State state = State.INITIAL; private String clientFirstMessageBare; private byte[] serverSignature = null; - ScramMechanism(final TagWriter tagWriter, final Account account, final SecureRandom rng) { - super(tagWriter, account, rng); + ScramMechanism(final Account account) { + super(account); // This nonce should be different for each authentication attempt. - clientNonce = CryptoHelper.random(100, rng); + this.clientNonce = CryptoHelper.random(100); clientFirstMessageBare = ""; } + protected abstract HMac getHMAC(); + + protected abstract Digest getDigest(); + + private KeyPair getKeyPair(final String password, final String salt, final int iterations) + throws ExecutionException { + return CACHE.get( + new CacheKey(getHMAC().getAlgorithmName(), password, salt, iterations), + () -> { + final byte[] saltedPassword, serverKey, clientKey; + saltedPassword = + hi( + password.getBytes(), + Base64.decode(salt, Base64.DEFAULT), + iterations); + serverKey = hmac(saltedPassword, SERVER_KEY_BYTES); + clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES); + return new KeyPair(clientKey, serverKey); + }); + } + private byte[] hmac(final byte[] key, final byte[] input) throws InvalidKeyException { final HMac hMac = getHMAC(); hMac.init(new KeyParameter(key)); @@ -123,8 +98,11 @@ private byte[] hi(final byte[] key, final byte[] salt, final int iterations) @Override public String getClientFirstMessage() { if (clientFirstMessageBare.isEmpty() && state == State.INITIAL) { - clientFirstMessageBare = "n=" + CryptoHelper.saslEscape(CryptoHelper.saslPrep(account.getUsername())) + - ",r=" + this.clientNonce; + clientFirstMessageBare = + "n=" + + CryptoHelper.saslEscape(CryptoHelper.saslPrep(account.getUsername())) + + ",r=" + + this.clientNonce; state = State.AUTH_TEXT_SENT; } return Base64.encodeToString( @@ -173,7 +151,8 @@ public String getResponse(final String challenge) throws AuthenticationException * MUST cause authentication failure when the attribute is parsed by * the other end. */ - throw new AuthenticationException("Server sent reserved token: `m'"); + throw new AuthenticationException( + "Server sent reserved token: `m'"); } } } @@ -182,20 +161,33 @@ public String getResponse(final String challenge) throws AuthenticationException throw new AuthenticationException("Server did not send iteration count"); } if (nonce.isEmpty() || !nonce.startsWith(clientNonce)) { - throw new AuthenticationException("Server nonce does not contain client nonce: " + nonce); + throw new AuthenticationException( + "Server nonce does not contain client nonce: " + nonce); } if (salt.isEmpty()) { throw new AuthenticationException("Server sent empty salt"); } - final String clientFinalMessageWithoutProof = "c=" + Base64.encodeToString( - GS2_HEADER.getBytes(), Base64.NO_WRAP) + ",r=" + nonce; - final byte[] authMessage = (clientFirstMessageBare + ',' + new String(serverFirstMessage) + ',' - + clientFinalMessageWithoutProof).getBytes(); + final String clientFinalMessageWithoutProof = + "c=" + + Base64.encodeToString(GS2_HEADER.getBytes(), Base64.NO_WRAP) + + ",r=" + + nonce; + final byte[] authMessage = + (clientFirstMessageBare + + ',' + + new String(serverFirstMessage) + + ',' + + clientFinalMessageWithoutProof) + .getBytes(); final KeyPair keys; try { - keys = getKeyPair(CryptoHelper.saslPrep(account.getPassword()), salt, iterationCount); + keys = + getKeyPair( + CryptoHelper.saslPrep(account.getPassword()), + salt, + iterationCount); } catch (ExecutionException e) { throw new AuthenticationException("Invalid keys generated"); } @@ -213,35 +205,69 @@ public String getResponse(final String challenge) throws AuthenticationException final byte[] clientProof = new byte[keys.clientKey.length]; if (clientSignature.length < keys.clientKey.length) { - throw new AuthenticationException("client signature was shorter than clientKey"); + throw new AuthenticationException( + "client signature was shorter than clientKey"); } for (int i = 0; i < clientProof.length; i++) { clientProof[i] = (byte) (keys.clientKey[i] ^ clientSignature[i]); } - - final String clientFinalMessage = clientFinalMessageWithoutProof + ",p=" + - Base64.encodeToString(clientProof, Base64.NO_WRAP); + final String clientFinalMessage = + clientFinalMessageWithoutProof + + ",p=" + + Base64.encodeToString(clientProof, Base64.NO_WRAP); state = State.RESPONSE_SENT; return Base64.encodeToString(clientFinalMessage.getBytes(), Base64.NO_WRAP); case RESPONSE_SENT: try { - final String clientCalculatedServerFinalMessage = "v=" + - Base64.encodeToString(serverSignature, Base64.NO_WRAP); - if (!clientCalculatedServerFinalMessage.equals(new String(Base64.decode(challenge, Base64.DEFAULT)))) { + final String clientCalculatedServerFinalMessage = + "v=" + Base64.encodeToString(serverSignature, Base64.NO_WRAP); + if (!clientCalculatedServerFinalMessage.equals( + new String(Base64.decode(challenge, Base64.DEFAULT)))) { throw new Exception(); } state = State.VALID_SERVER_RESPONSE; return ""; } catch (Exception e) { - throw new AuthenticationException("Server final message does not match calculated final message"); + throw new AuthenticationException( + "Server final message does not match calculated final message"); } default: throw new InvalidStateException(state); } } + private static class CacheKey { + final String algorithm; + final String password; + final String salt; + final int iterations; + + private CacheKey(String algorithm, String password, String salt, int iterations) { + this.algorithm = algorithm; + this.password = password; + this.salt = salt; + this.iterations = iterations; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CacheKey cacheKey = (CacheKey) o; + return iterations == cacheKey.iterations + && Objects.equal(algorithm, cacheKey.algorithm) + && Objects.equal(password, cacheKey.password) + && Objects.equal(salt, cacheKey.salt); + } + + @Override + public int hashCode() { + return Objects.hashCode(algorithm, password, salt, iterations); + } + } + private static class KeyPair { final byte[] clientKey; final byte[] serverKey; diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java index c58dd147c..472c4dea1 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java @@ -4,15 +4,16 @@ import org.bouncycastle.crypto.digests.SHA1Digest; import org.bouncycastle.crypto.macs.HMac; -import java.security.SecureRandom; - import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.xml.TagWriter; public class ScramSha1 extends ScramMechanism { public static final String MECHANISM = "SCRAM-SHA-1"; + public ScramSha1(final Account account) { + super(account); + } + @Override protected HMac getHMAC() { return new HMac(new SHA1Digest()); @@ -23,10 +24,6 @@ protected Digest getDigest() { return new SHA1Digest(); } - public ScramSha1(final TagWriter tagWriter, final Account account, final SecureRandom rng) { - super(tagWriter, account, rng); - } - @Override public int getPriority() { return 20; diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java index d5dc42b07..f3f6cab57 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java @@ -4,15 +4,16 @@ import org.bouncycastle.crypto.digests.SHA256Digest; import org.bouncycastle.crypto.macs.HMac; -import java.security.SecureRandom; - import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.xml.TagWriter; public class ScramSha256 extends ScramMechanism { public static final String MECHANISM = "SCRAM-SHA-256"; + public ScramSha256(final Account account) { + super(account); + } + @Override protected HMac getHMAC() { return new HMac(new SHA256Digest()); @@ -23,10 +24,6 @@ protected Digest getDigest() { return new SHA256Digest(); } - public ScramSha256(final TagWriter tagWriter, final Account account, final SecureRandom rng) { - super(tagWriter, account, rng); - } - @Override public int getPriority() { return 25; diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java index dbd30945c..9a2f1a82d 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java @@ -4,15 +4,16 @@ import org.bouncycastle.crypto.digests.SHA512Digest; import org.bouncycastle.crypto.macs.HMac; -import java.security.SecureRandom; - import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.xml.TagWriter; public class ScramSha512 extends ScramMechanism { public static final String MECHANISM = "SCRAM-SHA-512"; + public ScramSha512(final Account account) { + super(account); + } + @Override protected HMac getHMAC() { return new HMac(new SHA512Digest()); @@ -23,10 +24,6 @@ protected Digest getDigest() { return new SHA512Digest(); } - public ScramSha512(final TagWriter tagWriter, final Account account, final SecureRandom rng) { - super(tagWriter, account, rng); - } - @Override public int getPriority() { return 30; diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/Tokenizer.java b/src/main/java/eu/siacs/conversations/crypto/sasl/Tokenizer.java index f9ba24f09..3038fb060 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/Tokenizer.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/Tokenizer.java @@ -6,9 +6,7 @@ import java.util.List; import java.util.NoSuchElementException; -/** - * A tokenizer for GS2 header strings - */ +/** A tokenizer for GS2 header strings */ public final class Tokenizer implements Iterator, Iterable { private final List parts; private int index; @@ -50,18 +48,19 @@ public String next() { } /** - * Removes the last object returned by {@code next} from the collection. - * This method can only be called once between each call to {@code next}. + * Removes the last object returned by {@code next} from the collection. This method can only be + * called once between each call to {@code next}. * * @throws UnsupportedOperationException if removing is not supported by the collection being - * iterated. - * @throws IllegalStateException if {@code next} has not been called, or {@code remove} has - * already been called after the last call to {@code next}. + * iterated. + * @throws IllegalStateException if {@code next} has not been called, or {@code remove} has + * already been called after the last call to {@code next}. */ @Override public void remove() { if (index <= 0) { - throw new IllegalStateException("You can't delete an element before first next() method call"); + throw new IllegalStateException( + "You can't delete an element before first next() method call"); } parts.remove(--index); } diff --git a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java index 566ce1e6a..27f5c3fc7 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.http; +import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; + import android.os.Build; import android.util.Log; @@ -147,7 +149,7 @@ private void setupTrustManager(final OkHttpClient.Builder builder, final boolean trustManager = mXmppConnectionService.getMemorizingTrustManager().getNonInteractive(); } try { - final SSLSocketFactory sf = new TLSSocketFactory(new X509TrustManager[]{trustManager}, mXmppConnectionService.getRNG()); + final SSLSocketFactory sf = new TLSSocketFactory(new X509TrustManager[]{trustManager}, SECURE_RANDOM); builder.sslSocketFactory(sf, trustManager); builder.hostnameVerifier(new StrictHostnameVerifier()); } catch (final KeyManagementException | NoSuchAlgorithmException ignored) { diff --git a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java index 3e478dd0f..e2366dd48 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.http; +import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; + import android.util.Log; import androidx.annotation.NonNull; @@ -124,7 +126,7 @@ public void init(boolean delay) { || message.getEncryption() == Message.ENCRYPTION_AXOLOTL || message.getEncryption() == Message.ENCRYPTION_OTR) { this.key = new byte[44]; - mXmppConnectionService.getRNG().nextBytes(this.key); + SECURE_RANDOM.nextBytes(this.key); this.file.setKeyAndIv(this.key); } this.file.setExpectedSize(originalFileSize + (file.getKey() != null ? 16 : 0)); diff --git a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java index 79cad9520..e74af3773 100644 --- a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java +++ b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.services; +import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; + import android.util.Log; import org.jetbrains.annotations.NotNull; @@ -502,7 +504,7 @@ public class Query { this.start = start.getTimestamp(); } this.end = end; - this.queryId = new BigInteger(50, mXmppConnectionService.getRNG()).toString(32); + this.queryId = new BigInteger(50, SECURE_RANDOM).toString(32); this.version = version; } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index ba2c8514e..586b717ff 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1,6 +1,7 @@ package eu.siacs.conversations.services; import static eu.siacs.conversations.utils.Compatibility.s; +import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; import android.Manifest; import android.annotation.SuppressLint; @@ -379,7 +380,6 @@ public void onBind(final Account account) { } }; private final AtomicLong mLastExpiryRun = new AtomicLong(0); - private SecureRandom mRandom; private final LruCache, ServiceDiscoveryResult> discoCache = new LruCache<>(20); private final OnStatusChanged statusListener = new OnStatusChanged() { @@ -451,7 +451,7 @@ public void onStatusChanged(final Account account) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": went into offline state during low ping mode. reconnecting now"); reconnectAccount(account, true, false); } else { - int timeToReconnect = mRandom.nextInt(10) + 2; + final int timeToReconnect = SECURE_RANDOM.nextInt(10) + 2; scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode()); } } else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) { @@ -1143,7 +1143,6 @@ public void onCreate() { Log.e(Config.LOGTAG, "unable to initialize security provider", throwable); } Resolver.init(this); - this.mRandom = new SecureRandom(); updateMemorizingTrustmanager(); final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); final int cacheSize = maxMemory / 8; @@ -3269,7 +3268,7 @@ public boolean createAdhocConference(final Account account, } return false; } - final Jid jid = Jid.of(CryptoHelper.pronounceable(getRNG()), server, null); + final Jid jid = Jid.of(CryptoHelper.pronounceable(), server, null); final Conversation conversation = findOrCreateConversation(account, jid, true, false, true); joinMuc(conversation, new OnConferenceJoined() { @Override @@ -4366,10 +4365,6 @@ public void sendReadMarker(final Conversation conversation, String upToUuid) { } } - public SecureRandom getRNG() { - return this.mRandom; - } - public MemorizingTrustManager getMemorizingTrustManager() { return this.mMemorizingTrustManager; } diff --git a/src/main/java/eu/siacs/conversations/ui/CreatePublicChannelDialog.java b/src/main/java/eu/siacs/conversations/ui/CreatePublicChannelDialog.java index 3c23b06eb..8f5e2e6d2 100644 --- a/src/main/java/eu/siacs/conversations/ui/CreatePublicChannelDialog.java +++ b/src/main/java/eu/siacs/conversations/ui/CreatePublicChannelDialog.java @@ -43,7 +43,6 @@ public class CreatePublicChannelDialog extends DialogFragment implements OnBacke private boolean jidWasModified = false; private boolean nameEntered = false; private boolean skipTetxWatcher = false; - private static final SecureRandom RANDOM = new SecureRandom(); public static CreatePublicChannelDialog newInstance(List accounts) { CreatePublicChannelDialog dialog = new CreatePublicChannelDialog(); @@ -158,7 +157,7 @@ private static String getJidSuggestion(CreatePublicChannelDialogBinding binding) try { return Jid.of(localpart, domain, null).toEscapedString(); } catch (IllegalArgumentException e) { - return Jid.of(CryptoHelper.pronounceable(RANDOM), domain, null).toEscapedString(); + return Jid.of(CryptoHelper.pronounceable(), domain, null).toEscapedString(); } } } diff --git a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java index a92d48825..85ea2bb86 100644 --- a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.utils; +import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; + import android.os.Bundle; import android.util.Base64; import android.util.Pair; @@ -59,12 +61,12 @@ public static String createPassword(SecureRandom random) { return builder.toString(); } - public static String pronounceable(SecureRandom random) { - final int rand = random.nextInt(4); + public static String pronounceable() { + final int rand = SECURE_RANDOM.nextInt(4); char[] output = new char[rand * 2 + (5 - rand)]; - boolean vowel = random.nextBoolean(); + boolean vowel = SECURE_RANDOM.nextBoolean(); for (int i = 0; i < output.length; ++i) { - output[i] = vowel ? VOWELS[random.nextInt(VOWELS.length)] : CONSONANTS[random.nextInt(CONSONANTS.length)]; + output[i] = vowel ? VOWELS[SECURE_RANDOM.nextInt(VOWELS.length)] : CONSONANTS[SECURE_RANDOM.nextInt(CONSONANTS.length)]; vowel = !vowel; } return String.valueOf(output); @@ -117,9 +119,9 @@ public static String saslPrep(final String s) { return Normalizer.normalize(s, Normalizer.Form.NFKC); } - public static String random(int length, SecureRandom random) { + public static String random(final int length) { final byte[] bytes = new byte[length]; - random.nextBytes(bytes); + SECURE_RANDOM.nextBytes(bytes); return Base64.encodeToString(bytes, Base64.NO_PADDING | Base64.NO_WRAP | Base64.URL_SAFE); } diff --git a/src/main/java/eu/siacs/conversations/utils/Random.java b/src/main/java/eu/siacs/conversations/utils/Random.java new file mode 100644 index 000000000..792c1fce1 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/utils/Random.java @@ -0,0 +1,13 @@ +package eu.siacs.conversations.utils; + +import java.security.SecureRandom; + +public final class Random { + + public static final SecureRandom SECURE_RANDOM = new SecureRandom(); + + private Random() { + + } + +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 70bc347b3..a1336e22c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.xmpp; +import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; + import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -521,7 +523,7 @@ private SSLSocketFactory getSSLSocketFactory() ? trustManager.getInteractive(domain) : trustManager.getNonInteractive(domain) }, - mXmppConnectionService.getRNG()); + SECURE_RANDOM); return sc.getSocketFactory(); } @@ -1216,23 +1218,11 @@ private void processStreamFeatures(final Tag currentTag) throws IOException { } private void authenticate(final SaslMechanism.Version version) throws IOException { - final List mechanisms = extractMechanisms(streamFeatures.findChild("mechanisms")); - if (mechanisms.contains(External.MECHANISM) && account.getPrivateKeyAlias() != null) { - saslMechanism = new External(tagWriter, account, mXmppConnectionService.getRNG()); - } else if (mechanisms.contains(ScramSha512.MECHANISM)) { - saslMechanism = new ScramSha512(tagWriter, account, mXmppConnectionService.getRNG()); - } else if (mechanisms.contains(ScramSha256.MECHANISM)) { - saslMechanism = new ScramSha256(tagWriter, account, mXmppConnectionService.getRNG()); - } else if (mechanisms.contains(ScramSha1.MECHANISM)) { - saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG()); - } else if (mechanisms.contains(Plain.MECHANISM) - && !account.getJid().getDomain().toEscapedString().equals("nimbuzz.com")) { - saslMechanism = new Plain(tagWriter, account); - } else if (mechanisms.contains(DigestMd5.MECHANISM)) { - saslMechanism = new DigestMd5(tagWriter, account, mXmppConnectionService.getRNG()); - } else if (mechanisms.contains(Anonymous.MECHANISM)) { - saslMechanism = new Anonymous(tagWriter, account, mXmppConnectionService.getRNG()); - } + final Element element = streamFeatures.findChild("mechanisms"); + final Collection mechanisms = Collections2.transform(element.getChildren(), c -> c == null ? null : c.getContent()); + final SaslMechanism.Factory factory = new SaslMechanism.Factory(account); + this.saslMechanism = factory.of(mechanisms); + if (saslMechanism == null) { Log.d( Config.LOGTAG, @@ -1317,12 +1307,8 @@ private Element generateBindRequest(final Collection bindFeatures) { return bind; } - private static List extractMechanisms(final Element stream) { - final ArrayList mechanisms = new ArrayList<>(stream.getChildren().size()); - for (final Element child : stream.getChildren()) { - mechanisms.add(child.getContent()); - } - return mechanisms; + private static Collection extractMechanisms(final Element stream) { + return Collections2.transform(stream.getChildren(), c -> c == null ? null : c.getContent()); } private void register() { @@ -1963,8 +1949,8 @@ private String nextRandomId() { return nextRandomId(false); } - private String nextRandomId(boolean s) { - return CryptoHelper.random(s ? 3 : 9, mXmppConnectionService.getRNG()); + private String nextRandomId(final boolean s) { + return CryptoHelper.random(s ? 3 : 9); } public String sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback) { From b78acb6fcad3609b383f5d302a6e831f544a746d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 6 Sep 2022 14:53:12 +0200 Subject: [PATCH 186/394] extract channel binding types via XEP-0440 --- .../crypto/sasl/ChannelBinding.java | 27 +++++++++++++ .../crypto/sasl/SaslMechanism.java | 3 +- .../crypto/sasl/ScramMechanism.java | 5 ++- .../conversations/crypto/sasl/ScramSha1.java | 2 +- .../crypto/sasl/ScramSha256.java | 2 +- .../crypto/sasl/ScramSha512.java | 2 +- .../eu/siacs/conversations/xml/Namespace.java | 1 + .../conversations/xmpp/XmppConnection.java | 39 +++++++++++++------ 8 files changed, 63 insertions(+), 18 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java new file mode 100644 index 000000000..847c50e9d --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java @@ -0,0 +1,27 @@ +package eu.siacs.conversations.crypto.sasl; + +import android.util.Log; + +import com.google.common.base.CaseFormat; + +import eu.siacs.conversations.Config; + +public enum ChannelBinding { + NONE, + TLS_EXPORTER, + TLS_SERVER_END_POINT, + TLS_UNIQUE; + + public static ChannelBinding of(final String type) { + if (type == null) { + return null; + } + try { + return valueOf( + CaseFormat.LOWER_HYPHEN.converterTo(CaseFormat.UPPER_UNDERSCORE).convert(type)); + } catch (final IllegalArgumentException e) { + Log.d(Config.LOGTAG, type + " is not a known channel binding"); + return null; + } + } +} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java index ce2d5cd6a..13360a063 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java @@ -90,7 +90,8 @@ public Factory(final Account account) { this.account = account; } - public SaslMechanism of(final Collection mechanisms) { + public SaslMechanism of( + final Collection mechanisms, final Collection bindings) { if (mechanisms.contains(External.MECHANISM) && account.getPrivateKeyAlias() != null) { return new External(account); } else if (mechanisms.contains(ScramSha512.MECHANISM)) { diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java index 0fe7434a8..887128a0c 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java @@ -25,14 +25,15 @@ abstract class ScramMechanism extends SaslMechanism { private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes(); private static final Cache CACHE = CacheBuilder.newBuilder().maximumSize(10).build(); + protected final ChannelBinding channelBinding; private final String clientNonce; protected State state = State.INITIAL; private String clientFirstMessageBare; private byte[] serverSignature = null; - ScramMechanism(final Account account) { + ScramMechanism(final Account account, final ChannelBinding channelBinding) { super(account); - + this.channelBinding = channelBinding; // This nonce should be different for each authentication attempt. this.clientNonce = CryptoHelper.random(100); clientFirstMessageBare = ""; diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java index 472c4dea1..9bcc8ad47 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java @@ -11,7 +11,7 @@ public class ScramSha1 extends ScramMechanism { public static final String MECHANISM = "SCRAM-SHA-1"; public ScramSha1(final Account account) { - super(account); + super(account, ChannelBinding.NONE); } @Override diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java index f3f6cab57..610ed788b 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java @@ -11,7 +11,7 @@ public class ScramSha256 extends ScramMechanism { public static final String MECHANISM = "SCRAM-SHA-256"; public ScramSha256(final Account account) { - super(account); + super(account, ChannelBinding.NONE); } @Override diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java index 9a2f1a82d..3d54b39e9 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java @@ -11,7 +11,7 @@ public class ScramSha512 extends ScramMechanism { public static final String MECHANISM = "SCRAM-SHA-512"; public ScramSha512(final Account account) { - super(account); + super(account, ChannelBinding.NONE); } @Override diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index a4fd8c063..e9f9639ec 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -17,6 +17,7 @@ public final class Namespace { public static final String OOB = "jabber:x:oob"; public static final String SASL = "urn:ietf:params:xml:ns:xmpp-sasl"; public static final String SASL_2 = "urn:xmpp:sasl:1"; + public static final String CHANNEL_BINDING = "urn:xmpp:sasl-cb:0"; public static final String TLS = "urn:ietf:params:xml:ns:xmpp-tls"; public static final String PUBSUB = "http://jabber.org/protocol/pubsub"; public static final String PUBSUB_PUBLISH_OPTIONS = PUBSUB + "#publish-options"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index a1336e22c..b5d7fd1af 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -14,6 +14,7 @@ import androidx.annotation.NonNull; +import com.google.common.base.Predicates; import com.google.common.base.Strings; import com.google.common.collect.Collections2; @@ -62,14 +63,8 @@ import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.XmppDomainVerifier; import eu.siacs.conversations.crypto.axolotl.AxolotlService; -import eu.siacs.conversations.crypto.sasl.Anonymous; -import eu.siacs.conversations.crypto.sasl.DigestMd5; -import eu.siacs.conversations.crypto.sasl.External; -import eu.siacs.conversations.crypto.sasl.Plain; +import eu.siacs.conversations.crypto.sasl.ChannelBinding; import eu.siacs.conversations.crypto.sasl.SaslMechanism; -import eu.siacs.conversations.crypto.sasl.ScramSha1; -import eu.siacs.conversations.crypto.sasl.ScramSha256; -import eu.siacs.conversations.crypto.sasl.ScramSha512; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.ServiceDiscoveryResult; @@ -720,7 +715,7 @@ private boolean processSuccess(final Element success) Log.d( Config.LOGTAG, account.getJid().asBareJid().toString() + ": logged in (using " + version + ")"); - //TODO store mechanism name + // TODO store mechanism name account.setKey(Account.PINNED_MECHANISM_KEY, String.valueOf(saslMechanism.getPriority())); if (version == SaslMechanism.Version.SASL_2) { final String authorizationIdentifier = @@ -784,7 +779,7 @@ private boolean processSuccess(final Element success) account.getJid().asBareJid() + ": successfully enabled carbons"); features.carbonsEnabled = true; } - //TODO if both are set mark account ready for pipelining + // TODO if both are set mark account ready for pipelining sendPostBindInitialization(streamManagementEnabled != null, carbonsEnabled != null); } } @@ -1218,10 +1213,30 @@ private void processStreamFeatures(final Tag currentTag) throws IOException { } private void authenticate(final SaslMechanism.Version version) throws IOException { - final Element element = streamFeatures.findChild("mechanisms"); - final Collection mechanisms = Collections2.transform(element.getChildren(), c -> c == null ? null : c.getContent()); + Log.d(Config.LOGTAG, "stream features: " + this.streamFeatures); + final Element element = + this.streamFeatures.findChild("mechanisms"); // TODO get from correct NS + final Collection mechanisms = + Collections2.transform( + Collections2.filter( + element.getChildren(), + c -> c != null && "mechanism".equals(c.getName())), + c -> c == null ? null : c.getContent()); + final Element cbElement = + this.streamFeatures.findChild("sasl-channel-binding", Namespace.CHANNEL_BINDING); + final Collection channelBindings = + Collections2.filter( + Collections2.transform( + Collections2.filter( + cbElement == null + ? Collections.emptyList() + : cbElement.getChildren(), + c -> c != null && "channel-binding".equals(c.getName())), + c -> c == null ? null : ChannelBinding.of(c.getAttribute("type"))), + Predicates.notNull()); + Log.d(Config.LOGTAG, "channel bindings: " + channelBindings); final SaslMechanism.Factory factory = new SaslMechanism.Factory(account); - this.saslMechanism = factory.of(mechanisms); + this.saslMechanism = factory.of(mechanisms, channelBindings); if (saslMechanism == null) { Log.d( From 5da9f5b3a344001a85f5299e69557d23771bcc9d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 6 Sep 2022 16:28:28 +0200 Subject: [PATCH 187/394] refactor ScramMechanism to support PLUS --- .../java/eu/siacs/conversations/Config.java | 2 + .../conversations/crypto/sasl/DigestMd5.java | 5 +- .../crypto/sasl/SaslMechanism.java | 13 +++++- .../crypto/sasl/ScramMechanism.java | 46 +++++++++++++++---- .../crypto/sasl/ScramPlusMechanism.java | 22 +++++++++ .../crypto/sasl/ScramSha1Plus.java | 36 +++++++++++++++ .../conversations/xmpp/XmppConnection.java | 19 ++++++-- 7 files changed, 126 insertions(+), 17 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java create mode 100644 src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 377be3ba1..f7c3dd151 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -57,6 +57,8 @@ public static boolean multipleEncryptionChoices() { public static final long CONTACT_SYNC_RETRY_INTERVAL = 1000L * 60 * 5; + public static final boolean SASL_2_ENABLED = false; + //Notification settings public static final boolean HIDE_MESSAGE_TEXT_IN_NOTIFICATION = false; public static final boolean ALWAYS_NOTIFY_BY_DEFAULT = false; diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java b/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java index 7229299ef..b75d0883f 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java @@ -6,6 +6,8 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import javax.net.ssl.SSLSocket; + import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.utils.CryptoHelper; @@ -29,7 +31,8 @@ public String getMechanism() { } @Override - public String getResponse(final String challenge) throws AuthenticationException { + public String getResponse(final String challenge, final SSLSocket sslSocket) + throws AuthenticationException { switch (state) { case INITIAL: state = State.RESPONSE_SENT; diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java index 13360a063..5fafde9e9 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java @@ -4,6 +4,8 @@ import java.util.Collection; +import javax.net.ssl.SSLSocket; + import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; @@ -31,7 +33,8 @@ public String getClientFirstMessage() { return ""; } - public String getResponse(final String challenge) throws AuthenticationException { + public String getResponse(final String challenge, final SSLSocket sslSocket) + throws AuthenticationException { return ""; } @@ -112,4 +115,12 @@ public SaslMechanism of( } } } + + public static String namespace(final Version version) { + if (version == Version.SASL) { + return Namespace.SASL; + } else { + return Namespace.SASL_2; + } + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java index 887128a0c..e6bc3a15d 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java @@ -2,6 +2,7 @@ import android.util.Base64; +import com.google.common.base.CaseFormat; import com.google.common.base.Objects; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; @@ -14,18 +15,19 @@ import java.security.InvalidKeyException; import java.util.concurrent.ExecutionException; +import javax.net.ssl.SSLSocket; + import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.utils.CryptoHelper; abstract class ScramMechanism extends SaslMechanism { - // TODO: When channel binding (SCRAM-SHA1-PLUS) is supported in future, generalize this to - // indicate support and/or usage. - private static final String GS2_HEADER = "n,,"; + private static final byte[] CLIENT_KEY_BYTES = "Client Key".getBytes(); private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes(); private static final Cache CACHE = CacheBuilder.newBuilder().maximumSize(10).build(); protected final ChannelBinding channelBinding; + private final String gs2Header; private final String clientNonce; protected State state = State.INITIAL; private String clientFirstMessageBare; @@ -34,6 +36,16 @@ abstract class ScramMechanism extends SaslMechanism { ScramMechanism(final Account account, final ChannelBinding channelBinding) { super(account); this.channelBinding = channelBinding; + if (channelBinding == ChannelBinding.NONE) { + this.gs2Header = "n,,"; + } else { + this.gs2Header = + String.format( + "p=%s,,", + CaseFormat.UPPER_UNDERSCORE + .converterTo(CaseFormat.LOWER_HYPHEN) + .convert(channelBinding.toString())); + } // This nonce should be different for each authentication attempt. this.clientNonce = CryptoHelper.random(100); clientFirstMessageBare = ""; @@ -69,7 +81,7 @@ private byte[] hmac(final byte[] key, final byte[] input) throws InvalidKeyExcep return out; } - public byte[] digest(byte[] bytes) { + public byte[] digest(final byte[] bytes) { final Digest digest = getDigest(); digest.reset(); digest.update(bytes, 0, bytes.length); @@ -107,12 +119,13 @@ public String getClientFirstMessage() { state = State.AUTH_TEXT_SENT; } return Base64.encodeToString( - (GS2_HEADER + clientFirstMessageBare).getBytes(Charset.defaultCharset()), + (gs2Header + clientFirstMessageBare).getBytes(Charset.defaultCharset()), Base64.NO_WRAP); } @Override - public String getResponse(final String challenge) throws AuthenticationException { + public String getResponse(final String challenge, final SSLSocket socket) + throws AuthenticationException { switch (state) { case AUTH_TEXT_SENT: if (challenge == null) { @@ -169,11 +182,17 @@ public String getResponse(final String challenge) throws AuthenticationException throw new AuthenticationException("Server sent empty salt"); } + final byte[] channelBindingData = getChannelBindingData(socket); + + final int gs2Len = this.gs2Header.getBytes().length; + final byte[] cMessage = new byte[gs2Len + channelBindingData.length]; + System.arraycopy(this.gs2Header.getBytes(), 0, cMessage, 0, gs2Len); + System.arraycopy( + channelBindingData, 0, cMessage, gs2Len, channelBindingData.length); + final String clientFinalMessageWithoutProof = - "c=" - + Base64.encodeToString(GS2_HEADER.getBytes(), Base64.NO_WRAP) - + ",r=" - + nonce; + "c=" + Base64.encodeToString(cMessage, Base64.NO_WRAP) + ",r=" + nonce; + final byte[] authMessage = (clientFirstMessageBare + ',' @@ -239,6 +258,13 @@ public String getResponse(final String challenge) throws AuthenticationException } } + protected byte[] getChannelBindingData(final SSLSocket sslSocket) throws AuthenticationException { + if (this.channelBinding == ChannelBinding.NONE) { + return new byte[0]; + } + throw new AssertionError("getChannelBindingData needs to be overwritten"); + } + private static class CacheKey { final String algorithm; final String password; diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java new file mode 100644 index 000000000..0067a4237 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java @@ -0,0 +1,22 @@ +package eu.siacs.conversations.crypto.sasl; + +import javax.net.ssl.SSLSocket; + +import eu.siacs.conversations.entities.Account; + +abstract class ScramPlusMechanism extends ScramMechanism { + ScramPlusMechanism(Account account, ChannelBinding channelBinding) { + super(account, channelBinding); + } + + @Override + protected byte[] getChannelBindingData(final SSLSocket sslSocket) throws AuthenticationException { + if (this.channelBinding == ChannelBinding.NONE) { + throw new AuthenticationException(String.format("%s is not a valid channel binding", ChannelBinding.NONE)); + } + if (sslSocket == null) { + throw new AuthenticationException("Channel binding attempt on non secure socket"); + } + throw new AssertionError("not yet implemented"); + } +} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java new file mode 100644 index 000000000..34d9009fc --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java @@ -0,0 +1,36 @@ +package eu.siacs.conversations.crypto.sasl; + +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.digests.SHA1Digest; +import org.bouncycastle.crypto.macs.HMac; + +import eu.siacs.conversations.entities.Account; + +public class ScramSha1Plus extends ScramPlusMechanism { + + public static final String MECHANISM = "SCRAM-SHA-1-PLUS"; + + public ScramSha1Plus(final Account account, final ChannelBinding channelBinding) { + super(account, channelBinding); + } + + @Override + protected HMac getHMAC() { + return new HMac(new SHA1Digest()); + } + + @Override + protected Digest getDigest() { + return new SHA1Digest(); + } + + @Override + public int getPriority() { + return 35; //higher than SCRAM-SHA512 (30) + } + + @Override + public String getMechanism() { + return MECHANISM; + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index b5d7fd1af..a1719dd25 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -607,7 +607,7 @@ private void processStream() throws XmlPullParserException, IOException { throw new AssertionError("Missing implementation for " + version); } try { - response.setContent(saslMechanism.getResponse(challenge.getContent())); + response.setContent(saslMechanism.getResponse(challenge.getContent(), sslSocketOrNull(socket))); } catch (final SaslMechanism.AuthenticationException e) { // TODO: Send auth abort tag. Log.e(Config.LOGTAG, e.toString()); @@ -707,7 +707,7 @@ private boolean processSuccess(final Element success) throw new AssertionError("Missing implementation for " + version); } try { - saslMechanism.getResponse(challenge); + saslMechanism.getResponse(challenge, sslSocketOrNull(socket)); } catch (final SaslMechanism.AuthenticationException e) { Log.e(Config.LOGTAG, String.valueOf(e)); throw new StateChangingException(Account.State.UNAUTHORIZED); @@ -798,6 +798,14 @@ private boolean processSuccess(final Element success) } } + private static SSLSocket sslSocketOrNull(final Socket socket) { + if (socket instanceof SSLSocket) { + return (SSLSocket) socket; + } else { + return null; + } + } + private void processEnabled(final Element enabled) { final String streamId; if (enabled.getAttributeAsBoolean("resume")) { @@ -1170,7 +1178,8 @@ private void processStreamFeatures(final Tag currentTag) throws IOException { } else if (!this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE) && account.isOptionSet(Account.OPTION_REGISTER)) { throw new StateChangingException(Account.State.REGISTRATION_NOT_SUPPORTED); - } else if (this.streamFeatures.hasChild("mechanisms", Namespace.SASL_2) + } else if (Config.SASL_2_ENABLED + && this.streamFeatures.hasChild("mechanisms", Namespace.SASL_2) && shouldAuthenticate && isSecure) { authenticate(SaslMechanism.Version.SASL_2); @@ -1213,9 +1222,8 @@ private void processStreamFeatures(final Tag currentTag) throws IOException { } private void authenticate(final SaslMechanism.Version version) throws IOException { - Log.d(Config.LOGTAG, "stream features: " + this.streamFeatures); final Element element = - this.streamFeatures.findChild("mechanisms"); // TODO get from correct NS + this.streamFeatures.findChild("mechanisms", SaslMechanism.namespace(version)); final Collection mechanisms = Collections2.transform( Collections2.filter( @@ -1234,6 +1242,7 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio c -> c != null && "channel-binding".equals(c.getName())), c -> c == null ? null : ChannelBinding.of(c.getAttribute("type"))), Predicates.notNull()); + Log.d(Config.LOGTAG,"mechanisms: "+mechanisms); Log.d(Config.LOGTAG, "channel bindings: " + channelBindings); final SaslMechanism.Factory factory = new SaslMechanism.Factory(account); this.saslMechanism = factory.of(mechanisms, channelBindings); From 6d3d9dfe26a83fb49cbc12009bc5dfe1bea09704 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 6 Sep 2022 16:43:51 +0200 Subject: [PATCH 188/394] support channel binding with tls-exporter --- .../crypto/sasl/SaslMechanism.java | 19 ++++++++------- .../crypto/sasl/ScramMechanism.java | 3 ++- .../crypto/sasl/ScramPlusMechanism.java | 23 +++++++++++++++---- .../crypto/sasl/ScramSha1Plus.java | 2 +- 4 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java index 5fafde9e9..4380ad93c 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java @@ -18,6 +18,14 @@ protected SaslMechanism(final Account account) { this.account = account; } + public static String namespace(final Version version) { + if (version == Version.SASL) { + return Namespace.SASL; + } else { + return Namespace.SASL_2; + } + } + /** * The priority is used to pin the authentication mechanism. If authentication fails, it MAY be * retried with another mechanism of the same priority, but MUST NOT be tried with a mechanism @@ -97,6 +105,9 @@ public SaslMechanism of( final Collection mechanisms, final Collection bindings) { if (mechanisms.contains(External.MECHANISM) && account.getPrivateKeyAlias() != null) { return new External(account); + } else if (mechanisms.contains(ScramSha1Plus.MECHANISM) + && bindings.contains(ChannelBinding.TLS_EXPORTER)) { + return new ScramSha1Plus(account, ChannelBinding.TLS_EXPORTER); } else if (mechanisms.contains(ScramSha512.MECHANISM)) { return new ScramSha512(account); } else if (mechanisms.contains(ScramSha256.MECHANISM)) { @@ -115,12 +126,4 @@ public SaslMechanism of( } } } - - public static String namespace(final Version version) { - if (version == Version.SASL) { - return Namespace.SASL; - } else { - return Namespace.SASL_2; - } - } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java index e6bc3a15d..62f221b74 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java @@ -258,7 +258,8 @@ public String getResponse(final String challenge, final SSLSocket socket) } } - protected byte[] getChannelBindingData(final SSLSocket sslSocket) throws AuthenticationException { + protected byte[] getChannelBindingData(final SSLSocket sslSocket) + throws AuthenticationException { if (this.channelBinding == ChannelBinding.NONE) { return new byte[0]; } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java index 0067a4237..3b0dbb6e1 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java @@ -1,22 +1,35 @@ package eu.siacs.conversations.crypto.sasl; +import org.conscrypt.Conscrypt; + +import javax.net.ssl.SSLException; import javax.net.ssl.SSLSocket; import eu.siacs.conversations.entities.Account; abstract class ScramPlusMechanism extends ScramMechanism { + + private static final String EXPORTER_LABEL = "EXPORTER-Channel-Binding"; + ScramPlusMechanism(Account account, ChannelBinding channelBinding) { super(account, channelBinding); } @Override - protected byte[] getChannelBindingData(final SSLSocket sslSocket) throws AuthenticationException { - if (this.channelBinding == ChannelBinding.NONE) { - throw new AuthenticationException(String.format("%s is not a valid channel binding", ChannelBinding.NONE)); - } + protected byte[] getChannelBindingData(final SSLSocket sslSocket) + throws AuthenticationException { if (sslSocket == null) { throw new AuthenticationException("Channel binding attempt on non secure socket"); } - throw new AssertionError("not yet implemented"); + if (this.channelBinding == ChannelBinding.TLS_EXPORTER) { + try { + return Conscrypt.exportKeyingMaterial(sslSocket, EXPORTER_LABEL, new byte[0], 32); + } catch (final SSLException e) { + throw new AuthenticationException("Could not export keying material"); + } + } else { + throw new AuthenticationException( + String.format("%s is not a valid channel binding", ChannelBinding.NONE)); + } } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java index 34d9009fc..d4f2fcb0b 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java @@ -26,7 +26,7 @@ protected Digest getDigest() { @Override public int getPriority() { - return 35; //higher than SCRAM-SHA512 (30) + return 35; // higher than SCRAM-SHA512 (30) } @Override From 789d1dc2259fa930c3751647c60526841f68abb6 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 6 Sep 2022 17:01:57 +0200 Subject: [PATCH 189/394] support tls-unique for TLSv1.2 --- .../crypto/sasl/ChannelBinding.java | 12 ++++++++++++ .../conversations/crypto/sasl/SaslMechanism.java | 6 +++--- .../crypto/sasl/ScramPlusMechanism.java | 16 +++++++++++++++- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java index 847c50e9d..81bd12705 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java @@ -4,6 +4,8 @@ import com.google.common.base.CaseFormat; +import java.util.Collection; + import eu.siacs.conversations.Config; public enum ChannelBinding { @@ -24,4 +26,14 @@ public static ChannelBinding of(final String type) { return null; } } + + public static ChannelBinding best(final Collection bindings) { + if (bindings.contains(TLS_EXPORTER)) { + return TLS_EXPORTER; + } else if (bindings.contains(TLS_UNIQUE)) { + return TLS_UNIQUE; + } else { + return null; + } + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java index 4380ad93c..829a4e6ea 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java @@ -103,11 +103,11 @@ public Factory(final Account account) { public SaslMechanism of( final Collection mechanisms, final Collection bindings) { + final ChannelBinding channelBinding = ChannelBinding.best(bindings); if (mechanisms.contains(External.MECHANISM) && account.getPrivateKeyAlias() != null) { return new External(account); - } else if (mechanisms.contains(ScramSha1Plus.MECHANISM) - && bindings.contains(ChannelBinding.TLS_EXPORTER)) { - return new ScramSha1Plus(account, ChannelBinding.TLS_EXPORTER); + } else if (mechanisms.contains(ScramSha1Plus.MECHANISM) && channelBinding != null) { + return new ScramSha1Plus(account, channelBinding); } else if (mechanisms.contains(ScramSha512.MECHANISM)) { return new ScramSha512(account); } else if (mechanisms.contains(ScramSha256.MECHANISM)) { diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java index 3b0dbb6e1..8f6dec20e 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java @@ -22,11 +22,25 @@ protected byte[] getChannelBindingData(final SSLSocket sslSocket) throw new AuthenticationException("Channel binding attempt on non secure socket"); } if (this.channelBinding == ChannelBinding.TLS_EXPORTER) { + final byte[] keyingMaterial; try { - return Conscrypt.exportKeyingMaterial(sslSocket, EXPORTER_LABEL, new byte[0], 32); + keyingMaterial = + Conscrypt.exportKeyingMaterial(sslSocket, EXPORTER_LABEL, new byte[0], 32); } catch (final SSLException e) { throw new AuthenticationException("Could not export keying material"); } + if (keyingMaterial == null) { + throw new AuthenticationException( + "Could not export keying material. Socket not ready"); + } + return keyingMaterial; + } else if (this.channelBinding == ChannelBinding.TLS_UNIQUE) { + final byte[] unique = Conscrypt.getTlsUnique(sslSocket); + if (unique == null) { + throw new AuthenticationException( + "Could not retrieve tls unique. Socket not ready"); + } + return unique; } else { throw new AuthenticationException( String.format("%s is not a valid channel binding", ChannelBinding.NONE)); From e8bce17940f53669027fd31086cda24204be549e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 6 Sep 2022 17:39:58 +0200 Subject: [PATCH 190/394] add scram-sha256 and 512 in their plus variants --- .../crypto/sasl/SaslMechanism.java | 4 +++ .../crypto/sasl/ScramMechanism.java | 3 ++ .../crypto/sasl/ScramSha256Plus.java | 36 +++++++++++++++++++ .../crypto/sasl/ScramSha512Plus.java | 36 +++++++++++++++++++ 4 files changed, 79 insertions(+) create mode 100644 src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256Plus.java create mode 100644 src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512Plus.java diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java index 829a4e6ea..aaff4cc82 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java @@ -106,6 +106,10 @@ public SaslMechanism of( final ChannelBinding channelBinding = ChannelBinding.best(bindings); if (mechanisms.contains(External.MECHANISM) && account.getPrivateKeyAlias() != null) { return new External(account); + } else if (mechanisms.contains(ScramSha512Plus.MECHANISM) && channelBinding != null) { + return new ScramSha512Plus(account, channelBinding); + } else if (mechanisms.contains(ScramSha256Plus.MECHANISM) && channelBinding != null) { + return new ScramSha256Plus(account, channelBinding); } else if (mechanisms.contains(ScramSha1Plus.MECHANISM) && channelBinding != null) { return new ScramSha1Plus(account, channelBinding); } else if (mechanisms.contains(ScramSha512.MECHANISM)) { diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java index 62f221b74..aba434e3a 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java @@ -37,6 +37,9 @@ abstract class ScramMechanism extends SaslMechanism { super(account); this.channelBinding = channelBinding; if (channelBinding == ChannelBinding.NONE) { + // TODO this needs to be changed to "y,," for the scram internal down grade protection + // but we might risk compatibility issues if the server supports a binding that we don’t + // support this.gs2Header = "n,,"; } else { this.gs2Header = diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256Plus.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256Plus.java new file mode 100644 index 000000000..f48a052ab --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256Plus.java @@ -0,0 +1,36 @@ +package eu.siacs.conversations.crypto.sasl; + +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.macs.HMac; + +import eu.siacs.conversations.entities.Account; + +public class ScramSha256Plus extends ScramPlusMechanism { + + public static final String MECHANISM = "SCRAM-SHA-256-PLUS"; + + public ScramSha256Plus(final Account account, final ChannelBinding channelBinding) { + super(account, channelBinding); + } + + @Override + protected HMac getHMAC() { + return new HMac(new SHA256Digest()); + } + + @Override + protected Digest getDigest() { + return new SHA256Digest(); + } + + @Override + public int getPriority() { + return 40; + } + + @Override + public String getMechanism() { + return MECHANISM; + } +} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512Plus.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512Plus.java new file mode 100644 index 000000000..8cec1f33f --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512Plus.java @@ -0,0 +1,36 @@ +package eu.siacs.conversations.crypto.sasl; + +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.digests.SHA512Digest; +import org.bouncycastle.crypto.macs.HMac; + +import eu.siacs.conversations.entities.Account; + +public class ScramSha512Plus extends ScramPlusMechanism { + + public static final String MECHANISM = "SCRAM-SHA-512-PLUS"; + + public ScramSha512Plus(final Account account, final ChannelBinding channelBinding) { + super(account, channelBinding); + } + + @Override + protected HMac getHMAC() { + return new HMac(new SHA512Digest()); + } + + @Override + protected Digest getDigest() { + return new SHA512Digest(); + } + + @Override + public int getPriority() { + return 45; + } + + @Override + public String getMechanism() { + return MECHANISM; + } +} From d4ec1eaf3878dc1f43f1bf81124731de64537bfd Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 7 Sep 2022 10:31:11 +0200 Subject: [PATCH 191/394] refactor processFailure and processChallange into methods --- .../conversations/xmpp/XmppConnection.java | 111 ++++++++++-------- 1 file changed, 60 insertions(+), 51 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index a1719dd25..5bcc99f13 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -559,61 +559,13 @@ private void processStream() throws XmlPullParserException, IOException { throw new StateChangingException(Account.State.TLS_ERROR); } else if (nextTag.isStart("failure")) { final Element failure = tagReader.readElement(nextTag); - final SaslMechanism.Version version; - try { - version = SaslMechanism.Version.of(failure); - } catch (final IllegalArgumentException e) { - throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); - } - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": login failure " + version); - if (failure.hasChild("temporary-auth-failure")) { - throw new StateChangingException(Account.State.TEMPORARY_AUTH_FAILURE); - } else if (failure.hasChild("account-disabled")) { - final String text = failure.findChildContent("text"); - if (Strings.isNullOrEmpty(text)) { - throw new StateChangingException(Account.State.UNAUTHORIZED); - } - final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(text); - if (matcher.find()) { - final HttpUrl url; - try { - url = HttpUrl.get(text.substring(matcher.start(), matcher.end())); - } catch (final IllegalArgumentException e) { - throw new StateChangingException(Account.State.UNAUTHORIZED); - } - if (url.isHttps()) { - this.redirectionUrl = url; - throw new StateChangingException(Account.State.PAYMENT_REQUIRED); - } - } - } - throw new StateChangingException(Account.State.UNAUTHORIZED); + processFailure(failure); } else if (nextTag.isStart("continue", Namespace.SASL_2)) { + // two step sasl2 - we don’t support this yet throw new StateChangingException(Account.State.INCOMPATIBLE_CLIENT); } else if (nextTag.isStart("challenge")) { final Element challenge = tagReader.readElement(nextTag); - final SaslMechanism.Version version; - try { - version = SaslMechanism.Version.of(challenge); - } catch (final IllegalArgumentException e) { - throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); - } - final Element response; - if (version == SaslMechanism.Version.SASL) { - response = new Element("response", Namespace.SASL); - } else if (version == SaslMechanism.Version.SASL_2) { - response = new Element("response", Namespace.SASL_2); - } else { - throw new AssertionError("Missing implementation for " + version); - } - try { - response.setContent(saslMechanism.getResponse(challenge.getContent(), sslSocketOrNull(socket))); - } catch (final SaslMechanism.AuthenticationException e) { - // TODO: Send auth abort tag. - Log.e(Config.LOGTAG, e.toString()); - throw new StateChangingException(Account.State.UNAUTHORIZED); - } - tagWriter.writeElement(response); + processChallenge(challenge); } else if (nextTag.isStart("enabled", Namespace.STREAM_MANAGEMENT)) { final Element enabled = tagReader.readElement(nextTag); processEnabled(enabled); @@ -690,6 +642,31 @@ private void processStream() throws XmlPullParserException, IOException { } } + private void processChallenge(Element challenge) throws IOException { + final SaslMechanism.Version version; + try { + version = SaslMechanism.Version.of(challenge); + } catch (final IllegalArgumentException e) { + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } + final Element response; + if (version == SaslMechanism.Version.SASL) { + response = new Element("response", Namespace.SASL); + } else if (version == SaslMechanism.Version.SASL_2) { + response = new Element("response", Namespace.SASL_2); + } else { + throw new AssertionError("Missing implementation for " + version); + } + try { + response.setContent(saslMechanism.getResponse(challenge.getContent(), sslSocketOrNull(socket))); + } catch (final SaslMechanism.AuthenticationException e) { + // TODO: Send auth abort tag. + Log.e(Config.LOGTAG, e.toString()); + throw new StateChangingException(Account.State.UNAUTHORIZED); + } + tagWriter.writeElement(response); + } + private boolean processSuccess(final Element success) throws IOException, XmlPullParserException { final SaslMechanism.Version version; @@ -798,6 +775,38 @@ private boolean processSuccess(final Element success) } } + private void processFailure(final Element failure) throws StateChangingException { + final SaslMechanism.Version version; + try { + version = SaslMechanism.Version.of(failure); + } catch (final IllegalArgumentException e) { + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": login failure " + version); + if (failure.hasChild("temporary-auth-failure")) { + throw new StateChangingException(Account.State.TEMPORARY_AUTH_FAILURE); + } else if (failure.hasChild("account-disabled")) { + final String text = failure.findChildContent("text"); + if (Strings.isNullOrEmpty(text)) { + throw new StateChangingException(Account.State.UNAUTHORIZED); + } + final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(text); + if (matcher.find()) { + final HttpUrl url; + try { + url = HttpUrl.get(text.substring(matcher.start(), matcher.end())); + } catch (final IllegalArgumentException e) { + throw new StateChangingException(Account.State.UNAUTHORIZED); + } + if (url.isHttps()) { + this.redirectionUrl = url; + throw new StateChangingException(Account.State.PAYMENT_REQUIRED); + } + } + } + throw new StateChangingException(Account.State.UNAUTHORIZED); + } + private static SSLSocket sslSocketOrNull(final Socket socket) { if (socket instanceof SSLSocket) { return (SSLSocket) socket; From 018e0d9edfd212c866063e04297a59b500b2c393 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 7 Sep 2022 11:08:54 +0200 Subject: [PATCH 192/394] add (inactive) channel binding end-point code --- .../crypto/sasl/ScramPlusMechanism.java | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java index 8f6dec20e..8de4524f2 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java @@ -1,11 +1,24 @@ package eu.siacs.conversations.crypto.sasl; +import android.util.Log; + +import org.bouncycastle.jcajce.provider.digest.SHA256; import org.conscrypt.Conscrypt; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; + import javax.net.ssl.SSLException; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; +import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.utils.CryptoHelper; abstract class ScramPlusMechanism extends ScramMechanism { @@ -41,9 +54,59 @@ protected byte[] getChannelBindingData(final SSLSocket sslSocket) "Could not retrieve tls unique. Socket not ready"); } return unique; + } else if (this.channelBinding == ChannelBinding.TLS_SERVER_END_POINT) { + final byte[] endPoint = getServerEndPointChannelBinding(sslSocket.getSession()); + Log.d(Config.LOGTAG, "retrieved endpoint " + CryptoHelper.bytesToHex(endPoint)); + return endPoint; } else { throw new AuthenticationException( String.format("%s is not a valid channel binding", ChannelBinding.NONE)); } } + + private byte[] getServerEndPointChannelBinding(final SSLSession session) + throws AuthenticationException { + final Certificate[] certificates; + try { + certificates = session.getPeerCertificates(); + } catch (final SSLPeerUnverifiedException e) { + throw new AuthenticationException("Could not verify peer certificates"); + } + if (certificates == null || certificates.length == 0) { + throw new AuthenticationException("Could not retrieve peer certificate"); + } + final X509Certificate certificate; + if (certificates[0] instanceof X509Certificate) { + certificate = (X509Certificate) certificates[0]; + } else { + throw new AuthenticationException("Certificate was not X509"); + } + final String algorithm = certificate.getSigAlgName(); + final int withIndex = algorithm.indexOf("with"); + if (withIndex <= 0) { + throw new AuthenticationException("Unable to parse SigAlgName"); + } + final String hashAlgorithm = algorithm.substring(0, withIndex); + final MessageDigest messageDigest; + // https://www.rfc-editor.org/rfc/rfc5929#section-4.1 + if ("MD5".equalsIgnoreCase(hashAlgorithm) || "SHA1".equalsIgnoreCase(hashAlgorithm)) { + messageDigest = new SHA256.Digest(); + } else { + try { + messageDigest = MessageDigest.getInstance(hashAlgorithm); + } catch (final NoSuchAlgorithmException e) { + throw new AuthenticationException( + "Could not instantiate message digest for " + hashAlgorithm); + } + } + Log.d(Config.LOGTAG, "hashing certificate with " + messageDigest.getAlgorithm()); + final byte[] encodedCertificate; + try { + encodedCertificate = certificate.getEncoded(); + } catch (final CertificateEncodingException e) { + throw new AuthenticationException("Could not encode certificate"); + } + messageDigest.update(encodedCertificate); + return messageDigest.digest(); + } } From ecbfe33e8d4b86603c343f6c92324211e7b76261 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 7 Sep 2022 12:08:50 +0200 Subject: [PATCH 193/394] support end-point channel binding as last choice option --- .../siacs/conversations/crypto/sasl/ChannelBinding.java | 2 ++ .../conversations/crypto/sasl/ScramPlusMechanism.java | 8 +------- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java index 81bd12705..c9211c898 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java @@ -32,6 +32,8 @@ public static ChannelBinding best(final Collection bindings) { return TLS_EXPORTER; } else if (bindings.contains(TLS_UNIQUE)) { return TLS_UNIQUE; + } else if (bindings.contains(TLS_SERVER_END_POINT)) { + return TLS_SERVER_END_POINT; } else { return null; } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java index 8de4524f2..8b23e9c92 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java @@ -1,7 +1,5 @@ package eu.siacs.conversations.crypto.sasl; -import android.util.Log; - import org.bouncycastle.jcajce.provider.digest.SHA256; import org.conscrypt.Conscrypt; @@ -16,9 +14,7 @@ import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; -import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.utils.CryptoHelper; abstract class ScramPlusMechanism extends ScramMechanism { @@ -56,11 +52,10 @@ protected byte[] getChannelBindingData(final SSLSocket sslSocket) return unique; } else if (this.channelBinding == ChannelBinding.TLS_SERVER_END_POINT) { final byte[] endPoint = getServerEndPointChannelBinding(sslSocket.getSession()); - Log.d(Config.LOGTAG, "retrieved endpoint " + CryptoHelper.bytesToHex(endPoint)); return endPoint; } else { throw new AuthenticationException( - String.format("%s is not a valid channel binding", ChannelBinding.NONE)); + String.format("%s is not a valid channel binding", channelBinding)); } } @@ -99,7 +94,6 @@ private byte[] getServerEndPointChannelBinding(final SSLSession session) "Could not instantiate message digest for " + hashAlgorithm); } } - Log.d(Config.LOGTAG, "hashing certificate with " + messageDigest.getAlgorithm()); final byte[] encodedCertificate; try { encodedCertificate = certificate.getEncoded(); From f7996a6c3c7fe23eeb2b005aec56eaf2b6e50397 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 7 Sep 2022 16:29:51 +0200 Subject: [PATCH 194/394] catch illegal state exception when copying file --- .../conversations/persistance/FileBackend.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 0d1c03fcb..2d5496f2f 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -684,7 +684,7 @@ private void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyExcepti } catch (final FileWriterException e) { cleanup(file); throw new FileCopyException(R.string.error_unable_to_create_temporary_file); - } catch (final SecurityException e) { + } catch (final SecurityException | IllegalStateException e) { cleanup(file); throw new FileCopyException(R.string.error_security_exception); } catch (final IOException e) { @@ -1576,19 +1576,19 @@ private int getMediaRuntime(final File file) { return 0; } return Integer.parseInt(value); - } catch (final IllegalArgumentException e) { + } catch (final Exception e) { return 0; } } private Dimensions getImageDimensions(File file) { - BitmapFactory.Options options = new BitmapFactory.Options(); + final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(file.getAbsolutePath(), options); - int rotation = getRotation(file); - boolean rotated = rotation == 90 || rotation == 270; - int imageHeight = rotated ? options.outWidth : options.outHeight; - int imageWidth = rotated ? options.outHeight : options.outWidth; + final int rotation = getRotation(file); + final boolean rotated = rotation == 90 || rotation == 270; + final int imageHeight = rotated ? options.outWidth : options.outHeight; + final int imageWidth = rotated ? options.outHeight : options.outWidth; return new Dimensions(imageHeight, imageWidth); } From a95d0fa8d368e12a6b191b20b47541694141210d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 9 Sep 2022 16:55:47 +0200 Subject: [PATCH 195/394] use resolveActivityInfo to display nagivate to button resolveActivity on the other hand only finds apps that are category_default fixes #4375 --- src/main/AndroidManifest.xml | 3 + .../ui/ShowLocationActivity.java | 403 +++++++++--------- 2 files changed, 213 insertions(+), 193 deletions(-) diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index e37b5ab36..24265da61 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -64,6 +64,9 @@
+ + + diff --git a/src/main/java/eu/siacs/conversations/ui/ShowLocationActivity.java b/src/main/java/eu/siacs/conversations/ui/ShowLocationActivity.java index 43c55de49..d4b6a2e30 100644 --- a/src/main/java/eu/siacs/conversations/ui/ShowLocationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ShowLocationActivity.java @@ -3,8 +3,8 @@ import android.content.ActivityNotFoundException; import android.content.ClipData; import android.content.ClipboardManager; -import android.content.ComponentName; import android.content.Intent; +import android.content.pm.ActivityInfo; import android.location.Location; import android.location.LocationListener; import android.net.Uri; @@ -17,6 +17,7 @@ import androidx.annotation.NonNull; import androidx.databinding.DataBindingUtil; +import org.jetbrains.annotations.NotNull; import org.osmdroid.util.GeoPoint; import java.util.HashMap; @@ -32,198 +33,214 @@ import eu.siacs.conversations.ui.widget.MyLocation; import eu.siacs.conversations.utils.LocationProvider; - public class ShowLocationActivity extends LocationActivity implements LocationListener { - private GeoPoint loc = LocationProvider.FALLBACK; - private ActivityShowLocationBinding binding; - - - private Uri createGeoUri() { - return Uri.parse("geo:" + this.loc.getLatitude() + "," + this.loc.getLongitude()); - } - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - this.binding = DataBindingUtil.setContentView(this,R.layout.activity_show_location); - setSupportActionBar(binding.toolbar); - - configureActionBar(getSupportActionBar()); - setupMapView(this.binding.map, this.loc); - - this.binding.fab.setOnClickListener(view -> startNavigation()); - - final Intent intent = getIntent(); - if (intent != null) { - final String action = intent.getAction(); - if (action == null) { - return; - } - switch (action) { - case "eu.siacs.conversations.location.show": - if (intent.hasExtra("longitude") && intent.hasExtra("latitude")) { - final double longitude = intent.getDoubleExtra("longitude", 0); - final double latitude = intent.getDoubleExtra("latitude", 0); - this.loc = new GeoPoint(latitude, longitude); - } - break; - case Intent.ACTION_VIEW: - final Uri geoUri = intent.getData(); - - // Attempt to set zoom level if the geo URI specifies it - if (geoUri != null) { - final HashMap query = UriHelper.parseQueryString(geoUri.getQuery()); - - // Check for zoom level. - final String z = query.get("z"); - if (z != null) { - try { - mapController.setZoom(Double.valueOf(z)); - } catch (final Exception ignored) { - } - } - - // Check for the actual geo query. - boolean posInQuery = false; - final String q = query.get("q"); - if (q != null) { - final Pattern latlng = Pattern.compile("/^([-+]?[0-9]+(\\.[0-9]+)?),([-+]?[0-9]+(\\.[0-9]+)?)(\\(.*\\))?/"); - final Matcher m = latlng.matcher(q); - if (m.matches()) { - try { - this.loc = new GeoPoint(Double.valueOf(m.group(1)), Double.valueOf(m.group(3))); - posInQuery = true; - } catch (final Exception ignored) { - } - } - } - - final String schemeSpecificPart = geoUri.getSchemeSpecificPart(); - if (schemeSpecificPart != null && !schemeSpecificPart.isEmpty()) { - try { - final GeoPoint latlong = LocationHelper.parseLatLong(schemeSpecificPart); - if (latlong != null && !posInQuery) { - this.loc = latlong; - } - } catch (final NumberFormatException ignored) { - } - } - } - - break; - } - updateLocationMarkers(); - } - } - - @Override - protected void gotoLoc(final boolean setZoomLevel) { - if (this.loc != null && mapController != null) { - if (setZoomLevel) { - mapController.setZoom(Config.Map.FINAL_ZOOM_LEVEL); - } - mapController.animateTo(new GeoPoint(this.loc)); - } - } - - @Override - public void onRequestPermissionsResult(final int requestCode, - @NonNull final String[] permissions, - @NonNull final int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - updateUi(); - } - - @Override - protected void setMyLoc(final Location location) { - this.myLoc = location; - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.menu_show_location, menu); - updateUi(); - return true; - } - - @Override - protected void updateLocationMarkers() { - super.updateLocationMarkers(); - if (this.myLoc != null) { - this.binding.map.getOverlays().add(new MyLocation(this, null, this.myLoc)); - } - this.binding.map.getOverlays().add(new Marker(this.marker_icon, this.loc)); - } - - @Override - protected void onPause() { - super.onPause(); - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case R.id.action_copy_location: - final ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); - if (clipboard != null) { - final ClipData clip = ClipData.newPlainText("location", createGeoUri().toString()); - clipboard.setPrimaryClip(clip); - Toast.makeText(this,R.string.url_copied_to_clipboard,Toast.LENGTH_SHORT).show(); - } - return true; - case R.id.action_share_location: - final Intent shareIntent = new Intent(); - shareIntent.setAction(Intent.ACTION_SEND); - shareIntent.putExtra(Intent.EXTRA_TEXT, createGeoUri().toString()); - shareIntent.setType("text/plain"); - try { - startActivity(Intent.createChooser(shareIntent, getText(R.string.share_with))); - } catch (final ActivityNotFoundException e) { - //This should happen only on faulty androids because normally chooser is always available - Toast.makeText(this, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT).show(); - } - return true; - } - return super.onOptionsItemSelected(item); - } - - private void startNavigation() { - startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse( - "google.navigation:q=" + - this.loc.getLatitude() + "," + this.loc.getLongitude() - ))); - } - - @Override - protected void updateUi() { - final Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse("google.navigation:q=0,0")); - final ComponentName component = i.resolveActivity(getPackageManager()); - this.binding.fab.setVisibility(component == null ? View.GONE : View.VISIBLE); - } - - @Override - public void onLocationChanged(final Location location) { - if (LocationHelper.isBetterLocation(location, this.myLoc)) { - this.myLoc = location; - updateLocationMarkers(); - } - } - - @Override - public void onStatusChanged(final String provider, final int status, final Bundle extras) { - - } - - @Override - public void onProviderEnabled(final String provider) { - - } - - @Override - public void onProviderDisabled(final String provider) { - - } + private GeoPoint loc = LocationProvider.FALLBACK; + private ActivityShowLocationBinding binding; + + private Uri createGeoUri() { + return Uri.parse("geo:" + this.loc.getLatitude() + "," + this.loc.getLongitude()); + } + + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + this.binding = DataBindingUtil.setContentView(this, R.layout.activity_show_location); + setSupportActionBar(binding.toolbar); + + configureActionBar(getSupportActionBar()); + setupMapView(this.binding.map, this.loc); + + this.binding.fab.setOnClickListener(view -> startNavigation()); + + final Intent intent = getIntent(); + if (intent != null) { + final String action = intent.getAction(); + if (action == null) { + return; + } + switch (action) { + case "eu.siacs.conversations.location.show": + if (intent.hasExtra("longitude") && intent.hasExtra("latitude")) { + final double longitude = intent.getDoubleExtra("longitude", 0); + final double latitude = intent.getDoubleExtra("latitude", 0); + this.loc = new GeoPoint(latitude, longitude); + } + break; + case Intent.ACTION_VIEW: + final Uri geoUri = intent.getData(); + + // Attempt to set zoom level if the geo URI specifies it + if (geoUri != null) { + final HashMap query = + UriHelper.parseQueryString(geoUri.getQuery()); + + // Check for zoom level. + final String z = query.get("z"); + if (z != null) { + try { + mapController.setZoom(Double.valueOf(z)); + } catch (final Exception ignored) { + } + } + + // Check for the actual geo query. + boolean posInQuery = false; + final String q = query.get("q"); + if (q != null) { + final Pattern latlng = + Pattern.compile( + "/^([-+]?[0-9]+(\\.[0-9]+)?),([-+]?[0-9]+(\\.[0-9]+)?)(\\(.*\\))?/"); + final Matcher m = latlng.matcher(q); + if (m.matches()) { + try { + this.loc = + new GeoPoint( + Double.valueOf(m.group(1)), + Double.valueOf(m.group(3))); + posInQuery = true; + } catch (final Exception ignored) { + } + } + } + + final String schemeSpecificPart = geoUri.getSchemeSpecificPart(); + if (schemeSpecificPart != null && !schemeSpecificPart.isEmpty()) { + try { + final GeoPoint latlong = + LocationHelper.parseLatLong(schemeSpecificPart); + if (latlong != null && !posInQuery) { + this.loc = latlong; + } + } catch (final NumberFormatException ignored) { + } + } + } + + break; + } + updateLocationMarkers(); + } + } + + @Override + protected void gotoLoc(final boolean setZoomLevel) { + if (this.loc != null && mapController != null) { + if (setZoomLevel) { + mapController.setZoom(Config.Map.FINAL_ZOOM_LEVEL); + } + mapController.animateTo(new GeoPoint(this.loc)); + } + } + + @Override + public void onRequestPermissionsResult( + final int requestCode, + @NonNull final String[] permissions, + @NonNull final int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + updateUi(); + } + + @Override + protected void setMyLoc(final Location location) { + this.myLoc = location; + } + + @Override + public boolean onCreateOptionsMenu(@NotNull final Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.menu_show_location, menu); + updateUi(); + return true; + } + + @Override + protected void updateLocationMarkers() { + super.updateLocationMarkers(); + if (this.myLoc != null) { + this.binding.map.getOverlays().add(new MyLocation(this, null, this.myLoc)); + } + this.binding.map.getOverlays().add(new Marker(this.marker_icon, this.loc)); + } + + @Override + protected void onPause() { + super.onPause(); + } + + @Override + public boolean onOptionsItemSelected(final MenuItem item) { + switch (item.getItemId()) { + case R.id.action_copy_location: + final ClipboardManager clipboard = + (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); + if (clipboard != null) { + final ClipData clip = + ClipData.newPlainText("location", createGeoUri().toString()); + clipboard.setPrimaryClip(clip); + Toast.makeText(this, R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT) + .show(); + } + return true; + case R.id.action_share_location: + final Intent shareIntent = new Intent(); + shareIntent.setAction(Intent.ACTION_SEND); + shareIntent.putExtra(Intent.EXTRA_TEXT, createGeoUri().toString()); + shareIntent.setType("text/plain"); + try { + startActivity(Intent.createChooser(shareIntent, getText(R.string.share_with))); + } catch (final ActivityNotFoundException e) { + // This should happen only on faulty androids because normally chooser is always + // available + Toast.makeText( + this, + R.string.no_application_found_to_open_file, + Toast.LENGTH_SHORT) + .show(); + } + return true; + } + return super.onOptionsItemSelected(item); + } + + private void startNavigation() { + final Intent intent = getStartNavigationIntent(); + startActivity(intent); + } + + private Intent getStartNavigationIntent() { + return new Intent( + Intent.ACTION_VIEW, + Uri.parse( + "google.navigation:q=" + + this.loc.getLatitude() + + "," + + this.loc.getLongitude())); + } + + @Override + protected void updateUi() { + final Intent intent = getStartNavigationIntent(); + final ActivityInfo activityInfo = intent.resolveActivityInfo(getPackageManager(), 0); + this.binding.fab.setVisibility(activityInfo == null ? View.GONE : View.VISIBLE); + } + + @Override + public void onLocationChanged(@NotNull final Location location) { + if (LocationHelper.isBetterLocation(location, this.myLoc)) { + this.myLoc = location; + updateLocationMarkers(); + } + } + + @Override + public void onStatusChanged(final String provider, final int status, final Bundle extras) {} + + @Override + public void onProviderEnabled(@NotNull final String provider) {} + + @Override + public void onProviderDisabled(@NotNull final String provider) {} } From 82316d13b09eeb9184456013e62a6a6956cb6f98 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 9 Sep 2022 19:06:37 +0200 Subject: [PATCH 196/394] use weak reference to activity when using threads fixes #4366 --- .../conversations/ui/RecordingActivity.java | 60 +++++++++++-------- .../conversations/ui/RtpSessionActivity.java | 42 +++++++++---- 2 files changed, 66 insertions(+), 36 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java index bc9972316..6c58a404d 100644 --- a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java @@ -18,6 +18,7 @@ import androidx.databinding.DataBindingUtil; import java.io.File; +import java.lang.ref.WeakReference; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; @@ -136,30 +137,41 @@ protected void stopRecording(final boolean saveFile) { } } if (saveFile) { - new Thread( - () -> { - try { - if (!outputFileWrittenLatch.await(2, TimeUnit.SECONDS)) { - Log.d( - Config.LOGTAG, - "time out waiting for output file to be written"); - } - } catch (InterruptedException e) { - Log.d( - Config.LOGTAG, - "interrupted while waiting for output file to be written", - e); - } - runOnUiThread( - () -> { - setResult( - Activity.RESULT_OK, - new Intent() - .setData(Uri.fromFile(mOutputFile))); - finish(); - }); - }) - .start(); + new Thread(new Finisher(outputFileWrittenLatch, mOutputFile, this)).start(); + } + } + + private static class Finisher implements Runnable { + + private final CountDownLatch latch; + private final File outputFile; + private final WeakReference activityReference; + + private Finisher(CountDownLatch latch, File outputFile, Activity activity) { + this.latch = latch; + this.outputFile = outputFile; + this.activityReference = new WeakReference<>(activity); + } + + @Override + public void run() { + try { + if (!latch.await(5, TimeUnit.SECONDS)) { + Log.d(Config.LOGTAG, "time out waiting for output file to be written"); + } + } catch (final InterruptedException e) { + Log.d(Config.LOGTAG, "interrupted while waiting for output file to be written", e); + } + final Activity activity = activityReference.get(); + if (activity == null) { + return; + } + activity.runOnUiThread( + () -> { + activity.setResult( + Activity.RESULT_OK, new Intent().setData(Uri.fromFile(outputFile))); + activity.finish(); + }); } } diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index cbf00d04b..e73fdb23c 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -5,6 +5,7 @@ import android.Manifest; import android.annotation.SuppressLint; +import android.app.Activity; import android.app.PictureInPictureParams; import android.content.ActivityNotFoundException; import android.content.Context; @@ -297,21 +298,38 @@ private void checkRecorderAndAcceptCall() { } private void checkMicrophoneAvailabilityAsync() { - new Thread(this::checkMicrophoneAvailability).start(); + new Thread(new MicrophoneAvailabilityCheck(this)).start(); } - private void checkMicrophoneAvailability() { - final long start = SystemClock.elapsedRealtime(); - final boolean isMicrophoneAvailable = AppRTCAudioManager.isMicrophoneAvailable(); - final long stop = SystemClock.elapsedRealtime(); - Log.d(Config.LOGTAG, "checking microphone availability took " + (stop - start) + "ms"); - if (isMicrophoneAvailable) { - return; + private static class MicrophoneAvailabilityCheck implements Runnable { + + private final WeakReference activityReference; + + private MicrophoneAvailabilityCheck(final Activity activity) { + this.activityReference = new WeakReference<>(activity); + } + + @Override + public void run() { + final long start = SystemClock.elapsedRealtime(); + final boolean isMicrophoneAvailable = AppRTCAudioManager.isMicrophoneAvailable(); + final long stop = SystemClock.elapsedRealtime(); + Log.d(Config.LOGTAG, "checking microphone availability took " + (stop - start) + "ms"); + if (isMicrophoneAvailable) { + return; + } + final Activity activity = activityReference.get(); + if (activity == null) { + return; + } + activity.runOnUiThread( + () -> + Toast.makeText( + activity, + R.string.microphone_unavailable, + Toast.LENGTH_LONG) + .show()); } - runOnUiThread( - () -> - Toast.makeText(this, R.string.microphone_unavailable, Toast.LENGTH_LONG) - .show()); } private void putScreenInCallMode() { From 6e53ab36949a4b8e7d3307504bb92d4e5d501938 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 10 Sep 2022 12:36:35 +0200 Subject: [PATCH 197/394] allow invite only when muc is online. fixes #4218 --- src/main/java/eu/siacs/conversations/entities/MucOptions.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index 060b1b6f6..cc1c358de 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -156,7 +156,8 @@ public boolean hasVCards() { } public boolean canInvite() { - return !membersOnly() || self.getRole().ranks(Role.MODERATOR) || allowInvites(); + final boolean hasPermission = !membersOnly() || self.getRole().ranks(Role.MODERATOR) || allowInvites(); + return hasPermission && online(); } public boolean allowInvites() { From d0efe6eae2fb79d0f96373ffc51b7a40c2f3ff80 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 14 Sep 2022 12:27:02 +0200 Subject: [PATCH 198/394] bump recording wait for write to 8s --- .../java/eu/siacs/conversations/ui/RecordingActivity.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java index 6c58a404d..ad8684b72 100644 --- a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java @@ -156,7 +156,7 @@ private Finisher(CountDownLatch latch, File outputFile, Activity activity) { @Override public void run() { try { - if (!latch.await(5, TimeUnit.SECONDS)) { + if (!latch.await(8, TimeUnit.SECONDS)) { Log.d(Config.LOGTAG, "time out waiting for output file to be written"); } } catch (final InterruptedException e) { @@ -199,7 +199,7 @@ private void setupOutputFile() { setupFileObserver(parentDirectory); } - private void setupFileObserver(File directory) { + private void setupFileObserver(final File directory) { mFileObserver = new FileObserver(directory.getAbsolutePath()) { @Override @@ -219,7 +219,7 @@ private void tick() { } @Override - public void onClick(View view) { + public void onClick(final View view) { switch (view.getId()) { case R.id.cancel_button: mHandler.removeCallbacks(mTickExecutor); From c1abca35da2fd8887fe0dd85935a63a930accba0 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 14 Sep 2022 12:49:15 +0200 Subject: [PATCH 199/394] copy bookmarks before passing them to other parts of the app for read closes #4381 --- src/main/java/eu/siacs/conversations/Config.java | 2 +- .../java/eu/siacs/conversations/entities/Account.java | 9 ++++++--- .../conversations/services/XmppConnectionService.java | 8 ++++---- .../conversations/ui/StartConversationActivity.java | 4 ++-- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index f7c3dd151..a3eacc9db 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -57,7 +57,7 @@ public static boolean multipleEncryptionChoices() { public static final long CONTACT_SYNC_RETRY_INTERVAL = 1000L * 60 * 5; - public static final boolean SASL_2_ENABLED = false; + public static final boolean SASL_2_ENABLED = true; //Notification settings public static final boolean HIDE_MESSAGE_TEXT_IN_NOTIFICATION = false; diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 9220cc192..10f6ed8a8 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -6,6 +6,7 @@ import android.util.Log; import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; import org.json.JSONException; import org.json.JSONObject; @@ -488,17 +489,19 @@ public Roster getRoster() { } public Collection getBookmarks() { - return this.bookmarks.values(); + synchronized (this.bookmarks) { + return ImmutableList.copyOf(this.bookmarks.values()); + } } - public void setBookmarks(Map bookmarks) { + public void setBookmarks(final Map bookmarks) { synchronized (this.bookmarks) { this.bookmarks.clear(); this.bookmarks.putAll(bookmarks); } } - public void putBookmark(Bookmark bookmark) { + public void putBookmark(final Bookmark bookmark) { synchronized (this.bookmarks) { this.bookmarks.put(bookmark.getJid(), bookmark); } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 586b717ff..e4af37947 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1862,7 +1862,7 @@ private void pushBookmarksPrivateXml(Account account) { IqPacket iqPacket = new IqPacket(IqPacket.TYPE.SET); Element query = iqPacket.query("jabber:iq:private"); Element storage = query.addChild("storage", "storage:bookmarks"); - for (Bookmark bookmark : account.getBookmarks()) { + for (final Bookmark bookmark : account.getBookmarks()) { storage.addChild(bookmark); } sendIqPacket(account, iqPacket, mDefaultIqHandler); @@ -1870,8 +1870,8 @@ private void pushBookmarksPrivateXml(Account account) { private void pushBookmarksPep(Account account) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": pushing bookmarks via pep"); - Element storage = new Element("storage", "storage:bookmarks"); - for (Bookmark bookmark : account.getBookmarks()) { + final Element storage = new Element("storage", "storage:bookmarks"); + for (final Bookmark bookmark : account.getBookmarks()) { storage.addChild(bookmark); } pushNodeAndEnforcePublishOptions(account, Namespace.BOOKMARKS, storage, "current", PublishOptions.persistentWhitelistAccess()); @@ -4418,7 +4418,7 @@ public Collection getKnownConferenceHosts() { for (final Account account : accounts) { if (account.getXmppConnection() != null) { mucServers.addAll(account.getXmppConnection().getMucServers()); - for (Bookmark bookmark : account.getBookmarks()) { + for (final Bookmark bookmark : account.getBookmarks()) { final Jid jid = bookmark.getJid(); final String s = jid == null ? null : jid.getDomain().toEscapedString(); if (s != null) { diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index 994797779..91807295b 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -980,9 +980,9 @@ protected void filterContacts(String needle) { protected void filterConferences(String needle) { this.conferences.clear(); - for (Account account : xmppConnectionService.getAccounts()) { + for (final Account account : xmppConnectionService.getAccounts()) { if (account.getStatus() != Account.State.DISABLED) { - for (Bookmark bookmark : account.getBookmarks()) { + for (final Bookmark bookmark : account.getBookmarks()) { if (bookmark.match(this, needle)) { this.conferences.add(bookmark); } From 9ae0475413334b33b713dae6b84a070c890d1d76 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Wed, 14 Sep 2022 10:13:17 -0500 Subject: [PATCH 200/394] Show the name of the sender in search results (#4379) Just like a MUC, search results lack the context to be sure who sent a message, so show the name in the result item. --- .../java/eu/siacs/conversations/ui/SearchActivity.java | 2 +- .../eu/siacs/conversations/ui/adapter/MessageAdapter.java | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/SearchActivity.java b/src/main/java/eu/siacs/conversations/ui/SearchActivity.java index f5f4eb175..ec279f58e 100644 --- a/src/main/java/eu/siacs/conversations/ui/SearchActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/SearchActivity.java @@ -97,7 +97,7 @@ public void onCreate(final Bundle bundle) { this.binding = DataBindingUtil.setContentView(this, R.layout.activity_search); setSupportActionBar(this.binding.toolbar); configureActionBar(getSupportActionBar()); - this.messageListAdapter = new MessageAdapter(this, this.messages); + this.messageListAdapter = new MessageAdapter(this, this.messages, uuid == null); this.messageListAdapter.setOnContactPictureClicked(this); this.binding.searchResults.setAdapter(messageListAdapter); registerForContextMenu(this.binding.searchResults); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index a5ba05819..bb954f45e 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -87,6 +87,7 @@ public class MessageAdapter extends ArrayAdapter { private OnContactPictureClicked mOnContactPictureClickedListener; private OnContactPictureLongClicked mOnContactPictureLongClickedListener; private boolean mUseGreenBackground = false; + private boolean mForceNames = false; public MessageAdapter(XmppActivity activity, List messages) { super(activity, 0, messages); @@ -96,6 +97,10 @@ public MessageAdapter(XmppActivity activity, List messages) { updatePreferences(); } + public MessageAdapter(XmppActivity activity, List messages, boolean forceNames) { + this(activity, messages); + mForceNames = forceNames; + } private static void resetClickListener(View... views) { for (View view : views) { @@ -233,7 +238,7 @@ private void displayStatus(ViewHolder viewHolder, Message message, int type, boo error = true; break; default: - if (multiReceived) { + if (mForceNames || multiReceived) { info = UIHelper.getMessageDisplayName(message); } break; From 82efb6f1dbf290f86433416535179000cb084ba9 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 14 Sep 2022 17:51:22 +0200 Subject: [PATCH 201/394] code clean up --- .../siacs/conversations/entities/Account.java | 38 +++++++++---------- .../persistance/DatabaseBackend.java | 29 ++++++-------- .../ui/adapter/MessageAdapter.java | 10 ++--- 3 files changed, 36 insertions(+), 41 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 10f6ed8a8..1c16ab20b 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -125,31 +125,31 @@ private Account(final String uuid, final Jid jid, public static Account fromCursor(final Cursor cursor) { final Jid jid; try { - String resource = cursor.getString(cursor.getColumnIndex(RESOURCE)); + String resource = cursor.getString(cursor.getColumnIndexOrThrow(RESOURCE)); jid = Jid.of( - cursor.getString(cursor.getColumnIndex(USERNAME)), - cursor.getString(cursor.getColumnIndex(SERVER)), + cursor.getString(cursor.getColumnIndexOrThrow(USERNAME)), + cursor.getString(cursor.getColumnIndexOrThrow(SERVER)), resource == null || resource.trim().isEmpty() ? null : resource); } catch (final IllegalArgumentException ignored) { - Log.d(Config.LOGTAG, cursor.getString(cursor.getColumnIndex(USERNAME)) + "@" + cursor.getString(cursor.getColumnIndex(SERVER))); + Log.d(Config.LOGTAG, cursor.getString(cursor.getColumnIndexOrThrow(USERNAME)) + "@" + cursor.getString(cursor.getColumnIndexOrThrow(SERVER))); throw new AssertionError(ignored); } - return new Account(cursor.getString(cursor.getColumnIndex(UUID)), + return new Account(cursor.getString(cursor.getColumnIndexOrThrow(UUID)), jid, - cursor.getString(cursor.getColumnIndex(PASSWORD)), - cursor.getInt(cursor.getColumnIndex(OPTIONS)), - cursor.getString(cursor.getColumnIndex(ROSTERVERSION)), - cursor.getString(cursor.getColumnIndex(KEYS)), - cursor.getString(cursor.getColumnIndex(AVATAR)), - cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)), - cursor.getString(cursor.getColumnIndex(HOSTNAME)), - cursor.getInt(cursor.getColumnIndex(PORT)), - Presence.Status.fromShowString(cursor.getString(cursor.getColumnIndex(STATUS))), - cursor.getString(cursor.getColumnIndex(STATUS_MESSAGE))); - } - - public boolean httpUploadAvailable(long filesize) { - return xmppConnection != null && xmppConnection.getFeatures().httpUpload(filesize); + cursor.getString(cursor.getColumnIndexOrThrow(PASSWORD)), + cursor.getInt(cursor.getColumnIndexOrThrow(OPTIONS)), + cursor.getString(cursor.getColumnIndexOrThrow(ROSTERVERSION)), + cursor.getString(cursor.getColumnIndexOrThrow(KEYS)), + cursor.getString(cursor.getColumnIndexOrThrow(AVATAR)), + cursor.getString(cursor.getColumnIndexOrThrow(DISPLAY_NAME)), + cursor.getString(cursor.getColumnIndexOrThrow(HOSTNAME)), + cursor.getInt(cursor.getColumnIndexOrThrow(PORT)), + Presence.Status.fromShowString(cursor.getString(cursor.getColumnIndexOrThrow(STATUS))), + cursor.getString(cursor.getColumnIndexOrThrow(STATUS_MESSAGE))); + } + + public boolean httpUploadAvailable(long size) { + return xmppConnection != null && xmppConnection.getFeatures().httpUpload(size); } public boolean httpUploadAvailable() { diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index de9bc0d2c..9e4bf9f8e 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -967,33 +967,28 @@ public List getAccounts() { } public List getAccountJids(final boolean enabledOnly) { - SQLiteDatabase db = this.getReadableDatabase(); + final SQLiteDatabase db = this.getReadableDatabase(); final List jids = new ArrayList<>(); final String[] columns = new String[]{Account.USERNAME, Account.SERVER}; - String where = enabledOnly ? "not options & (1 <<1)" : null; - Cursor cursor = db.query(Account.TABLENAME, columns, where, null, null, null, null); - try { - while (cursor.moveToNext()) { + final String where = enabledOnly ? "not options & (1 <<1)" : null; + try (final Cursor cursor = db.query(Account.TABLENAME, columns, where, null, null, null, null)) { + while (cursor != null && cursor.moveToNext()) { jids.add(Jid.of(cursor.getString(0), cursor.getString(1), null)); } + } catch (final Exception e) { return jids; - } catch (Exception e) { - return jids; - } finally { - if (cursor != null) { - cursor.close(); - } } + return jids; } private List getAccounts(SQLiteDatabase db) { - List list = new ArrayList<>(); - Cursor cursor = db.query(Account.TABLENAME, null, null, null, null, - null, null); - while (cursor.moveToNext()) { - list.add(Account.fromCursor(cursor)); + final List list = new ArrayList<>(); + try (final Cursor cursor = + db.query(Account.TABLENAME, null, null, null, null, null, null)) { + while (cursor != null && cursor.moveToNext()) { + list.add(Account.fromCursor(cursor)); + } } - cursor.close(); return list; } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index bb954f45e..fb7aec14d 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -87,19 +87,19 @@ public class MessageAdapter extends ArrayAdapter { private OnContactPictureClicked mOnContactPictureClickedListener; private OnContactPictureLongClicked mOnContactPictureLongClickedListener; private boolean mUseGreenBackground = false; - private boolean mForceNames = false; + private final boolean mForceNames; - public MessageAdapter(XmppActivity activity, List messages) { + public MessageAdapter(final XmppActivity activity, final List messages, final boolean forceNames) { super(activity, 0, messages); this.audioPlayer = new AudioPlayer(this); this.activity = activity; metrics = getContext().getResources().getDisplayMetrics(); updatePreferences(); + this.mForceNames = forceNames; } - public MessageAdapter(XmppActivity activity, List messages, boolean forceNames) { - this(activity, messages); - mForceNames = forceNames; + public MessageAdapter(final XmppActivity activity, final List messages) { + this(activity, messages, false); } private static void resetClickListener(View... views) { From 495f79921dea64268caca789d1ee2e455e5f5d2c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 15 Sep 2022 12:22:05 +0200 Subject: [PATCH 202/394] store full sasl mechanism (not just priority) --- .../conversations/ui/MagicCreateActivity.java | 2 +- .../crypto/sasl/ChannelBinding.java | 12 ++++ .../crypto/sasl/SaslMechanism.java | 6 ++ .../crypto/sasl/ScramPlusMechanism.java | 9 ++- .../siacs/conversations/entities/Account.java | 72 +++++++++++++++---- .../persistance/DatabaseBackend.java | 46 ++++++------ .../conversations/ui/EditAccountActivity.java | 6 +- .../conversations/xmpp/XmppConnection.java | 7 +- 8 files changed, 117 insertions(+), 43 deletions(-) diff --git a/src/conversations/java/eu/siacs/conversations/ui/MagicCreateActivity.java b/src/conversations/java/eu/siacs/conversations/ui/MagicCreateActivity.java index 6f0386672..38761befd 100644 --- a/src/conversations/java/eu/siacs/conversations/ui/MagicCreateActivity.java +++ b/src/conversations/java/eu/siacs/conversations/ui/MagicCreateActivity.java @@ -100,7 +100,7 @@ protected void onCreate(final Bundle savedInstanceState) { account.setOption(Account.OPTION_MAGIC_CREATE, true); account.setOption(Account.OPTION_FIXED_USERNAME, fixedUsername); if (this.preAuth != null) { - account.setKey(Account.PRE_AUTH_REGISTRATION_TOKEN, this.preAuth); + account.setKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN, this.preAuth); } xmppConnectionService.createAccount(account); } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java index c9211c898..d8307a76d 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java @@ -3,6 +3,7 @@ import android.util.Log; import com.google.common.base.CaseFormat; +import com.google.common.base.Strings; import java.util.Collection; @@ -27,6 +28,17 @@ public static ChannelBinding of(final String type) { } } + public static ChannelBinding get(final String name) { + if (Strings.isNullOrEmpty(name)) { + return NONE; + } + try { + return valueOf(name); + } catch (final IllegalArgumentException e) { + return NONE; + } + } + public static ChannelBinding best(final Collection bindings) { if (bindings.contains(TLS_EXPORTER)) { return TLS_EXPORTER; diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java index aaff4cc82..e5b940b87 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java @@ -3,6 +3,7 @@ import com.google.common.base.Strings; import java.util.Collection; +import java.util.Collections; import javax.net.ssl.SSLSocket; @@ -129,5 +130,10 @@ public SaslMechanism of( return null; } } + + public SaslMechanism of(final String mechanism, final ChannelBinding channelBinding) { + return of(Collections.singleton(mechanism), Collections.singleton(channelBinding)); + } + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java index 8b23e9c92..707883d73 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java @@ -16,7 +16,7 @@ import eu.siacs.conversations.entities.Account; -abstract class ScramPlusMechanism extends ScramMechanism { +public abstract class ScramPlusMechanism extends ScramMechanism { private static final String EXPORTER_LABEL = "EXPORTER-Channel-Binding"; @@ -51,8 +51,7 @@ protected byte[] getChannelBindingData(final SSLSocket sslSocket) } return unique; } else if (this.channelBinding == ChannelBinding.TLS_SERVER_END_POINT) { - final byte[] endPoint = getServerEndPointChannelBinding(sslSocket.getSession()); - return endPoint; + return getServerEndPointChannelBinding(sslSocket.getSession()); } else { throw new AuthenticationException( String.format("%s is not a valid channel binding", channelBinding)); @@ -103,4 +102,8 @@ private byte[] getServerEndPointChannelBinding(final SSLSession session) messageDigest.update(encodedCertificate); return messageDigest.digest(); } + + public ChannelBinding getChannelBinding() { + return this.channelBinding; + } } diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 1c16ab20b..8446abbbd 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -25,6 +25,9 @@ import eu.siacs.conversations.crypto.PgpDecryptionService; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; +import eu.siacs.conversations.crypto.sasl.ChannelBinding; +import eu.siacs.conversations.crypto.sasl.SaslMechanism; +import eu.siacs.conversations.crypto.sasl.ScramPlusMechanism; import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.UIHelper; @@ -50,9 +53,9 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable public static final String STATUS = "status"; public static final String STATUS_MESSAGE = "status_message"; public static final String RESOURCE = "resource"; + public static final String PINNED_MECHANISM = "pinned_mechanism"; + public static final String PINNED_CHANNEL_BINDING = "pinned_channel_binding"; - public static final String PINNED_MECHANISM_KEY = "pinned_mechanism"; - public static final String PRE_AUTH_REGISTRATION_TOKEN = "pre_auth_registration"; public static final int OPTION_USETLS = 0; public static final int OPTION_DISABLED = 1; @@ -64,8 +67,13 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable public static final int OPTION_HTTP_UPLOAD_AVAILABLE = 7; public static final int OPTION_UNVERIFIED = 8; public static final int OPTION_FIXED_USERNAME = 9; + private static final String KEY_PGP_SIGNATURE = "pgp_signature"; private static final String KEY_PGP_ID = "pgp_id"; + private static final String KEY_PINNED_MECHANISM = "pinned_mechanism"; + public static final String KEY_PRE_AUTH_REGISTRATION_TOKEN = "pre_auth_registration"; + + protected final JSONObject keys; private final Roster roster = new Roster(this); private final Collection blocklist = new CopyOnWriteArraySet<>(); @@ -90,18 +98,20 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable private XmppConnection xmppConnection = null; private long mEndGracePeriod = 0L; private final Map bookmarks = new HashMap<>(); - private Presence.Status presenceStatus = Presence.Status.ONLINE; - private String presenceStatusMessage = null; + private Presence.Status presenceStatus; + private String presenceStatusMessage; + private String pinnedMechanism; + private String pinnedChannelBinding; public Account(final Jid jid, final String password) { this(java.util.UUID.randomUUID().toString(), jid, - password, 0, null, "", null, null, null, 5222, Presence.Status.ONLINE, null); + password, 0, null, "", null, null, null, 5222, Presence.Status.ONLINE, null, null, null); } private Account(final String uuid, final Jid jid, final String password, final int options, final String rosterVersion, final String keys, final String avatar, String displayName, String hostname, int port, - final Presence.Status status, String statusMessage) { + final Presence.Status status, String statusMessage, final String pinnedMechanism, final String pinnedChannelBinding) { this.uuid = uuid; this.jid = jid; this.password = password; @@ -120,19 +130,21 @@ private Account(final String uuid, final Jid jid, this.port = port; this.presenceStatus = status; this.presenceStatusMessage = statusMessage; + this.pinnedMechanism = pinnedMechanism; + this.pinnedChannelBinding = pinnedChannelBinding; } public static Account fromCursor(final Cursor cursor) { final Jid jid; try { - String resource = cursor.getString(cursor.getColumnIndexOrThrow(RESOURCE)); + final String resource = cursor.getString(cursor.getColumnIndexOrThrow(RESOURCE)); jid = Jid.of( cursor.getString(cursor.getColumnIndexOrThrow(USERNAME)), cursor.getString(cursor.getColumnIndexOrThrow(SERVER)), resource == null || resource.trim().isEmpty() ? null : resource); - } catch (final IllegalArgumentException ignored) { + } catch (final IllegalArgumentException e) { Log.d(Config.LOGTAG, cursor.getString(cursor.getColumnIndexOrThrow(USERNAME)) + "@" + cursor.getString(cursor.getColumnIndexOrThrow(SERVER))); - throw new AssertionError(ignored); + throw new AssertionError(e); } return new Account(cursor.getString(cursor.getColumnIndexOrThrow(UUID)), jid, @@ -145,7 +157,9 @@ public static Account fromCursor(final Cursor cursor) { cursor.getString(cursor.getColumnIndexOrThrow(HOSTNAME)), cursor.getInt(cursor.getColumnIndexOrThrow(PORT)), Presence.Status.fromShowString(cursor.getString(cursor.getColumnIndexOrThrow(STATUS))), - cursor.getString(cursor.getColumnIndexOrThrow(STATUS_MESSAGE))); + cursor.getString(cursor.getColumnIndexOrThrow(STATUS_MESSAGE)), + cursor.getString(cursor.getColumnIndexOrThrow(PINNED_MECHANISM)), + cursor.getString(cursor.getColumnIndexOrThrow(PINNED_CHANNEL_BINDING))); } public boolean httpUploadAvailable(long size) { @@ -289,6 +303,38 @@ public void setStatus(final State status) { } } + public void setPinnedMechanism(final SaslMechanism mechanism) { + this.pinnedMechanism = mechanism.getMechanism(); + if (mechanism instanceof ScramPlusMechanism) { + this.pinnedChannelBinding = ((ScramPlusMechanism) mechanism).getChannelBinding().toString(); + } + } + + public void resetPinnedMechanism() { + this.pinnedMechanism = null; + this.pinnedChannelBinding = null; + setKey(Account.KEY_PINNED_MECHANISM, String.valueOf(-1)); + } + + public int getPinnedMechanismPriority() { + final int fallback = getKeyAsInt(KEY_PINNED_MECHANISM, -1); + if (Strings.isNullOrEmpty(this.pinnedMechanism)) { + return fallback; + } + final SaslMechanism saslMechanism = getPinnedMechanism(); + if (saslMechanism == null) { + return fallback; + } else { + return saslMechanism.getPriority(); + } + } + + public SaslMechanism getPinnedMechanism() { + final String mechanism = Strings.nullToEmpty(this.pinnedMechanism); + final ChannelBinding channelBinding = ChannelBinding.get(this.pinnedChannelBinding); + return new SaslMechanism.Factory(this).of(mechanism, channelBinding); + } + public State getTrueStatus() { return this.status; } @@ -361,8 +407,8 @@ public boolean setKey(final String keyName, final String keyValue) { } } - public boolean setPrivateKeyAlias(String alias) { - return setKey("private_key_alias", alias); + public void setPrivateKeyAlias(final String alias) { + setKey("private_key_alias", alias); } public String getPrivateKeyAlias() { @@ -388,6 +434,8 @@ public ContentValues getContentValues() { values.put(STATUS, presenceStatus.toShowString()); values.put(STATUS_MESSAGE, presenceStatusMessage); values.put(RESOURCE, jid.getResource()); + values.put(PINNED_MECHANISM, pinnedMechanism); + values.put(PINNED_CHANNEL_BINDING, pinnedChannelBinding); return values; } diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 9e4bf9f8e..49de553eb 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -64,7 +64,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { private static final String DATABASE_NAME = "history"; - private static final int DATABASE_VERSION = 49; + private static final int DATABASE_VERSION = 50; private static boolean requiresMessageIndexRebuild = false; private static DatabaseBackend instance = null; @@ -230,6 +230,8 @@ public void onCreate(SQLiteDatabase db) { + Account.KEYS + " TEXT, " + Account.HOSTNAME + " TEXT, " + Account.RESOURCE + " TEXT," + + Account.PINNED_MECHANISM + " TEXT," + + Account.PINNED_CHANNEL_BINDING + " TEXT," + Account.PORT + " NUMBER DEFAULT 5222)"); db.execSQL("create table " + Conversation.TABLENAME + " (" + Conversation.UUID + " TEXT PRIMARY KEY, " + Conversation.NAME @@ -589,6 +591,11 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.endTransaction(); requiresMessageIndexRebuild = true; } + if (oldVersion < 50 && newVersion >= 50) { + db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.PINNED_MECHANISM + " TEXT"); + db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.PINNED_CHANNEL_BINDING + " TEXT"); + + } } private void canonicalizeJids(SQLiteDatabase db) { @@ -938,20 +945,19 @@ public Conversation findConversation(final Account account, final Jid contactJid contactJid.asBareJid().toString() + "/%", contactJid.asBareJid().toString() }; - Cursor cursor = db.query(Conversation.TABLENAME, null, + try(final Cursor cursor = db.query(Conversation.TABLENAME, null, Conversation.ACCOUNT + "=? AND (" + Conversation.CONTACTJID - + " like ? OR " + Conversation.CONTACTJID + "=?)", selectionArgs, null, null, null); - if (cursor.getCount() == 0) { - cursor.close(); - return null; - } - cursor.moveToFirst(); - Conversation conversation = Conversation.fromCursor(cursor); - cursor.close(); - if (conversation.getJid() instanceof InvalidJid) { - return null; + + " like ? OR " + Conversation.CONTACTJID + "=?)", selectionArgs, null, null, null)) { + if (cursor.getCount() == 0) { + return null; + } + cursor.moveToFirst(); + final Conversation conversation = Conversation.fromCursor(cursor); + if (conversation.getJid() instanceof InvalidJid) { + return null; + } + return conversation; } - return conversation; } public void updateConversation(final Conversation conversation) { @@ -1024,14 +1030,14 @@ public boolean updateMessage(Message message, String uuid) { } public void readRoster(Roster roster) { - SQLiteDatabase db = this.getReadableDatabase(); - Cursor cursor; - String[] args = {roster.getAccount().getUuid()}; - cursor = db.query(Contact.TABLENAME, null, Contact.ACCOUNT + "=?", args, null, null, null); - while (cursor.moveToNext()) { - roster.initContact(Contact.fromCursor(cursor)); + final SQLiteDatabase db = this.getReadableDatabase(); + final String[] args = {roster.getAccount().getUuid()}; + try (final Cursor cursor = + db.query(Contact.TABLENAME, null, Contact.ACCOUNT + "=?", args, null, null, null)) { + while (cursor.moveToNext()) { + roster.initContact(Contact.fromCursor(cursor)); + } } - cursor.close(); } public void writeRoster(final Roster roster) { diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 19424ee2b..8eee27627 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -181,7 +181,7 @@ public void onClick(final View v) { } if (inNeedOfSaslAccept()) { - mAccount.setKey(Account.PINNED_MECHANISM_KEY, String.valueOf(-1)); + mAccount.resetPinnedMechanism(); if (!xmppConnectionService.updateAccount(mAccount)) { Toast.makeText(EditAccountActivity.this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show(); } @@ -421,7 +421,7 @@ private void deleteAccountAndReturnIfNecessary() { } else { preset = jid.getDomain(); } - final Intent intent = SignupUtils.getTokenRegistrationIntent(this, preset, mAccount.getKey(Account.PRE_AUTH_REGISTRATION_TOKEN)); + final Intent intent = SignupUtils.getTokenRegistrationIntent(this, preset, mAccount.getKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN)); StartConversationActivity.addInviteUri(intent, getIntent()); startActivity(intent); return; @@ -892,7 +892,7 @@ public boolean onOptionsItemSelected(final MenuItem item) { } private boolean inNeedOfSaslAccept() { - return mAccount != null && mAccount.getLastErrorStatus() == Account.State.DOWNGRADE_ATTACK && mAccount.getKeyAsInt(Account.PINNED_MECHANISM_KEY, -1) >= 0 && !accountInfoEdited(); + return mAccount != null && mAccount.getLastErrorStatus() == Account.State.DOWNGRADE_ATTACK && mAccount.getPinnedMechanismPriority() >= 0 && !accountInfoEdited(); } private void shareBarcode() { diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 5bcc99f13..3bcec5a5a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -692,8 +692,7 @@ private boolean processSuccess(final Element success) Log.d( Config.LOGTAG, account.getJid().asBareJid().toString() + ": logged in (using " + version + ")"); - // TODO store mechanism name - account.setKey(Account.PINNED_MECHANISM_KEY, String.valueOf(saslMechanism.getPriority())); + account.setPinnedMechanism(saslMechanism); if (version == SaslMechanism.Version.SASL_2) { final String authorizationIdentifier = success.findChildContent("authorization-identifier"); @@ -1264,7 +1263,7 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio + mechanisms); throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } - final int pinnedMechanism = account.getKeyAsInt(Account.PINNED_MECHANISM_KEY, -1); + final int pinnedMechanism = account.getPinnedMechanismPriority(); if (pinnedMechanism > saslMechanism.getPriority()) { Log.e( Config.LOGTAG, @@ -1345,7 +1344,7 @@ private static Collection extractMechanisms(final Element stream) { } private void register() { - final String preAuth = account.getKey(Account.PRE_AUTH_REGISTRATION_TOKEN); + final String preAuth = account.getKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN); if (preAuth != null && features.invite()) { final IqPacket preAuthRequest = new IqPacket(IqPacket.TYPE.SET); preAuthRequest.addChild("preauth", Namespace.PARS).setAttribute("token", preAuth); From bf15070fef52edb494c3496e2f75921091e69802 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 15 Sep 2022 13:10:15 +0200 Subject: [PATCH 203/394] bump sasl2 namespace --- .../java/eu/siacs/conversations/xml/Namespace.java | 2 +- .../eu/siacs/conversations/xmpp/XmppConnection.java | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index e9f9639ec..819e5fb21 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -16,7 +16,7 @@ public final class Namespace { public static final String DATA = "jabber:x:data"; public static final String OOB = "jabber:x:oob"; public static final String SASL = "urn:ietf:params:xml:ns:xmpp-sasl"; - public static final String SASL_2 = "urn:xmpp:sasl:1"; + public static final String SASL_2 = "urn:xmpp:sasl:2"; public static final String CHANNEL_BINDING = "urn:xmpp:sasl-cb:0"; public static final String TLS = "urn:ietf:params:xml:ns:xmpp-tls"; public static final String PUBSUB = "http://jabber.org/protocol/pubsub"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 3bcec5a5a..d0ea1349f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -1187,7 +1187,7 @@ private void processStreamFeatures(final Tag currentTag) throws IOException { && account.isOptionSet(Account.OPTION_REGISTER)) { throw new StateChangingException(Account.State.REGISTRATION_NOT_SUPPORTED); } else if (Config.SASL_2_ENABLED - && this.streamFeatures.hasChild("mechanisms", Namespace.SASL_2) + && this.streamFeatures.hasChild("authentication", Namespace.SASL_2) && shouldAuthenticate && isSecure) { authenticate(SaslMechanism.Version.SASL_2); @@ -1230,8 +1230,12 @@ private void processStreamFeatures(final Tag currentTag) throws IOException { } private void authenticate(final SaslMechanism.Version version) throws IOException { - final Element element = - this.streamFeatures.findChild("mechanisms", SaslMechanism.namespace(version)); + final Element element; + if (version == SaslMechanism.Version.SASL) { + element = this.streamFeatures.findChild("mechanisms", Namespace.SASL); + } else { + element = this.streamFeatures.findChild("authentication", Namespace.SASL_2); + } final Collection mechanisms = Collections2.transform( Collections2.filter( From 5a3cca9554367ba0d4b9cb48990a57d5f69dcef1 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 15 Sep 2022 14:28:51 +0200 Subject: [PATCH 204/394] use bind 2 tag and sasl 2 user-agent --- .../conversations/utils/PhoneHelper.java | 68 +++++++++++++------ .../conversations/xmpp/XmppConnection.java | 19 ++++-- 2 files changed, 58 insertions(+), 29 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java index a894cab67..9ff492578 100644 --- a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java @@ -12,27 +12,51 @@ public class PhoneHelper { - @SuppressLint("HardwareIds") - public static String getAndroidId(Context context) { - return Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); - } + @SuppressLint("HardwareIds") + public static String getAndroidId(Context context) { + return Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); + } - public static Uri getProfilePictureUri(Context context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context.checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { - return null; - } - final String[] projection = new String[]{Profile._ID, Profile.PHOTO_URI}; - final Cursor cursor; - try { - cursor = context.getContentResolver().query(Profile.CONTENT_URI, projection, null, null, null); - } catch (Throwable e) { - return null; - } - if (cursor == null) { - return null; - } - final String uri = cursor.moveToFirst() ? cursor.getString(1) : null; - cursor.close(); - return uri == null ? null : Uri.parse(uri); - } + public static Uri getProfilePictureUri(Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + && context.checkSelfPermission(Manifest.permission.READ_CONTACTS) + != PackageManager.PERMISSION_GRANTED) { + return null; + } + final String[] projection = new String[] {Profile._ID, Profile.PHOTO_URI}; + final Cursor cursor; + try { + cursor = + context.getContentResolver() + .query(Profile.CONTENT_URI, projection, null, null, null); + } catch (Throwable e) { + return null; + } + if (cursor == null) { + return null; + } + final String uri = cursor.moveToFirst() ? cursor.getString(1) : null; + cursor.close(); + return uri == null ? null : Uri.parse(uri); + } + + public static boolean isEmulator() { + return (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) + || Build.FINGERPRINT.startsWith("generic") + || Build.FINGERPRINT.startsWith("unknown") + || Build.HARDWARE.contains("goldfish") + || Build.HARDWARE.contains("ranchu") + || Build.MODEL.contains("google_sdk") + || Build.MODEL.contains("Emulator") + || Build.MODEL.contains("Android SDK built for x86") + || Build.MANUFACTURER.contains("Genymotion") + || Build.PRODUCT.contains("sdk_google") + || Build.PRODUCT.contains("google_sdk") + || Build.PRODUCT.contains("sdk") + || Build.PRODUCT.contains("sdk_x86") + || Build.PRODUCT.contains("sdk_gphone64_arm64") + || Build.PRODUCT.contains("vbox86p") + || Build.PRODUCT.contains("emulator") + || Build.PRODUCT.contains("simulator"); + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index d0ea1349f..d4335db50 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -5,6 +5,7 @@ import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.os.Build; import android.os.SystemClock; import android.security.KeyChain; import android.util.Base64; @@ -59,6 +60,7 @@ import javax.net.ssl.X509KeyManager; import javax.net.ssl.X509TrustManager; +import eu.siacs.conversations.BuildConfig; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.XmppDomainVerifier; @@ -77,6 +79,7 @@ import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.Patterns; +import eu.siacs.conversations.utils.PhoneHelper; import eu.siacs.conversations.utils.Resolver; import eu.siacs.conversations.utils.SSLSocketHelper; import eu.siacs.conversations.utils.SocksSocketFactory; @@ -1292,6 +1295,14 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio if (!Strings.isNullOrEmpty(firstMessage)) { authenticate.addChild("initial-response").setContent(firstMessage); } + final Element userAgent = authenticate.addChild("user-agent"); + userAgent.setAttribute("id", account.getUuid()); + userAgent.addChild("software").setContent(mXmppConnectionService.getString(R.string.app_name)); + if (!PhoneHelper.isEmulator()) { + userAgent + .addChild("device") + .setContent(String.format("%s %s", Build.MANUFACTURER, Build.MODEL)); + } final Element inline = this.streamFeatures.findChild("inline", Namespace.SASL_2); final boolean inlineStreamManagement = inline != null && inline.hasChild("sm", "urn:xmpp:sm:3"); @@ -1330,9 +1341,7 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio private Element generateBindRequest(final Collection bindFeatures) { Log.d(Config.LOGTAG, "inline bind features: " + bindFeatures); final Element bind = new Element("bind", Namespace.BIND2); - final Element clientId = bind.addChild("client-id"); - clientId.setAttribute("tag", mXmppConnectionService.getString(R.string.app_name)); - clientId.setContent(account.getUuid()); + bind.addChild("tag").setContent(mXmppConnectionService.getString(R.string.app_name)); final Element features = bind.addChild("features"); if (bindFeatures.contains(Namespace.CARBONS)) { features.addChild("enable", Namespace.CARBONS); @@ -1343,10 +1352,6 @@ private Element generateBindRequest(final Collection bindFeatures) { return bind; } - private static Collection extractMechanisms(final Element stream) { - return Collections2.transform(stream.getChildren(), c -> c == null ? null : c.getContent()); - } - private void register() { final String preAuth = account.getKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN); if (preAuth != null && features.invite()) { From 9f5da6753929a1a368dabcb67f91c2cf461997f8 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 24 Sep 2022 11:59:53 +0200 Subject: [PATCH 205/394] use bind:0 namespace --- .../conversations/parser/MessageParser.java | 2 +- .../eu/siacs/conversations/xml/Namespace.java | 2 +- .../conversations/xmpp/XmppConnection.java | 32 +++++++++++-------- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 76945c472..46355354a 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -233,7 +233,7 @@ private void parseEvent(final Element event, final Jid from, final Account accou Element item = items.findChild("item"); Set deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received PEP device list " + deviceIds + " update from " + from + ", processing... "); - AxolotlService axolotlService = account.getAxolotlService(); + final AxolotlService axolotlService = account.getAxolotlService(); axolotlService.registerDevices(from, deviceIds); } else if (Namespace.BOOKMARKS.equals(node) && account.getJid().asBareJid().equals(from)) { if (account.getXmppConnection().getFeatures().bookmarksConversion()) { diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 819e5fb21..7c7edde6e 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -26,7 +26,7 @@ public final class Namespace { public static final String NICK = "http://jabber.org/protocol/nick"; public static final String FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL = "http://jabber.org/protocol/offline"; public static final String BIND = "urn:ietf:params:xml:ns:xmpp-bind"; - public static final String BIND2 = "urn:xmpp:bind2:1"; + public static final String BIND2 = "urn:xmpp:bind:0"; public static final String STREAM_MANAGEMENT = "urn:xmpp:sm:3"; public static final String CSI = "urn:xmpp:csi:0"; public static final String CARBONS = "urn:xmpp:carbons:2"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index d4335db50..5e4a6c0ed 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -60,7 +60,6 @@ import javax.net.ssl.X509KeyManager; import javax.net.ssl.X509TrustManager; -import eu.siacs.conversations.BuildConfig; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.XmppDomainVerifier; @@ -1233,16 +1232,16 @@ private void processStreamFeatures(final Tag currentTag) throws IOException { } private void authenticate(final SaslMechanism.Version version) throws IOException { - final Element element; + final Element authElement; if (version == SaslMechanism.Version.SASL) { - element = this.streamFeatures.findChild("mechanisms", Namespace.SASL); + authElement = this.streamFeatures.findChild("mechanisms", Namespace.SASL); } else { - element = this.streamFeatures.findChild("authentication", Namespace.SASL_2); + authElement = this.streamFeatures.findChild("authentication", Namespace.SASL_2); } final Collection mechanisms = Collections2.transform( Collections2.filter( - element.getChildren(), + authElement.getChildren(), c -> c != null && "mechanism".equals(c.getName())), c -> c == null ? null : c.getContent()); final Element cbElement = @@ -1297,24 +1296,29 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio } final Element userAgent = authenticate.addChild("user-agent"); userAgent.setAttribute("id", account.getUuid()); - userAgent.addChild("software").setContent(mXmppConnectionService.getString(R.string.app_name)); + userAgent + .addChild("software") + .setContent(mXmppConnectionService.getString(R.string.app_name)); if (!PhoneHelper.isEmulator()) { userAgent .addChild("device") .setContent(String.format("%s %s", Build.MANUFACTURER, Build.MODEL)); } - final Element inline = this.streamFeatures.findChild("inline", Namespace.SASL_2); + final Element inline = authElement.findChild("inline", Namespace.SASL_2); final boolean inlineStreamManagement = inline != null && inline.hasChild("sm", "urn:xmpp:sm:3"); - final boolean inlineBind2 = inline != null && inline.hasChild("bind", Namespace.BIND2); - final Element inlineBindFeatures = - this.streamFeatures.findChild("inline", Namespace.BIND2); - if (inlineBind2 && inlineBindFeatures != null) { + final Element inlineBind2 = + inline != null ? inline.findChild("bind", Namespace.BIND2) : null; + final Element inlineBind2Inline = + inlineBind2 != null ? inlineBind2.findChild("inline", Namespace.BIND2) : null; + if (inlineBind2 != null) { final Element bind = generateBindRequest( - Collections2.transform( - inlineBindFeatures.getChildren(), - c -> c == null ? null : c.getAttribute("var"))); + inlineBind2Inline == null + ? Collections.emptyList() + : Collections2.transform( + inlineBind2Inline.getChildren(), + c -> c == null ? null : c.getAttribute("var"))); authenticate.addChild(bind); } if (inlineStreamManagement && streamId != null) { From 126e8ef08cb1146976da157b20947466ed3e2303 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 24 Sep 2022 14:58:49 +0200 Subject: [PATCH 206/394] refactor sasl 2 authentication code --- .../conversations/xmpp/XmppConnection.java | 84 +++++++++++-------- 1 file changed, 48 insertions(+), 36 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 5e4a6c0ed..2ff1fc406 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -1290,43 +1290,10 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio authenticate.setContent(firstMessage); } } else if (version == SaslMechanism.Version.SASL_2) { - authenticate = new Element("authenticate", Namespace.SASL_2); - if (!Strings.isNullOrEmpty(firstMessage)) { - authenticate.addChild("initial-response").setContent(firstMessage); - } - final Element userAgent = authenticate.addChild("user-agent"); - userAgent.setAttribute("id", account.getUuid()); - userAgent - .addChild("software") - .setContent(mXmppConnectionService.getString(R.string.app_name)); - if (!PhoneHelper.isEmulator()) { - userAgent - .addChild("device") - .setContent(String.format("%s %s", Build.MANUFACTURER, Build.MODEL)); - } final Element inline = authElement.findChild("inline", Namespace.SASL_2); - final boolean inlineStreamManagement = - inline != null && inline.hasChild("sm", "urn:xmpp:sm:3"); - final Element inlineBind2 = - inline != null ? inline.findChild("bind", Namespace.BIND2) : null; - final Element inlineBind2Inline = - inlineBind2 != null ? inlineBind2.findChild("inline", Namespace.BIND2) : null; - if (inlineBind2 != null) { - final Element bind = - generateBindRequest( - inlineBind2Inline == null - ? Collections.emptyList() - : Collections2.transform( - inlineBind2Inline.getChildren(), - c -> c == null ? null : c.getAttribute("var"))); - authenticate.addChild(bind); - } - if (inlineStreamManagement && streamId != null) { - final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived); - this.mSmCatchupMessageCounter.set(0); - this.mWaitingForSmCatchup.set(true); - authenticate.addChild(resume); - } + final boolean sm = inline != null && inline.hasChild("sm", "urn:xmpp:sm:3"); + final Collection bindFeatures = bindFeatures(inline); + authenticate = generateAuthenticationRequest(firstMessage, bindFeatures, sm); } else { throw new AssertionError("Missing implementation for " + version); } @@ -1342,6 +1309,51 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio tagWriter.writeElement(authenticate); } + private static Collection bindFeatures(final Element inline) { + final Element inlineBind2 = + inline != null ? inline.findChild("bind", Namespace.BIND2) : null; + final Element inlineBind2Inline = + inlineBind2 != null ? inlineBind2.findChild("inline", Namespace.BIND2) : null; + if (inlineBind2 == null) { + return null; + } + if (inlineBind2Inline == null) { + return Collections.emptyList(); + } + return Collections2.transform( + inlineBind2Inline.getChildren(), c -> c == null ? null : c.getAttribute("var")); + } + + private Element generateAuthenticationRequest( + final String firstMessage, + final Collection bind, + final boolean inlineStreamManagement) { + final Element authenticate = new Element("authenticate", Namespace.SASL_2); + if (!Strings.isNullOrEmpty(firstMessage)) { + authenticate.addChild("initial-response").setContent(firstMessage); + } + final Element userAgent = authenticate.addChild("user-agent"); + userAgent.setAttribute("id", account.getUuid()); + userAgent + .addChild("software") + .setContent(mXmppConnectionService.getString(R.string.app_name)); + if (!PhoneHelper.isEmulator()) { + userAgent + .addChild("device") + .setContent(String.format("%s %s", Build.MANUFACTURER, Build.MODEL)); + } + if (bind != null) { + authenticate.addChild(generateBindRequest(bind)); + } + if (inlineStreamManagement && streamId != null) { + final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived); + this.mSmCatchupMessageCounter.set(0); + this.mWaitingForSmCatchup.set(true); + authenticate.addChild(resume); + } + return authenticate; + } + private Element generateBindRequest(final Collection bindFeatures) { Log.d(Config.LOGTAG, "inline bind features: " + bindFeatures); final Element bind = new Element("bind", Namespace.BIND2); From 32f9a58d9ab5b2b88d433687d45bc8c9ecc0d43d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 25 Sep 2022 14:13:04 +0200 Subject: [PATCH 207/394] pipeline sasl2 directly after stream start --- .../siacs/conversations/entities/Account.java | 4 +- .../conversations/ui/EditAccountActivity.java | 2 - .../eu/siacs/conversations/xml/Namespace.java | 1 + .../eu/siacs/conversations/xml/TagWriter.java | 11 +- .../conversations/xmpp/XmppConnection.java | 112 ++++++++++++------ .../siacs/conversations/xmpp/bind/Bind2.java | 33 ++++++ 6 files changed, 121 insertions(+), 42 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/xmpp/bind/Bind2.java diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 8446abbbd..bbfacf420 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -57,16 +57,14 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable public static final String PINNED_CHANNEL_BINDING = "pinned_channel_binding"; - public static final int OPTION_USETLS = 0; public static final int OPTION_DISABLED = 1; public static final int OPTION_REGISTER = 2; - public static final int OPTION_USECOMPRESSION = 3; public static final int OPTION_MAGIC_CREATE = 4; public static final int OPTION_REQUIRES_ACCESS_MODE_CHANGE = 5; public static final int OPTION_LOGGED_IN_SUCCESSFULLY = 6; public static final int OPTION_HTTP_UPLOAD_AVAILABLE = 7; - public static final int OPTION_UNVERIFIED = 8; public static final int OPTION_FIXED_USERNAME = 9; + public static final int OPTION_QUICKSTART_AVAILABLE = 10; private static final String KEY_PGP_SIGNATURE = "pgp_signature"; private static final String KEY_PGP_ID = "pgp_id"; diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 8eee27627..8959d4c38 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -286,8 +286,6 @@ public void onClick(final View v) { mAccount = new Account(jid.asBareJid(), password); mAccount.setPort(numericPort); mAccount.setHostname(hostname); - mAccount.setOption(Account.OPTION_USETLS, true); - mAccount.setOption(Account.OPTION_USECOMPRESSION, true); mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount); xmppConnectionService.createAccount(mAccount); } diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 7c7edde6e..d17891ff2 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -1,6 +1,7 @@ package eu.siacs.conversations.xml; public final class Namespace { + public static final String STREAMS = "http://etherx.jabber.org/streams"; public static final String DISCO_ITEMS = "http://jabber.org/protocol/disco#items"; public static final String DISCO_INFO = "http://jabber.org/protocol/disco#info"; public static final String EXTERNAL_SERVICE_DISCOVERY = "urn:xmpp:extdisco:2"; diff --git a/src/main/java/eu/siacs/conversations/xml/TagWriter.java b/src/main/java/eu/siacs/conversations/xml/TagWriter.java index 4f429377a..5a9f3317c 100644 --- a/src/main/java/eu/siacs/conversations/xml/TagWriter.java +++ b/src/main/java/eu/siacs/conversations/xml/TagWriter.java @@ -58,15 +58,20 @@ public void beginDocument() throws IOException { throw new IOException("output stream was null"); } outputStream.write(""); - outputStream.flush(); } - public synchronized void writeTag(Tag tag) throws IOException { + public void writeTag(final Tag tag) throws IOException { + writeTag(tag, true); + } + + public synchronized void writeTag(final Tag tag, final boolean flush) throws IOException { if (outputStream == null) { throw new IOException("output stream was null"); } outputStream.write(tag.toString()); - outputStream.flush(); + if (flush) { + outputStream.flush(); + } } public synchronized void writeElement(Element element) throws IOException { diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 2ff1fc406..911b22686 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -89,6 +89,7 @@ import eu.siacs.conversations.xml.Tag; import eu.siacs.conversations.xml.TagWriter; import eu.siacs.conversations.xml.XmlReader; +import eu.siacs.conversations.xmpp.bind.Bind2; import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; @@ -155,6 +156,7 @@ public class XmppConnection implements Runnable { private TagWriter tagWriter = new TagWriter(); private boolean shouldAuthenticate = true; private boolean inSmacksSession = false; + private boolean quickStartInProgress = false; private boolean isBound = false; private Element streamFeatures; private String streamId = null; @@ -270,11 +272,11 @@ protected void connect() { } Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": connecting"); features.encryptionEnabled = false; - inSmacksSession = false; - isBound = false; + this.inSmacksSession = false; + this.quickStartInProgress = false; + this.isBound = false; this.attempt++; - this.verifiedHostname = - null; // will be set if user entered hostname is being used or hostname was verified + this.verifiedHostname = null; // will be set if user entered hostname is being used or hostname was verified // with dnssec try { Socket localSocket; @@ -310,14 +312,14 @@ protected void connect() { try { startXmpp(localSocket); - } catch (InterruptedException e) { + } catch (final InterruptedException e) { Log.d( Config.LOGTAG, account.getJid().asBareJid() + ": thread was interrupted before beginning stream"); return; - } catch (Exception e) { - throw new IOException(e.getMessage()); + } catch (final Exception e) { + throw new IOException("Could not start stream", e); } } else { final String domain = account.getServer(); @@ -477,7 +479,7 @@ protected void connect() { * * @return true if server returns with valid xmpp, false otherwise */ - private boolean startXmpp(Socket socket) throws Exception { + private boolean startXmpp(final Socket socket) throws Exception { if (Thread.currentThread().isInterrupted()) { throw new InterruptedException(); } @@ -490,15 +492,22 @@ private boolean startXmpp(Socket socket) throws Exception { tagWriter.setOutputStream(socket.getOutputStream()); tagReader.setInputStream(socket.getInputStream()); tagWriter.beginDocument(); - sendStartStream(); + final boolean quickStart; + if (socket instanceof SSLSocket) { + SSLSocketHelper.log(account, (SSLSocket) socket); + quickStart = establishStream(true); + } else { + quickStart = establishStream(false); + } final Tag tag = tagReader.readTag(); if (Thread.currentThread().isInterrupted()) { throw new InterruptedException(); } - if (socket instanceof SSLSocket) { - SSLSocketHelper.log(account, (SSLSocket) socket); + final boolean success = tag != null && tag.isStart("stream", Namespace.STREAMS); + if (success && quickStart) { + this.quickStartInProgress = true; } - return tag != null && tag.isStart("stream"); + return success; } private SSLSocketFactory getSSLSocketFactory() @@ -761,11 +770,12 @@ private boolean processSuccess(final Element success) sendPostBindInitialization(streamManagementEnabled != null, carbonsEnabled != null); } } + this.quickStartInProgress = false; if (version == SaslMechanism.Version.SASL) { tagReader.reset(); - sendStartStream(); + sendStartStream(true); final Tag tag = tagReader.readTag(); - if (tag != null && tag.isStart("stream")) { + if (tag != null && tag.isStart("stream", Namespace.STREAMS)) { processStream(); return true; } else { @@ -1119,11 +1129,14 @@ private void switchOverToTls() throws XmlPullParserException, IOException { final SSLSocket sslSocket = upgradeSocketToTls(socket); tagReader.setInputStream(sslSocket.getInputStream()); tagWriter.setOutputStream(sslSocket.getOutputStream()); - sendStartStream(); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS connection established"); + final boolean quickStart = establishStream(true); + if (quickStart) { + this.quickStartInProgress = true; + } features.encryptionEnabled = true; final Tag tag = tagReader.readTag(); - if (tag != null && tag.isStart("stream")) { + if (tag != null && tag.isStart("stream", Namespace.STREAMS)) { SSLSocketHelper.log(account, sslSocket); processStream(); } else { @@ -1170,7 +1183,13 @@ private void processStreamFeatures(final Tag currentTag) throws IOException { final boolean isSecure = features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion(); final boolean needsBinding = !isBound && !account.isOptionSet(Account.OPTION_REGISTER); - if (this.streamFeatures.hasChild("starttls", Namespace.TLS) + if (this.quickStartInProgress) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": quick start in progress. ignoring features: " + + XmlHelper.printElementNames(this.streamFeatures)); + } else if (this.streamFeatures.hasChild("starttls", Namespace.TLS) && !features.encryptionEnabled) { sendStartTLS(); } else if (this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE) @@ -1238,6 +1257,7 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio } else { authElement = this.streamFeatures.findChild("authentication", Namespace.SASL_2); } + //TODO externalize final Collection mechanisms = Collections2.transform( Collections2.filter( @@ -1261,6 +1281,8 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio final SaslMechanism.Factory factory = new SaslMechanism.Factory(account); this.saslMechanism = factory.of(mechanisms, channelBindings); + //TODO externalize checks + if (saslMechanism == null) { Log.d( Config.LOGTAG, @@ -1282,6 +1304,7 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio + "). Possible downgrade attack?"); throw new StateChangingException(Account.State.DOWNGRADE_ATTACK); } + final boolean quickStartAvailable; final String firstMessage = saslMechanism.getClientFirstMessage(); final Element authenticate; if (version == SaslMechanism.Version.SASL) { @@ -1289,15 +1312,24 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio if (!Strings.isNullOrEmpty(firstMessage)) { authenticate.setContent(firstMessage); } + quickStartAvailable = false; } else if (version == SaslMechanism.Version.SASL_2) { final Element inline = authElement.findChild("inline", Namespace.SASL_2); final boolean sm = inline != null && inline.hasChild("sm", "urn:xmpp:sm:3"); - final Collection bindFeatures = bindFeatures(inline); + final Collection bindFeatures = Bind2.features(inline); + quickStartAvailable = + sm + && bindFeatures != null + && bindFeatures.containsAll(Bind2.QUICKSTART_FEATURES); authenticate = generateAuthenticationRequest(firstMessage, bindFeatures, sm); } else { throw new AssertionError("Missing implementation for " + version); } + if (account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, quickStartAvailable)) { + mXmppConnectionService.updateAccount(account); + } + Log.d( Config.LOGTAG, account.getJid().toString() @@ -1309,19 +1341,8 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio tagWriter.writeElement(authenticate); } - private static Collection bindFeatures(final Element inline) { - final Element inlineBind2 = - inline != null ? inline.findChild("bind", Namespace.BIND2) : null; - final Element inlineBind2Inline = - inlineBind2 != null ? inlineBind2.findChild("inline", Namespace.BIND2) : null; - if (inlineBind2 == null) { - return null; - } - if (inlineBind2Inline == null) { - return Collections.emptyList(); - } - return Collections2.transform( - inlineBind2Inline.getChildren(), c -> c == null ? null : c.getAttribute("var")); + private Element generateAuthenticationRequest(final String firstMessage) { + return generateAuthenticationRequest(firstMessage, Bind2.QUICKSTART_FEATURES, true); } private Element generateAuthenticationRequest( @@ -1988,14 +2009,37 @@ private void failPendingMessages(final String error) { } } - private void sendStartStream() throws IOException { + private boolean establishStream(final boolean secureConnection) throws IOException { + final SaslMechanism saslMechanism = account.getPinnedMechanism(); + if (secureConnection + && saslMechanism != null + && account.isOptionSet(Account.OPTION_QUICKSTART_AVAILABLE)) { + this.saslMechanism = saslMechanism; + final Element authenticate = + generateAuthenticationRequest(saslMechanism.getClientFirstMessage()); + authenticate.setAttribute("mechanism", saslMechanism.getMechanism()); + sendStartStream(false); + tagWriter.writeElement(authenticate); + Log.d( + Config.LOGTAG, + account.getJid().toString() + + ": quick start with " + + saslMechanism.getMechanism()); + return true; + } else { + sendStartStream(true); + return false; + } + } + + private void sendStartStream(final boolean flush) throws IOException { final Tag stream = Tag.start("stream:stream"); stream.setAttribute("to", account.getServer()); stream.setAttribute("version", "1.0"); stream.setAttribute("xml:lang", LocalizedContent.STREAM_LANGUAGE); stream.setAttribute("xmlns", "jabber:client"); - stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams"); - tagWriter.writeTag(stream); + stream.setAttribute("xmlns:stream", Namespace.STREAMS); + tagWriter.writeTag(stream, flush); } private String createNewResource() { diff --git a/src/main/java/eu/siacs/conversations/xmpp/bind/Bind2.java b/src/main/java/eu/siacs/conversations/xmpp/bind/Bind2.java new file mode 100644 index 000000000..21c957a0f --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/bind/Bind2.java @@ -0,0 +1,33 @@ +package eu.siacs.conversations.xmpp.bind; + +import com.google.common.collect.Collections2; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Namespace; + +public class Bind2 { + + public static final Collection QUICKSTART_FEATURES = Arrays.asList( + Namespace.CARBONS, + Namespace.STREAM_MANAGEMENT + ); + + public static Collection features(final Element inline) { + final Element inlineBind2 = + inline != null ? inline.findChild("bind", Namespace.BIND2) : null; + final Element inlineBind2Inline = + inlineBind2 != null ? inlineBind2.findChild("inline", Namespace.BIND2) : null; + if (inlineBind2 == null) { + return null; + } + if (inlineBind2Inline == null) { + return Collections.emptyList(); + } + return Collections2.transform( + inlineBind2Inline.getChildren(), c -> c == null ? null : c.getAttribute("var")); + } +} From 717aeddb82e25fed0b4dc4591985f762cb81536d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 25 Sep 2022 15:18:45 +0200 Subject: [PATCH 208/394] fix last commit. bring back option required by quicksy --- src/main/java/eu/siacs/conversations/entities/Account.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index bbfacf420..1817c24bb 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -63,6 +63,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable public static final int OPTION_REQUIRES_ACCESS_MODE_CHANGE = 5; public static final int OPTION_LOGGED_IN_SUCCESSFULLY = 6; public static final int OPTION_HTTP_UPLOAD_AVAILABLE = 7; + public static final int OPTION_UNVERIFIED = 8; public static final int OPTION_FIXED_USERNAME = 9; public static final int OPTION_QUICKSTART_AVAILABLE = 10; From 3d56d01826e53ce5bb5b6450d99a195957a40626 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 26 Sep 2022 07:53:48 +0200 Subject: [PATCH 209/394] handle case when server loses support for quick start --- .../conversations/xmpp/XmppConnection.java | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 911b22686..e753e4b39 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -898,6 +898,10 @@ private void processResumed(final Element resumed) throws StateChangingException } sendPacket(packet); } + changeStatusToOnline(); + } + + private void changeStatusToOnline() { Log.d( Config.LOGTAG, account.getJid().asBareJid() + ": online with resource " + account.getResource()); @@ -1184,12 +1188,20 @@ private void processStreamFeatures(final Tag currentTag) throws IOException { features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion(); final boolean needsBinding = !isBound && !account.isOptionSet(Account.OPTION_REGISTER); if (this.quickStartInProgress) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": quick start in progress. ignoring features: " - + XmlHelper.printElementNames(this.streamFeatures)); - } else if (this.streamFeatures.hasChild("starttls", Namespace.TLS) + if (this.streamFeatures.hasChild("authentication", Namespace.SASL_2)) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": quick start in progress. ignoring features: " + + XmlHelper.printElementNames(this.streamFeatures)); + return; + } + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": server lost support for SASL 2. quick start not possible"); + this.account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, false); + mXmppConnectionService.updateAccount(account); + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } + if (this.streamFeatures.hasChild("starttls", Namespace.TLS) && !features.encryptionEnabled) { sendStartTLS(); } else if (this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE) @@ -1878,13 +1890,10 @@ public boolean isMamPreferenceAlways() { } private void finalizeBind() { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": online with resource " + account.getResource()); if (bindListener != null) { bindListener.onBind(account); } - changeStatus(Account.State.ONLINE); + changeStatusToOnline(); } private void enableAdvancedStreamFeatures() { @@ -2012,6 +2021,7 @@ private void failPendingMessages(final String error) { private boolean establishStream(final boolean secureConnection) throws IOException { final SaslMechanism saslMechanism = account.getPinnedMechanism(); if (secureConnection + && Config.SASL_2_ENABLED && saslMechanism != null && account.isOptionSet(Account.OPTION_QUICKSTART_AVAILABLE)) { this.saslMechanism = saslMechanism; From cb775ece992a8ded54c4cccae484475edc548803 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 26 Sep 2022 09:47:53 +0200 Subject: [PATCH 210/394] wait for DB restore before bind --- .../services/XmppConnectionService.java | 12 +++++----- .../conversations/xmpp/XmppConnection.java | 23 ++++++++++++++++--- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index e4af37947..483016364 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1939,7 +1939,7 @@ private void restoreFromDatabase() { databaseBackend.expireOldMessages(deletionDate); } Log.d(Config.LOGTAG, "restoring roster..."); - for (Account account : accounts) { + for (final Account account : accounts) { databaseBackend.readRoster(account.getRoster()); account.initAccountServices(XmppConnectionService.this); //roster needs to be loaded at this stage } @@ -1977,11 +1977,11 @@ private void restoreMessages(Conversation conversation) { public void loadPhoneContacts() { mContactMergerExecutor.execute(() -> { - Map contacts = JabberIdContact.load(this); + final Map contacts = JabberIdContact.load(this); Log.d(Config.LOGTAG, "start merging phone contacts with roster"); - for (Account account : accounts) { - List withSystemAccounts = account.getRoster().getWithSystemAccounts(JabberIdContact.class); - for (JabberIdContact jidContact : contacts.values()) { + for (final Account account : accounts) { + final List withSystemAccounts = account.getRoster().getWithSystemAccounts(JabberIdContact.class); + for (final JabberIdContact jidContact : contacts.values()) { final Contact contact = account.getRoster().getContact(jidContact.getJid()); boolean needsCacheClean = contact.setPhoneContact(jidContact); if (needsCacheClean) { @@ -1989,7 +1989,7 @@ public void loadPhoneContacts() { } withSystemAccounts.remove(contact); } - for (Contact contact : withSystemAccounts) { + for (final Contact contact : withSystemAccounts) { boolean needsCacheClean = contact.unsetPhoneContact(JabberIdContact.class); if (needsCacheClean) { getAvatarService().clear(contact); diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index e753e4b39..2ba2daea0 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -753,6 +753,7 @@ private boolean processSuccess(final Element success) processFailed(failed, false); // wait for new stream features } if (bound != null) { + clearIqCallbacks(); this.isBound = true; final Element streamManagementEnabled = bound.findChild("enabled", Namespace.STREAM_MANAGEMENT); @@ -1134,7 +1135,12 @@ private void switchOverToTls() throws XmlPullParserException, IOException { tagReader.setInputStream(sslSocket.getInputStream()); tagWriter.setOutputStream(sslSocket.getOutputStream()); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS connection established"); - final boolean quickStart = establishStream(true); + final boolean quickStart; + try { + quickStart = establishStream(true); + } catch (final InterruptedException e) { + return; + } if (quickStart) { this.quickStartInProgress = true; } @@ -1333,6 +1339,17 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio sm && bindFeatures != null && bindFeatures.containsAll(Bind2.QUICKSTART_FEATURES); + if (bindFeatures != null) { + try { + mXmppConnectionService.restoredFromDatabaseLatch.await(); + } catch (final InterruptedException e) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": interrupted while waiting for DB restore during SASL2 bind"); + return; + } + } authenticate = generateAuthenticationRequest(firstMessage, bindFeatures, sm); } else { throw new AssertionError("Missing implementation for " + version); @@ -1876,7 +1893,6 @@ private void discoverCommands() { } } } - Log.d(Config.LOGTAG, commands.toString()); synchronized (this.commands) { this.commands.clear(); this.commands.putAll(commands); @@ -2018,12 +2034,13 @@ private void failPendingMessages(final String error) { } } - private boolean establishStream(final boolean secureConnection) throws IOException { + private boolean establishStream(final boolean secureConnection) throws IOException, InterruptedException { final SaslMechanism saslMechanism = account.getPinnedMechanism(); if (secureConnection && Config.SASL_2_ENABLED && saslMechanism != null && account.isOptionSet(Account.OPTION_QUICKSTART_AVAILABLE)) { + mXmppConnectionService.restoredFromDatabaseLatch.await(); this.saslMechanism = saslMechanism; final Element authenticate = generateAuthenticationRequest(saslMechanism.getClientFirstMessage()); From 10f30faf551e630315cdc9f9717be9b85929b15a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 1 Oct 2022 09:21:38 +0200 Subject: [PATCH 211/394] revert transcoder to 0.9.1 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 063dac0ce..1817807b7 100644 --- a/build.gradle +++ b/build.gradle @@ -60,7 +60,7 @@ dependencies { implementation 'org.whispersystems:signal-protocol-java:2.6.2' implementation 'com.makeramen:roundedimageview:2.3.0' implementation "com.wefika:flowlayout:0.4.1" - implementation 'com.otaliastudios:transcoder:0.10.4' + implementation 'com.otaliastudios:transcoder:0.9.1' implementation 'org.jxmpp:jxmpp-jid:1.0.3' implementation 'org.osmdroid:osmdroid-android:6.1.11' From 64b853f3acefc7151ecb8b74856ab7c9a8d55271 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 1 Oct 2022 09:25:41 +0200 Subject: [PATCH 212/394] bump various dependencies --- build.gradle | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 1817807b7..27e620963 100644 --- a/build.gradle +++ b/build.gradle @@ -33,7 +33,7 @@ configurations { dependencies { implementation 'androidx.viewpager:viewpager:1.0.0' - playstoreImplementation('com.google.firebase:firebase-messaging:23.0.7') { + playstoreImplementation('com.google.firebase:firebase-messaging:23.0.8') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' @@ -42,11 +42,11 @@ dependencies { quicksyPlaystoreImplementation 'com.google.android.gms:play-services-auth-api-phone:18.0.1' implementation 'org.sufficientlysecure:openpgp-api:10.0' implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0' - implementation 'androidx.appcompat:appcompat:1.5.0' + implementation 'androidx.appcompat:appcompat:1.5.1' implementation 'androidx.exifinterface:exifinterface:1.3.3' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' - implementation 'com.google.android.material:material:1.4.0' + implementation 'com.google.android.material:material:1.6.1' implementation "androidx.emoji2:emoji2:1.2.0" freeImplementation "androidx.emoji2:emoji2-bundled:1.2.0" @@ -60,6 +60,7 @@ dependencies { implementation 'org.whispersystems:signal-protocol-java:2.6.2' implementation 'com.makeramen:roundedimageview:2.3.0' implementation "com.wefika:flowlayout:0.4.1" + //noinspection GradleDependency implementation 'com.otaliastudios:transcoder:0.9.1' implementation 'org.jxmpp:jxmpp-jid:1.0.3' From 5735bca517a798a1ef6ffa87dc5882b69888cbdd Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 1 Oct 2022 09:26:07 +0200 Subject: [PATCH 213/394] minor code clean up --- .../java/eu/siacs/conversations/persistance/FileBackend.java | 5 ++--- .../java/eu/siacs/conversations/ui/RtpSessionActivity.java | 3 +++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 2d5496f2f..8519e5dbd 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -1602,7 +1602,6 @@ private Dimensions getVideoDimensions(File file) throws NotAVideoFile { return getVideoDimensions(metadataRetriever); } - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) private Dimensions getPdfDocumentDimensions(final File file) { final ParcelFileDescriptor fileDescriptor; try { @@ -1610,7 +1609,7 @@ private Dimensions getPdfDocumentDimensions(final File file) { if (fileDescriptor == null) { return new Dimensions(0, 0); } - } catch (FileNotFoundException e) { + } catch (final FileNotFoundException e) { return new Dimensions(0, 0); } try { @@ -1621,7 +1620,7 @@ private Dimensions getPdfDocumentDimensions(final File file) { page.close(); pdfRenderer.close(); return scalePdfDimensions(new Dimensions(height, width)); - } catch (IOException | SecurityException e) { + } catch (final IOException | SecurityException e) { Log.d(Config.LOGTAG, "unable to get dimensions for pdf document", e); return new Dimensions(0, 0); } diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index e73fdb23c..302fbf81d 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -521,6 +521,9 @@ public void onRequestPermissionsResult( @StringRes int res; final String firstDenied = getFirstDenied(permissionResult.grantResults, permissionResult.permissions); + if (firstDenied == null) { + return; + } if (Manifest.permission.RECORD_AUDIO.equals(firstDenied)) { res = R.string.no_microphone_permission; } else if (Manifest.permission.CAMERA.equals(firstDenied)) { From d435c1f2aef1454141d4f5099224b5a03d579dba Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 1 Oct 2022 11:26:52 +0200 Subject: [PATCH 214/394] let omemoOnly config overwrite OmemoSetting --- .../java/eu/siacs/conversations/Config.java | 7 +++- .../conversations/crypto/OmemoSetting.java | 10 ++++- .../conversations/ui/SettingsActivity.java | 39 +++++++++++-------- src/main/res/xml/preferences.xml | 3 +- 4 files changed, 39 insertions(+), 20 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index a3eacc9db..47226eb6e 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -15,10 +15,9 @@ public final class Config { private static final int UNENCRYPTED = 1; private static final int OPENPGP = 2; - private static final int OTR = 4; private static final int OMEMO = 8; - private static final int ENCRYPTION_MASK = UNENCRYPTED | OPENPGP | OTR | OMEMO; + private static final int ENCRYPTION_MASK = UNENCRYPTED | OPENPGP | OMEMO; public static boolean supportUnencrypted() { return (ENCRYPTION_MASK & UNENCRYPTED) != 0; @@ -32,6 +31,10 @@ public static boolean supportOmemo() { return (ENCRYPTION_MASK & OMEMO) != 0; } + public static boolean omemoOnly() { + return !multipleEncryptionChoices() && supportOmemo(); + } + public static boolean multipleEncryptionChoices() { return (ENCRYPTION_MASK & (ENCRYPTION_MASK - 1)) != 0; } diff --git a/src/main/java/eu/siacs/conversations/crypto/OmemoSetting.java b/src/main/java/eu/siacs/conversations/crypto/OmemoSetting.java index 5326ecae0..a531c39f3 100644 --- a/src/main/java/eu/siacs/conversations/crypto/OmemoSetting.java +++ b/src/main/java/eu/siacs/conversations/crypto/OmemoSetting.java @@ -34,6 +34,9 @@ import android.content.SharedPreferences; import android.preference.PreferenceManager; +import com.google.common.base.Strings; + +import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.ui.SettingsActivity; @@ -52,8 +55,13 @@ public static int getEncryption() { } public static void load(final Context context, final SharedPreferences sharedPreferences) { + if (Config.omemoOnly()) { + always = true; + encryption = Message.ENCRYPTION_AXOLOTL; + return; + } final String value = sharedPreferences.getString(SettingsActivity.OMEMO_SETTING, context.getResources().getString(R.string.omemo_setting_default)); - switch (value) { + switch (Strings.nullToEmpty(value)) { case "always": always = true; encryption = Message.ENCRYPTION_AXOLOTL; diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java index 21d2b956c..5e21e0b26 100644 --- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java @@ -298,26 +298,33 @@ public void onStart() { deleteOmemoPreference.setOnPreferenceClickListener( preference -> deleteOmemoIdentities()); } + if (Config.omemoOnly()) { + final PreferenceCategory privacyCategory = + (PreferenceCategory) mSettingsFragment.findPreference("privacy"); + final Preference omemoPreference =mSettingsFragment.findPreference(OMEMO_SETTING); + if (omemoPreference != null) { + privacyCategory.removePreference(omemoPreference); + } + } } private void changeOmemoSettingSummary() { - ListPreference omemoPreference = + final ListPreference omemoPreference = (ListPreference) mSettingsFragment.findPreference(OMEMO_SETTING); - if (omemoPreference != null) { - String value = omemoPreference.getValue(); - switch (value) { - case "always": - omemoPreference.setSummary(R.string.pref_omemo_setting_summary_always); - break; - case "default_on": - omemoPreference.setSummary(R.string.pref_omemo_setting_summary_default_on); - break; - case "default_off": - omemoPreference.setSummary(R.string.pref_omemo_setting_summary_default_off); - break; - } - } else { - Log.d(Config.LOGTAG, "unable to find preference named " + OMEMO_SETTING); + if (omemoPreference == null) { + return; + } + final String value = omemoPreference.getValue(); + switch (value) { + case "always": + omemoPreference.setSummary(R.string.pref_omemo_setting_summary_always); + break; + case "default_on": + omemoPreference.setSummary(R.string.pref_omemo_setting_summary_default_on); + break; + case "default_off": + omemoPreference.setSummary(R.string.pref_omemo_setting_summary_default_off); + break; } } diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml index 91b07210c..b46155836 100644 --- a/src/main/res/xml/preferences.xml +++ b/src/main/res/xml/preferences.xml @@ -15,7 +15,8 @@ android:targetPackage="com.huawei.systemmanager" /> - + Date: Wed, 12 Oct 2022 11:53:57 +0200 Subject: [PATCH 215/394] only run account options through int conversion. fixes #4390 --- .../siacs/conversations/services/ExportBackupService.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/ExportBackupService.java b/src/main/java/eu/siacs/conversations/services/ExportBackupService.java index 4e144f223..9826ecbc2 100644 --- a/src/main/java/eu/siacs/conversations/services/ExportBackupService.java +++ b/src/main/java/eu/siacs/conversations/services/ExportBackupService.java @@ -110,11 +110,9 @@ private static void accountExport(final SQLiteDatabase db, final String uuid, fi final String value = accountCursor.getString(i); if (value == null || Account.ROSTERVERSION.equals(accountCursor.getColumnName(i))) { builder.append("NULL"); - } else if (value.matches("\\d+")) { + } else if (Account.OPTIONS.equals(accountCursor.getColumnName(i)) && value.matches("\\d+")) { int intValue = Integer.parseInt(value); - if (Account.OPTIONS.equals(accountCursor.getColumnName(i))) { - intValue |= 1 << Account.OPTION_DISABLED; - } + intValue |= 1 << Account.OPTION_DISABLED; builder.append(intValue); } else { appendEscapedSQLString(builder, value); From ab0ea7096e57cdcc3f1056c7f8191680017ddbfa Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 12 Oct 2022 14:47:02 +0200 Subject: [PATCH 216/394] make it easier to disable muclumbus in Config --- .../services/ChannelDiscoveryService.java | 208 +++++++++++------- .../ui/ChannelDiscoveryActivity.java | 5 + .../conversations/ui/SettingsActivity.java | 18 +- 3 files changed, 142 insertions(+), 89 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java b/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java index c46568c3e..2f9553bfc 100644 --- a/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java +++ b/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java @@ -4,6 +4,7 @@ import androidx.annotation.NonNull; +import com.google.common.base.Strings; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; @@ -39,7 +40,6 @@ public class ChannelDiscoveryService { private final XmppConnectionService service; - private MuclumbusService muclumbusService; private final Cache> cache; @@ -50,16 +50,21 @@ public class ChannelDiscoveryService { } void initializeMuclumbusService() { + if (Strings.isNullOrEmpty(Config.CHANNEL_DISCOVERY)) { + this.muclumbusService = null; + return; + } final OkHttpClient.Builder builder = HttpConnectionManager.OK_HTTP_CLIENT.newBuilder(); if (service.useTorToConnect()) { builder.proxy(HttpConnectionManager.getProxy()); } - Retrofit retrofit = new Retrofit.Builder() - .client(builder.build()) - .baseUrl(Config.CHANNEL_DISCOVERY) - .addConverterFactory(GsonConverterFactory.create()) - .callbackExecutor(Executors.newSingleThreadExecutor()) - .build(); + final Retrofit retrofit = + new Retrofit.Builder() + .client(builder.build()) + .baseUrl(Config.CHANNEL_DISCOVERY) + .addConverterFactory(GsonConverterFactory.create()) + .callbackExecutor(Executors.newSingleThreadExecutor()) + .build(); this.muclumbusService = retrofit.create(MuclumbusService.class); } @@ -67,7 +72,10 @@ void cleanCache() { cache.invalidateAll(); } - void discover(@NonNull final String query, Method method, OnChannelSearchResultsFound onChannelSearchResultsFound) { + void discover( + @NonNull final String query, + Method method, + OnChannelSearchResultsFound onChannelSearchResultsFound) { final List result = cache.getIfPresent(key(method, query)); if (result != null) { onChannelSearchResultsFound.onChannelSearchResultsFound(result); @@ -84,59 +92,82 @@ void discover(@NonNull final String query, Method method, OnChannelSearchResults } } - private void discoverChannelsJabberNetwork(OnChannelSearchResultsFound listener) { - Call call = muclumbusService.getRooms(1); - try { - call.enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - final MuclumbusService.Rooms body = response.body(); - if (body == null) { - listener.onChannelSearchResultsFound(Collections.emptyList()); - logError(response); - return; + private void discoverChannelsJabberNetwork(final OnChannelSearchResultsFound listener) { + if (muclumbusService == null) { + listener.onChannelSearchResultsFound(Collections.emptyList()); + return; + } + final Call call = muclumbusService.getRooms(1); + call.enqueue( + new Callback() { + @Override + public void onResponse( + @NonNull Call call, + @NonNull Response response) { + final MuclumbusService.Rooms body = response.body(); + if (body == null) { + listener.onChannelSearchResultsFound(Collections.emptyList()); + logError(response); + return; + } + cache.put(key(Method.JABBER_NETWORK, ""), body.items); + listener.onChannelSearchResultsFound(body.items); } - cache.put(key(Method.JABBER_NETWORK, ""), body.items); - listener.onChannelSearchResultsFound(body.items); - } - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable throwable) { - Log.d(Config.LOGTAG, "Unable to query muclumbus on " + Config.CHANNEL_DISCOVERY, throwable); - listener.onChannelSearchResultsFound(Collections.emptyList()); - } - }); - } catch (Exception e) { - e.printStackTrace(); - } + @Override + public void onFailure( + @NonNull Call call, + @NonNull Throwable throwable) { + Log.d( + Config.LOGTAG, + "Unable to query muclumbus on " + Config.CHANNEL_DISCOVERY, + throwable); + listener.onChannelSearchResultsFound(Collections.emptyList()); + } + }); } - private void discoverChannelsJabberNetwork(final String query, OnChannelSearchResultsFound listener) { - MuclumbusService.SearchRequest searchRequest = new MuclumbusService.SearchRequest(query); - Call searchResultCall = muclumbusService.search(searchRequest); - - searchResultCall.enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - final MuclumbusService.SearchResult body = response.body(); - if (body == null) { - listener.onChannelSearchResultsFound(Collections.emptyList()); - logError(response); - return; - } - cache.put(key(Method.JABBER_NETWORK, query), body.result.items); - listener.onChannelSearchResultsFound(body.result.items); - } + private void discoverChannelsJabberNetwork( + final String query, final OnChannelSearchResultsFound listener) { + if (muclumbusService == null) { + listener.onChannelSearchResultsFound(Collections.emptyList()); + return; + } + final MuclumbusService.SearchRequest searchRequest = + new MuclumbusService.SearchRequest(query); + final Call searchResultCall = + muclumbusService.search(searchRequest); + searchResultCall.enqueue( + new Callback() { + @Override + public void onResponse( + @NonNull Call call, + @NonNull Response response) { + final MuclumbusService.SearchResult body = response.body(); + if (body == null) { + listener.onChannelSearchResultsFound(Collections.emptyList()); + logError(response); + return; + } + cache.put(key(Method.JABBER_NETWORK, query), body.result.items); + listener.onChannelSearchResultsFound(body.result.items); + } - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable throwable) { - Log.d(Config.LOGTAG, "Unable to query muclumbus on " + Config.CHANNEL_DISCOVERY, throwable); - listener.onChannelSearchResultsFound(Collections.emptyList()); - } - }); + @Override + public void onFailure( + @NonNull Call call, + @NonNull Throwable throwable) { + Log.d( + Config.LOGTAG, + "Unable to query muclumbus on " + Config.CHANNEL_DISCOVERY, + throwable); + listener.onChannelSearchResultsFound(Collections.emptyList()); + } + }); } - private void discoverChannelsLocalServers(final String query, final OnChannelSearchResultsFound listener) { + private void discoverChannelsLocalServers( + final String query, final OnChannelSearchResultsFound listener) { final Map localMucService = getLocalMucServices(); Log.d(Config.LOGTAG, "checking with " + localMucService.size() + " muc services"); if (localMucService.size() == 0) { @@ -156,38 +187,49 @@ private void discoverChannelsLocalServers(final String query, final OnChannelSea for (Map.Entry entry : localMucService.entrySet()) { IqPacket itemsRequest = service.getIqGenerator().queryDiscoItems(entry.getKey()); queriesInFlight.incrementAndGet(); - service.sendIqPacket(entry.getValue(), itemsRequest, (account, itemsResponse) -> { - if (itemsResponse.getType() == IqPacket.TYPE.RESULT) { - final List items = IqParser.items(itemsResponse); - for (Jid item : items) { - IqPacket infoRequest = service.getIqGenerator().queryDiscoInfo(item); - queriesInFlight.incrementAndGet(); - service.sendIqPacket(account, infoRequest, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket infoResponse) { - if (infoResponse.getType() == IqPacket.TYPE.RESULT) { - final Room room = IqParser.parseRoom(infoResponse); - if (room != null) { - rooms.add(room); - } - if (queriesInFlight.decrementAndGet() <= 0) { - finishDiscoSearch(rooms, query, listener); - } - } else { - queriesInFlight.decrementAndGet(); - } + service.sendIqPacket( + entry.getValue(), + itemsRequest, + (account, itemsResponse) -> { + if (itemsResponse.getType() == IqPacket.TYPE.RESULT) { + final List items = IqParser.items(itemsResponse); + for (Jid item : items) { + IqPacket infoRequest = + service.getIqGenerator().queryDiscoInfo(item); + queriesInFlight.incrementAndGet(); + service.sendIqPacket( + account, + infoRequest, + new OnIqPacketReceived() { + @Override + public void onIqPacketReceived( + Account account, IqPacket infoResponse) { + if (infoResponse.getType() + == IqPacket.TYPE.RESULT) { + final Room room = + IqParser.parseRoom(infoResponse); + if (room != null) { + rooms.add(room); + } + if (queriesInFlight.decrementAndGet() <= 0) { + finishDiscoSearch(rooms, query, listener); + } + } else { + queriesInFlight.decrementAndGet(); + } + } + }); } - }); - } - } - if (queriesInFlight.decrementAndGet() <= 0) { - finishDiscoSearch(rooms, query, listener); - } - }); + } + if (queriesInFlight.decrementAndGet() <= 0) { + finishDiscoSearch(rooms, query, listener); + } + }); } } - private void finishDiscoSearch(List rooms, String query, OnChannelSearchResultsFound listener) { + private void finishDiscoSearch( + List rooms, String query, OnChannelSearchResultsFound listener) { Collections.sort(rooms); cache.put(key(Method.LOCAL_SERVER, ""), rooms); if (query.isEmpty()) { @@ -241,7 +283,7 @@ private static void logError(final Response response) { try { Log.d(Config.LOGTAG, "error body=" + errorBody.string()); } catch (IOException e) { - //ignored + // ignored } } diff --git a/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java b/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java index 4687e5f63..5cf9417e9 100644 --- a/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java @@ -20,6 +20,8 @@ import androidx.databinding.DataBindingUtil; +import com.google.common.base.Strings; + import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicReference; @@ -90,6 +92,9 @@ protected void onCreate(final Bundle savedInstanceState) { } private static ChannelDiscoveryService.Method getMethod(final Context c) { + if ( Strings.isNullOrEmpty(Config.CHANNEL_DISCOVERY)) { + return ChannelDiscoveryService.Method.LOCAL_SERVER; + } if (QuickConversationsService.isQuicksy()) { return ChannelDiscoveryService.Method.JABBER_NETWORK; } diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java index 5e21e0b26..07c8a55db 100644 --- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java @@ -23,6 +23,8 @@ import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; +import com.google.common.base.Strings; + import java.io.File; import java.security.KeyStoreException; import java.util.ArrayList; @@ -96,20 +98,24 @@ public void onStart() { changeOmemoSettingSummary(); - if (QuickConversationsService.isQuicksy()) { - final PreferenceCategory connectionOptions = - (PreferenceCategory) mSettingsFragment.findPreference("connection_options"); + if (QuickConversationsService.isQuicksy() + || Strings.isNullOrEmpty(Config.CHANNEL_DISCOVERY)) { final PreferenceCategory groupChats = (PreferenceCategory) mSettingsFragment.findPreference("group_chats"); final Preference channelDiscoveryMethod = mSettingsFragment.findPreference("channel_discovery_method"); + if (groupChats != null && channelDiscoveryMethod != null) { + groupChats.removePreference(channelDiscoveryMethod); + } + } + + if (QuickConversationsService.isQuicksy()) { + final PreferenceCategory connectionOptions = + (PreferenceCategory) mSettingsFragment.findPreference("connection_options"); PreferenceScreen expert = (PreferenceScreen) mSettingsFragment.findPreference("expert"); if (connectionOptions != null) { expert.removePreference(connectionOptions); } - if (groupChats != null && channelDiscoveryMethod != null) { - groupChats.removePreference(channelDiscoveryMethod); - } } PreferenceScreen mainPreferenceScreen = From 90048e92bb39b2124fcbdd74944e8d1421c0e557 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 12 Oct 2022 18:43:05 +0200 Subject: [PATCH 217/394] use url span method to show context menu. fixes #4393 --- .../ui/ConversationFragment.java | 6 +- .../conversations/ui/util/MyLinkify.java | 38 +++++++++ .../conversations/ui/util/ShareUtil.java | 77 ++++++++++--------- 3 files changed, 81 insertions(+), 40 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 3b923adce..f6626c3a1 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1332,11 +1332,11 @@ private void populateContextMenu(ContextMenu menu) { && t == null) { copyMessage.setVisible(true); quoteMessage.setVisible(!showError && MessageUtils.prepareQuote(m).length() > 0); - String body = m.getMergedBody().toString(); - if (ShareUtil.containsXmppUri(body)) { + final String scheme = ShareUtil.getLinkScheme(m.getMergedBody()); + if ("xmpp".equals(scheme)) { copyLink.setTitle(R.string.copy_jabber_id); copyLink.setVisible(true); - } else if (Patterns.AUTOLINK_WEB_URL.matcher(body).find()) { + } else if (scheme != null) { copyLink.setVisible(true); } } diff --git a/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java b/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java index b72c5aa86..d5d3a82da 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java +++ b/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java @@ -31,9 +31,18 @@ import android.os.Build; import android.text.Editable; +import android.text.style.URLSpan; import android.text.util.Linkify; +import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; import java.util.Locale; +import java.util.Objects; import eu.siacs.conversations.ui.text.FixedURLSpan; import eu.siacs.conversations.utils.GeoHelper; @@ -118,4 +127,33 @@ public static void addLinks(Editable body, boolean includeGeo) { } FixedURLSpan.fix(body); } + + public static List extractLinks(final Editable body) { + MyLinkify.addLinks(body, false); + final Collection spans = + Arrays.asList(body.getSpans(0, body.length() - 1, URLSpan.class)); + final Collection urlWrappers = + Collections2.filter( + Collections2.transform( + spans, + s -> + s == null + ? null + : new UrlWrapper(body.getSpanStart(s), s.getURL())), + uw -> uw != null); + List sorted = ImmutableList.sortedCopyOf( + (a, b) -> Integer.compare(a.position, b.position), urlWrappers); + return Lists.transform(sorted, uw -> uw.url); + + } + + private static class UrlWrapper { + private final int position; + private final String url; + + private UrlWrapper(int position, String url) { + this.position = position; + this.url = url; + } + } } diff --git a/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java b/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java index 8ff81a203..007575307 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java +++ b/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java @@ -33,18 +33,14 @@ import android.content.Intent; import android.net.Uri; import android.text.SpannableStringBuilder; -import android.text.style.URLSpan; import android.widget.Toast; -import java.util.regex.Matcher; - import eu.siacs.conversations.R; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.ui.ConversationsActivity; import eu.siacs.conversations.ui.XmppActivity; -import eu.siacs.conversations.utils.Patterns; import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.xmpp.Jid; @@ -108,38 +104,45 @@ public static void copyUrlToClipboard(XmppActivity activity, Message message) { } } - public static void copyLinkToClipboard(final XmppActivity activity, final Message message) { - final SpannableStringBuilder body = message.getMergedBody(); - MyLinkify.addLinks(body, true); - for (final URLSpan urlspan : body.getSpans(0, body.length() - 1, URLSpan.class)) { - final Uri uri = Uri.parse(urlspan.getURL()); - if ("xmpp".equals(uri.getScheme())) { - try { - final Jid jid = new XmppUri(uri).getJid(); - if (activity.copyTextToClipboard(jid.asBareJid().toString(), R.string.account_settings_jabber_id)) { - Toast.makeText(activity,R.string.jabber_id_copied_to_clipboard, Toast.LENGTH_SHORT).show(); - } - return; - } catch (final Exception e) { - return; - } - } else { - if (activity.copyTextToClipboard(urlspan.getURL(),R.string.web_address)) { - Toast.makeText(activity,R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT).show(); - } - } - } - } + public static void copyLinkToClipboard(final XmppActivity activity, final Message message) { + final SpannableStringBuilder body = message.getMergedBody(); + for (final String url : MyLinkify.extractLinks(body)) { + final Uri uri = Uri.parse(url); + if ("xmpp".equals(uri.getScheme())) { + try { + final Jid jid = new XmppUri(uri).getJid(); + if (activity.copyTextToClipboard( + jid.asBareJid().toString(), R.string.account_settings_jabber_id)) { + Toast.makeText( + activity, + R.string.jabber_id_copied_to_clipboard, + Toast.LENGTH_SHORT) + .show(); + } + return; + } catch (final Exception e) { + return; + } + } else { + if (activity.copyTextToClipboard(url, R.string.web_address)) { + Toast.makeText(activity, R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT) + .show(); + } + return; + } + } + } - public static boolean containsXmppUri(String body) { - Matcher xmppPatternMatcher = Patterns.XMPP_PATTERN.matcher(body); - if (xmppPatternMatcher.find()) { - try { - return new XmppUri(body.substring(xmppPatternMatcher.start(), xmppPatternMatcher.end())).isValidJid(); - } catch (Exception e) { - return false; - } - } - return false; - } + public static String getLinkScheme(final SpannableStringBuilder body) { + MyLinkify.addLinks(body, false); + for (final String url : MyLinkify.extractLinks(body)) { + final Uri uri = Uri.parse(url); + if ("xmpp".equals(uri.getScheme())) { + return uri.getScheme(); + } else { + return "http"; + } + } + return null; + } } From 3d6c7bbf1c291640790710e7516efdbadd568214 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 13 Oct 2022 09:51:56 +0200 Subject: [PATCH 218/394] fix display glitch in username mode --- src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 8959d4c38..c0c43dda7 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -822,7 +822,6 @@ protected void onBackendConnected() { } if (mUsernameMode) { this.binding.accountJidLayout.setHint(getString(R.string.username_hint)); - this.binding.accountJid.setHint(R.string.username_hint); } else { final KnownHostsAdapter mKnownHostsAdapter = new KnownHostsAdapter(this, R.layout.simple_list_item, From 9a0c90f066c57c86fc81eed53f4bbc1c4e6a62c9 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 14 Oct 2022 13:13:21 +0200 Subject: [PATCH 219/394] read new stream features directly after success --- .../conversations/xmpp/XmppConnection.java | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 2ba2daea0..aaf40edf7 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -556,7 +556,7 @@ private void processStream() throws XmlPullParserException, IOException { while (nextTag != null && !nextTag.isEnd("stream")) { if (nextTag.isStart("error")) { processStreamError(nextTag); - } else if (nextTag.isStart("features")) { + } else if (nextTag.isStart("features", Namespace.STREAMS)) { processStreamFeatures(nextTag); } else if (nextTag.isStart("proceed", Namespace.TLS)) { switchOverToTls(); @@ -705,6 +705,22 @@ private boolean processSuccess(final Element success) account.getJid().asBareJid().toString() + ": logged in (using " + version + ")"); account.setPinnedMechanism(saslMechanism); if (version == SaslMechanism.Version.SASL_2) { + final Tag tag = tagReader.readTag(); + if (tag != null && tag.isStart("features", Namespace.STREAMS)) { + this.streamFeatures = tagReader.readElement(tag); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": processed NOP stream features after success " + + XmlHelper.printElementNames(this.streamFeatures)); + } else { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": server did not send stream features after SASL2 success"); + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } + Log.d(Config.LOGTAG, "success: " + success); final String authorizationIdentifier = success.findChildContent("authorization-identifier"); final Jid authorizationJid; @@ -746,7 +762,13 @@ private boolean processSuccess(final Element success) final Element bound = success.findChild("bound", Namespace.BIND2); final Element resumed = success.findChild("resumed", "urn:xmpp:sm:3"); final Element failed = success.findChild("failed", "urn:xmpp:sm:3"); - // TODO check if resumed and bound exist and throw bind failure + if (bound != null && resumed != null) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": server sent bound and resumed in SASL2 success"); + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } if (resumed != null && streamId != null) { processResumed(resumed); } else if (failed != null) { @@ -767,6 +789,7 @@ private boolean processSuccess(final Element success) account.getJid().asBareJid() + ": successfully enabled carbons"); features.carbonsEnabled = true; } + // TODO if we didn’t enable stream managment in bind do it now // TODO if both are set mark account ready for pipelining sendPostBindInitialization(streamManagementEnabled != null, carbonsEnabled != null); } @@ -1761,7 +1784,7 @@ private void sendPostBindInitialization( lastDiscoStarted = SystemClock.elapsedRealtime(); mXmppConnectionService.scheduleWakeUpCall( Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode()); - Element caps = streamFeatures.findChild("c"); + final Element caps = streamFeatures.findChild("c"); final String hash = caps == null ? null : caps.getAttribute("hash"); final String ver = caps == null ? null : caps.getAttribute("ver"); ServiceDiscoveryResult discoveryResult = null; From 7eb160386d0c1a76084fab004c41d596009fc678 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 14 Oct 2022 13:29:59 +0200 Subject: [PATCH 220/394] =?UTF-8?q?enable=20SM=20if=20it=20wasn=E2=80=99t?= =?UTF-8?q?=20enabled=20in=20bind=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/eu/siacs/conversations/xmpp/XmppConnection.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index aaf40edf7..b467d8dc7 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -780,8 +780,13 @@ private boolean processSuccess(final Element success) final Element streamManagementEnabled = bound.findChild("enabled", Namespace.STREAM_MANAGEMENT); final Element carbonsEnabled = bound.findChild("enabled", Namespace.CARBONS); + final boolean waitForDisco; if (streamManagementEnabled != null) { processEnabled(streamManagementEnabled); + waitForDisco = true; + } else { + //if we didn’t enable stream managment in bind do it now + waitForDisco = enableStreamManagement(); } if (carbonsEnabled != null) { Log.d( @@ -789,9 +794,7 @@ private boolean processSuccess(final Element success) account.getJid().asBareJid() + ": successfully enabled carbons"); features.carbonsEnabled = true; } - // TODO if we didn’t enable stream managment in bind do it now - // TODO if both are set mark account ready for pipelining - sendPostBindInitialization(streamManagementEnabled != null, carbonsEnabled != null); + sendPostBindInitialization(waitForDisco, carbonsEnabled != null); } } this.quickStartInProgress = false; From 0cd416298da9ce3844c099486a482bd4f5fa1a8d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 14 Oct 2022 20:00:36 +0200 Subject: [PATCH 221/394] ensure we only select channel binding methods available for tls version --- .../java/eu/siacs/conversations/Config.java | 2 +- .../crypto/sasl/ChannelBinding.java | 48 ++++++++-- .../crypto/sasl/SaslMechanism.java | 58 ++++++++++-- .../{SSLSocketHelper.java => SSLSockets.java} | 46 +++++++++- .../conversations/utils/TLSSocketFactory.java | 4 +- .../eu/siacs/conversations/xml/Namespace.java | 1 + .../conversations/xmpp/XmppConnection.java | 88 ++++++++----------- 7 files changed, 181 insertions(+), 66 deletions(-) rename src/main/java/eu/siacs/conversations/utils/{SSLSocketHelper.java => SSLSockets.java} (76%) diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 47226eb6e..812f6ae10 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -60,7 +60,7 @@ public static boolean multipleEncryptionChoices() { public static final long CONTACT_SYNC_RETRY_INTERVAL = 1000L * 60 * 5; - public static final boolean SASL_2_ENABLED = true; + public static final boolean QUICKSTART_ENABLED = true; //Notification settings public static final boolean HIDE_MESSAGE_TEXT_IN_NOTIFICATION = false; diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java index d8307a76d..fb1255566 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java @@ -3,11 +3,19 @@ import android.util.Log; import com.google.common.base.CaseFormat; +import com.google.common.base.Preconditions; +import com.google.common.base.Predicates; import com.google.common.base.Strings; +import com.google.common.collect.Collections2; +import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import eu.siacs.conversations.Config; +import eu.siacs.conversations.utils.SSLSockets; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Namespace; public enum ChannelBinding { NONE, @@ -15,7 +23,24 @@ public enum ChannelBinding { TLS_SERVER_END_POINT, TLS_UNIQUE; - public static ChannelBinding of(final String type) { + public static Collection of(final Element channelBinding) { + Preconditions.checkArgument( + channelBinding == null + || ("sasl-channel-binding".equals(channelBinding.getName()) + && Namespace.CHANNEL_BINDING.equals(channelBinding.getNamespace())), + "pass null or a valid channel binding stream feature"); + return Collections2.filter( + Collections2.transform( + Collections2.filter( + channelBinding == null + ? Collections.emptyList() + : channelBinding.getChildren(), + c -> c != null && "channel-binding".equals(c.getName())), + c -> c == null ? null : ChannelBinding.of(c.getAttribute("type"))), + Predicates.notNull()); + } + + private static ChannelBinding of(final String type) { if (type == null) { return null; } @@ -39,15 +64,28 @@ public static ChannelBinding get(final String name) { } } - public static ChannelBinding best(final Collection bindings) { - if (bindings.contains(TLS_EXPORTER)) { + public static ChannelBinding best( + final Collection bindings, final SSLSockets.Version sslVersion) { + if (sslVersion == SSLSockets.Version.NONE) { + return NONE; + } + if (bindings.contains(TLS_EXPORTER) && sslVersion == SSLSockets.Version.TLS_1_3) { return TLS_EXPORTER; - } else if (bindings.contains(TLS_UNIQUE)) { + } else if (bindings.contains(TLS_UNIQUE) + && Arrays.asList( + SSLSockets.Version.TLS_1_0, + SSLSockets.Version.TLS_1_1, + SSLSockets.Version.TLS_1_2) + .contains(sslVersion)) { return TLS_UNIQUE; } else if (bindings.contains(TLS_SERVER_END_POINT)) { return TLS_SERVER_END_POINT; } else { - return null; + return NONE; } } + + public static boolean ensureBest(final ChannelBinding channelBinding, final SSLSockets.Version sslVersion) { + return ChannelBinding.best(Collections.singleton(channelBinding), sslVersion) == channelBinding; + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java index e5b940b87..e0df3a2d4 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java @@ -1,13 +1,19 @@ package eu.siacs.conversations.crypto.sasl; +import android.util.Log; + +import com.google.common.base.Preconditions; import com.google.common.base.Strings; +import com.google.common.collect.Collections2; import java.util.Collection; import java.util.Collections; import javax.net.ssl.SSLSocket; +import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.utils.SSLSockets; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; @@ -47,6 +53,17 @@ public String getResponse(final String challenge, final SSLSocket sslSocket) return ""; } + public static Collection mechanisms(final Element authElement) { + if (authElement == null) { + return Collections.emptyList(); + } + return Collections2.transform( + Collections2.filter( + authElement.getChildren(), + c -> c != null && "mechanism".equals(c.getName())), + c -> c == null ? null : c.getContent()); + } + protected enum State { INITIAL, AUTH_TEXT_SENT, @@ -102,16 +119,19 @@ public Factory(final Account account) { this.account = account; } - public SaslMechanism of( - final Collection mechanisms, final Collection bindings) { - final ChannelBinding channelBinding = ChannelBinding.best(bindings); + private SaslMechanism of( + final Collection mechanisms, final ChannelBinding channelBinding) { + Preconditions.checkNotNull(channelBinding, "Use ChannelBinding.NONE instead of null"); if (mechanisms.contains(External.MECHANISM) && account.getPrivateKeyAlias() != null) { return new External(account); - } else if (mechanisms.contains(ScramSha512Plus.MECHANISM) && channelBinding != null) { + } else if (mechanisms.contains(ScramSha512Plus.MECHANISM) + && channelBinding != ChannelBinding.NONE) { return new ScramSha512Plus(account, channelBinding); - } else if (mechanisms.contains(ScramSha256Plus.MECHANISM) && channelBinding != null) { + } else if (mechanisms.contains(ScramSha256Plus.MECHANISM) + && channelBinding != ChannelBinding.NONE) { return new ScramSha256Plus(account, channelBinding); - } else if (mechanisms.contains(ScramSha1Plus.MECHANISM) && channelBinding != null) { + } else if (mechanisms.contains(ScramSha1Plus.MECHANISM) + && channelBinding != ChannelBinding.NONE) { return new ScramSha1Plus(account, channelBinding); } else if (mechanisms.contains(ScramSha512.MECHANISM)) { return new ScramSha512(account); @@ -131,9 +151,33 @@ public SaslMechanism of( } } + public SaslMechanism of( + final Collection mechanisms, + final Collection bindings, + final SSLSockets.Version sslVersion) { + final ChannelBinding channelBinding = ChannelBinding.best(bindings, sslVersion); + return of(mechanisms, channelBinding); + } + public SaslMechanism of(final String mechanism, final ChannelBinding channelBinding) { - return of(Collections.singleton(mechanism), Collections.singleton(channelBinding)); + return of(Collections.singleton(mechanism), channelBinding); } + } + public static SaslMechanism ensureAvailable( + final SaslMechanism mechanism, final SSLSockets.Version sslVersion) { + if (mechanism instanceof ScramPlusMechanism) { + final ChannelBinding cb = ((ScramPlusMechanism) mechanism).getChannelBinding(); + if (ChannelBinding.ensureBest(cb, sslVersion)) { + return mechanism; + } else { + Log.d( + Config.LOGTAG, + "pinned channel binding method " + cb + " no longer available"); + return null; + } + } else { + return mechanism; + } } } diff --git a/src/main/java/eu/siacs/conversations/utils/SSLSocketHelper.java b/src/main/java/eu/siacs/conversations/utils/SSLSockets.java similarity index 76% rename from src/main/java/eu/siacs/conversations/utils/SSLSocketHelper.java rename to src/main/java/eu/siacs/conversations/utils/SSLSockets.java index 53d4f4169..ae853bea8 100644 --- a/src/main/java/eu/siacs/conversations/utils/SSLSocketHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/SSLSockets.java @@ -5,9 +5,12 @@ import androidx.annotation.RequiresApi; +import com.google.common.base.Strings; + import org.conscrypt.Conscrypt; import java.lang.reflect.Method; +import java.net.Socket; import java.nio.charset.StandardCharsets; import java.security.NoSuchAlgorithmException; import java.util.Arrays; @@ -24,7 +27,7 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; -public class SSLSocketHelper { +public class SSLSockets { public static void setSecurity(final SSLSocket sslSocket) { final String[] supportProtocols; @@ -100,6 +103,45 @@ public static SSLContext getSSLContext() throws NoSuchAlgorithmException { public static void log(Account account, SSLSocket socket) { SSLSession session = socket.getSession(); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": protocol=" + session.getProtocol() + " cipher=" + session.getCipherSuite()); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": protocol=" + + session.getProtocol() + + " cipher=" + + session.getCipherSuite()); + } + + public static Version version(final Socket socket) { + if (socket instanceof SSLSocket) { + final SSLSocket sslSocket = (SSLSocket) socket; + return Version.of(sslSocket.getSession().getProtocol()); + } else { + return Version.NONE; + } + } + + public enum Version { + TLS_1_0, + TLS_1_1, + TLS_1_2, + TLS_1_3, + UNKNOWN, + NONE; + + private static Version of(final String protocol) { + switch (Strings.nullToEmpty(protocol)) { + case "TLSv1": + return TLS_1_0; + case "TLSv1.1": + return TLS_1_1; + case "TLSv1.2": + return TLS_1_2; + case "TLSv1.3": + return TLS_1_3; + default: + return UNKNOWN; + } + } } } diff --git a/src/main/java/eu/siacs/conversations/utils/TLSSocketFactory.java b/src/main/java/eu/siacs/conversations/utils/TLSSocketFactory.java index 8bd737c7e..6d5ce97aa 100644 --- a/src/main/java/eu/siacs/conversations/utils/TLSSocketFactory.java +++ b/src/main/java/eu/siacs/conversations/utils/TLSSocketFactory.java @@ -17,7 +17,7 @@ public class TLSSocketFactory extends SSLSocketFactory { private final SSLSocketFactory internalSSLSocketFactory; public TLSSocketFactory(X509TrustManager[] trustManager, SecureRandom random) throws KeyManagementException, NoSuchAlgorithmException { - SSLContext context = SSLSocketHelper.getSSLContext(); + SSLContext context = SSLSockets.getSSLContext(); context.init(null, trustManager, random); this.internalSSLSocketFactory = context.getSocketFactory(); } @@ -59,7 +59,7 @@ public Socket createSocket(InetAddress address, int port, InetAddress localAddre private static Socket enableTLSOnSocket(Socket socket) { if(socket instanceof SSLSocket) { - SSLSocketHelper.setSecurity((SSLSocket) socket); + SSLSockets.setSecurity((SSLSocket) socket); } return socket; } diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index d17891ff2..55f45c6b5 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -19,6 +19,7 @@ public final class Namespace { public static final String SASL = "urn:ietf:params:xml:ns:xmpp-sasl"; public static final String SASL_2 = "urn:xmpp:sasl:2"; public static final String CHANNEL_BINDING = "urn:xmpp:sasl-cb:0"; + public static final String FAST = "urn:xmpp:fast:0"; public static final String TLS = "urn:ietf:params:xml:ns:xmpp-tls"; public static final String PUBSUB = "http://jabber.org/protocol/pubsub"; public static final String PUBSUB_PUBLISH_OPTIONS = PUBSUB + "#publish-options"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index b467d8dc7..e693b2afa 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -15,9 +15,7 @@ import androidx.annotation.NonNull; -import com.google.common.base.Predicates; import com.google.common.base.Strings; -import com.google.common.collect.Collections2; import org.xmlpull.v1.XmlPullParserException; @@ -80,7 +78,7 @@ import eu.siacs.conversations.utils.Patterns; import eu.siacs.conversations.utils.PhoneHelper; import eu.siacs.conversations.utils.Resolver; -import eu.siacs.conversations.utils.SSLSocketHelper; +import eu.siacs.conversations.utils.SSLSockets; import eu.siacs.conversations.utils.SocksSocketFactory; import eu.siacs.conversations.utils.XmlHelper; import eu.siacs.conversations.xml.Element; @@ -494,10 +492,11 @@ private boolean startXmpp(final Socket socket) throws Exception { tagWriter.beginDocument(); final boolean quickStart; if (socket instanceof SSLSocket) { - SSLSocketHelper.log(account, (SSLSocket) socket); - quickStart = establishStream(true); + final SSLSocket sslSocket = (SSLSocket) socket; + SSLSockets.log(account, sslSocket); + quickStart = establishStream(SSLSockets.version(sslSocket)); } else { - quickStart = establishStream(false); + quickStart = establishStream(SSLSockets.Version.NONE); } final Tag tag = tagReader.readTag(); if (Thread.currentThread().isInterrupted()) { @@ -512,7 +511,7 @@ private boolean startXmpp(final Socket socket) throws Exception { private SSLSocketFactory getSSLSocketFactory() throws NoSuchAlgorithmException, KeyManagementException { - final SSLContext sc = SSLSocketHelper.getSSLContext(); + final SSLContext sc = SSLSockets.getSSLContext(); final MemorizingTrustManager trustManager = this.mXmppConnectionService.getMemorizingTrustManager(); final KeyManager[] keyManager; @@ -720,7 +719,6 @@ private boolean processSuccess(final Element success) + ": server did not send stream features after SASL2 success"); throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } - Log.d(Config.LOGTAG, "success: " + success); final String authorizationIdentifier = success.findChildContent("authorization-identifier"); final Jid authorizationJid; @@ -785,7 +783,7 @@ private boolean processSuccess(final Element success) processEnabled(streamManagementEnabled); waitForDisco = true; } else { - //if we didn’t enable stream managment in bind do it now + //if we did not enable stream management in bind do it now waitForDisco = enableStreamManagement(); } if (carbonsEnabled != null) { @@ -800,7 +798,7 @@ private boolean processSuccess(final Element success) this.quickStartInProgress = false; if (version == SaslMechanism.Version.SASL) { tagReader.reset(); - sendStartStream(true); + sendStartStream(false, true); final Tag tag = tagReader.readTag(); if (tag != null && tag.isStart("stream", Namespace.STREAMS)) { processStream(); @@ -1163,7 +1161,7 @@ private void switchOverToTls() throws XmlPullParserException, IOException { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS connection established"); final boolean quickStart; try { - quickStart = establishStream(true); + quickStart = establishStream(SSLSockets.version(sslSocket)); } catch (final InterruptedException e) { return; } @@ -1173,7 +1171,7 @@ private void switchOverToTls() throws XmlPullParserException, IOException { features.encryptionEnabled = true; final Tag tag = tagReader.readTag(); if (tag != null && tag.isStart("stream", Namespace.STREAMS)) { - SSLSocketHelper.log(account, sslSocket); + SSLSockets.log(account, sslSocket); processStream(); } else { throw new StateChangingException(Account.State.STREAM_OPENING_ERROR); @@ -1193,9 +1191,9 @@ private SSLSocket upgradeSocketToTls(final Socket socket) throws IOException { (SSLSocket) sslSocketFactory.createSocket( socket, address.getHostAddress(), socket.getPort(), true); - SSLSocketHelper.setSecurity(sslSocket); - SSLSocketHelper.setHostname(sslSocket, IDN.toASCII(account.getServer())); - SSLSocketHelper.setApplicationProtocol(sslSocket, "xmpp-client"); + SSLSockets.setSecurity(sslSocket); + SSLSockets.setHostname(sslSocket, IDN.toASCII(account.getServer())); + SSLSockets.setApplicationProtocol(sslSocket, "xmpp-client"); final XmppDomainVerifier xmppDomainVerifier = new XmppDomainVerifier(); try { if (!xmppDomainVerifier.verify( @@ -1251,8 +1249,7 @@ private void processStreamFeatures(final Tag currentTag) throws IOException { } else if (!this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE) && account.isOptionSet(Account.OPTION_REGISTER)) { throw new StateChangingException(Account.State.REGISTRATION_NOT_SUPPORTED); - } else if (Config.SASL_2_ENABLED - && this.streamFeatures.hasChild("authentication", Namespace.SASL_2) + } else if (this.streamFeatures.hasChild("authentication", Namespace.SASL_2) && shouldAuthenticate && isSecure) { authenticate(SaslMechanism.Version.SASL_2); @@ -1301,29 +1298,14 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio } else { authElement = this.streamFeatures.findChild("authentication", Namespace.SASL_2); } - //TODO externalize - final Collection mechanisms = - Collections2.transform( - Collections2.filter( - authElement.getChildren(), - c -> c != null && "mechanism".equals(c.getName())), - c -> c == null ? null : c.getContent()); + final Collection mechanisms = SaslMechanism.mechanisms(authElement); final Element cbElement = this.streamFeatures.findChild("sasl-channel-binding", Namespace.CHANNEL_BINDING); - final Collection channelBindings = - Collections2.filter( - Collections2.transform( - Collections2.filter( - cbElement == null - ? Collections.emptyList() - : cbElement.getChildren(), - c -> c != null && "channel-binding".equals(c.getName())), - c -> c == null ? null : ChannelBinding.of(c.getAttribute("type"))), - Predicates.notNull()); + final Collection channelBindings = ChannelBinding.of(cbElement); Log.d(Config.LOGTAG,"mechanisms: "+mechanisms); Log.d(Config.LOGTAG, "channel bindings: " + channelBindings); final SaslMechanism.Factory factory = new SaslMechanism.Factory(account); - this.saslMechanism = factory.of(mechanisms, channelBindings); + this.saslMechanism = factory.of(mechanisms, channelBindings, SSLSockets.version(this.socket)); //TODO externalize checks @@ -1360,6 +1342,9 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio } else if (version == SaslMechanism.Version.SASL_2) { final Element inline = authElement.findChild("inline", Namespace.SASL_2); final boolean sm = inline != null && inline.hasChild("sm", "urn:xmpp:sm:3"); + final Element fast = inline == null ? null : inline.findChild("fast", Namespace.FAST); + final Collection fastMechanisms = SaslMechanism.mechanisms(fast); + Log.d(Config.LOGTAG,"fast mechanisms: "+fastMechanisms); final Collection bindFeatures = Bind2.features(inline); quickStartAvailable = sm @@ -1434,12 +1419,11 @@ private Element generateBindRequest(final Collection bindFeatures) { Log.d(Config.LOGTAG, "inline bind features: " + bindFeatures); final Element bind = new Element("bind", Namespace.BIND2); bind.addChild("tag").setContent(mXmppConnectionService.getString(R.string.app_name)); - final Element features = bind.addChild("features"); if (bindFeatures.contains(Namespace.CARBONS)) { - features.addChild("enable", Namespace.CARBONS); + bind.addChild("enable", Namespace.CARBONS); } if (bindFeatures.contains(Namespace.STREAM_MANAGEMENT)) { - features.addChild(new EnablePacket()); + bind.addChild(new EnablePacket()); } return bind; } @@ -2060,34 +2044,40 @@ private void failPendingMessages(final String error) { } } - private boolean establishStream(final boolean secureConnection) throws IOException, InterruptedException { - final SaslMechanism saslMechanism = account.getPinnedMechanism(); + private boolean establishStream(final SSLSockets.Version sslVersion) + throws IOException, InterruptedException { + final SaslMechanism pinnedMechanism = + SaslMechanism.ensureAvailable(account.getPinnedMechanism(), sslVersion); + final boolean secureConnection = sslVersion != SSLSockets.Version.NONE; if (secureConnection - && Config.SASL_2_ENABLED - && saslMechanism != null + && Config.QUICKSTART_ENABLED + && pinnedMechanism != null && account.isOptionSet(Account.OPTION_QUICKSTART_AVAILABLE)) { mXmppConnectionService.restoredFromDatabaseLatch.await(); - this.saslMechanism = saslMechanism; + this.saslMechanism = pinnedMechanism; final Element authenticate = - generateAuthenticationRequest(saslMechanism.getClientFirstMessage()); - authenticate.setAttribute("mechanism", saslMechanism.getMechanism()); - sendStartStream(false); + generateAuthenticationRequest(pinnedMechanism.getClientFirstMessage()); + authenticate.setAttribute("mechanism", pinnedMechanism.getMechanism()); + sendStartStream(true, false); tagWriter.writeElement(authenticate); Log.d( Config.LOGTAG, account.getJid().toString() + ": quick start with " - + saslMechanism.getMechanism()); + + pinnedMechanism.getMechanism()); return true; } else { - sendStartStream(true); + sendStartStream(secureConnection, true); return false; } } - private void sendStartStream(final boolean flush) throws IOException { + private void sendStartStream(final boolean from, final boolean flush) throws IOException { final Tag stream = Tag.start("stream:stream"); stream.setAttribute("to", account.getServer()); + if (from) { + stream.setAttribute("from", account.getJid().asBareJid().toEscapedString()); + } stream.setAttribute("version", "1.0"); stream.setAttribute("xml:lang", LocalizedContent.STREAM_LANGUAGE); stream.setAttribute("xmlns", "jabber:client"); From 3378447f606024d89fc9f13ac5111c2cb0176ce9 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 15 Oct 2022 00:09:29 +0200 Subject: [PATCH 222/394] parse hash token names --- .../java/eu/siacs/conversations/Config.java | 2 +- .../crypto/sasl/ChannelBinding.java | 33 ++++- .../crypto/sasl/HashedToken.java | 114 ++++++++++++++++++ .../crypto/sasl/HashedTokenSha256.java | 24 ++++ .../conversations/xmpp/XmppConnection.java | 3 +- 5 files changed, 172 insertions(+), 4 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java create mode 100644 src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha256.java diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 812f6ae10..27d1f1097 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -60,7 +60,7 @@ public static boolean multipleEncryptionChoices() { public static final long CONTACT_SYNC_RETRY_INTERVAL = 1000L * 60 * 5; - public static final boolean QUICKSTART_ENABLED = true; + public static final boolean QUICKSTART_ENABLED = false; //Notification settings public static final boolean HIDE_MESSAGE_TEXT_IN_NOTIFICATION = false; diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java index fb1255566..26f9c9da0 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java @@ -6,7 +6,9 @@ import com.google.common.base.Preconditions; import com.google.common.base.Predicates; import com.google.common.base.Strings; +import com.google.common.collect.BiMap; import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableBiMap; import java.util.Arrays; import java.util.Collection; @@ -23,6 +25,16 @@ public enum ChannelBinding { TLS_SERVER_END_POINT, TLS_UNIQUE; + public static final BiMap SHORT_NAMES; + + static { + final ImmutableBiMap.Builder builder = ImmutableBiMap.builder(); + for (final ChannelBinding cb : values()) { + builder.put(cb, shortName(cb)); + } + SHORT_NAMES = builder.build(); + } + public static Collection of(final Element channelBinding) { Preconditions.checkArgument( channelBinding == null @@ -85,7 +97,24 @@ public static ChannelBinding best( } } - public static boolean ensureBest(final ChannelBinding channelBinding, final SSLSockets.Version sslVersion) { - return ChannelBinding.best(Collections.singleton(channelBinding), sslVersion) == channelBinding; + public static boolean ensureBest( + final ChannelBinding channelBinding, final SSLSockets.Version sslVersion) { + return ChannelBinding.best(Collections.singleton(channelBinding), sslVersion) + == channelBinding; + } + + private static String shortName(final ChannelBinding channelBinding) { + switch (channelBinding) { + case TLS_UNIQUE: + return "UNIQ"; + case TLS_EXPORTER: + return "EXPR"; + case TLS_SERVER_END_POINT: + return "ENDP"; + case NONE: + return "NONE"; + default: + throw new AssertionError("Missing short name for " + channelBinding); + } } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java new file mode 100644 index 000000000..f973c8377 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java @@ -0,0 +1,114 @@ +package eu.siacs.conversations.crypto.sasl; + +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.Multimap; +import com.google.common.hash.HashFunction; + +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import javax.net.ssl.SSLSocket; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.utils.SSLSockets; + +public abstract class HashedToken extends SaslMechanism { + + private static List HASH_FUNCTIONS = Arrays.asList("SHA-512", "SHA-256"); + + protected final ChannelBinding channelBinding; + + protected HashedToken(final Account account, final ChannelBinding channelBinding) { + super(account); + this.channelBinding = channelBinding; + } + + @Override + public int getPriority() { + throw new UnsupportedOperationException(); + } + + @Override + public String getClientFirstMessage() { + return null; // HMAC(token, "Initiator" || cb-data) + } + + @Override + public String getResponse(final String challenge, final SSLSocket socket) + throws AuthenticationException { + // todo verify that challenge matches HMAC(token, "Responder" || cb-data) + return null; + } + + protected abstract HashFunction getHashFunction(final byte[] key); + + public static final class Mechanism { + public final String hashFunction; + public final ChannelBinding channelBinding; + + public Mechanism(String hashFunction, ChannelBinding channelBinding) { + this.hashFunction = hashFunction; + this.channelBinding = channelBinding; + } + + public static Mechanism of(final String mechanism) { + final int first = mechanism.indexOf('-'); + final int last = mechanism.lastIndexOf('-'); + if (last <= first || mechanism.length() <= last) { + throw new IllegalArgumentException("Not a valid HashedToken name"); + } + if (mechanism.substring(0, first).equals("HT")) { + final String hashFunction = mechanism.substring(first + 1, last); + final String cbShortName = mechanism.substring(last + 1); + final ChannelBinding channelBinding = + ChannelBinding.SHORT_NAMES.inverse().get(cbShortName); + if (channelBinding == null) { + throw new IllegalArgumentException("Unknown channel binding " + cbShortName); + } + return new Mechanism(hashFunction, channelBinding); + } else { + throw new IllegalArgumentException("HashedToken name does not start with HT"); + } + } + + public static Multimap of(final Collection mechanisms) { + final ImmutableMultimap.Builder builder = + ImmutableMultimap.builder(); + for (final String name : mechanisms) { + try { + final Mechanism mechanism = Mechanism.of(name); + builder.put(mechanism.hashFunction, mechanism.channelBinding); + } catch (final IllegalArgumentException ignored) { + } + } + return builder.build(); + } + + public static Mechanism best( + final Collection mechanisms, final SSLSockets.Version sslVersion) { + final Multimap multimap = of(mechanisms); + for (final String hashFunction : HASH_FUNCTIONS) { + final Collection channelBindings = multimap.get(hashFunction); + if (channelBindings.isEmpty()) { + continue; + } + final ChannelBinding cb = ChannelBinding.best(channelBindings, sslVersion); + return new Mechanism(hashFunction, cb); + } + return null; + } + + @NotNull + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("hashFunction", hashFunction) + .add("channelBinding", channelBinding) + .toString(); + } + } +} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha256.java b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha256.java new file mode 100644 index 000000000..fae756485 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha256.java @@ -0,0 +1,24 @@ +package eu.siacs.conversations.crypto.sasl; + +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; + +import eu.siacs.conversations.entities.Account; + +public class HashedTokenSha256 extends HashedToken { + + public HashedTokenSha256(final Account account, final ChannelBinding channelBinding) { + super(account, channelBinding); + } + + @Override + protected HashFunction getHashFunction(final byte[] key) { + return Hashing.hmacSha256(key); + } + + @Override + public String getMechanism() { + final String cbShortName = ChannelBinding.SHORT_NAMES.get(this.channelBinding); + return String.format("HT-SHA-256-%s", cbShortName); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index e693b2afa..f2b4c932a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -63,6 +63,7 @@ import eu.siacs.conversations.crypto.XmppDomainVerifier; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.sasl.ChannelBinding; +import eu.siacs.conversations.crypto.sasl.HashedToken; import eu.siacs.conversations.crypto.sasl.SaslMechanism; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Message; @@ -1344,7 +1345,7 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio final boolean sm = inline != null && inline.hasChild("sm", "urn:xmpp:sm:3"); final Element fast = inline == null ? null : inline.findChild("fast", Namespace.FAST); final Collection fastMechanisms = SaslMechanism.mechanisms(fast); - Log.d(Config.LOGTAG,"fast mechanisms: "+fastMechanisms); + Log.d(Config.LOGTAG,"fast mechanism: "+ HashedToken.Mechanism.best(fastMechanisms, SSLSockets.version(this.socket))); final Collection bindFeatures = Bind2.features(inline); quickStartAvailable = sm From c13787873c0324a1e40ae8c5c4700f297bd4a31f Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 15 Oct 2022 12:27:38 +0200 Subject: [PATCH 223/394] request fast token --- .../crypto/sasl/HashedToken.java | 12 ++++++++-- .../crypto/sasl/HashedTokenSha512.java | 24 +++++++++++++++++++ .../conversations/xmpp/XmppConnection.java | 18 +++++++++++--- 3 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha512.java diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java index f973c8377..b54864fea 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java @@ -18,7 +18,9 @@ public abstract class HashedToken extends SaslMechanism { - private static List HASH_FUNCTIONS = Arrays.asList("SHA-512", "SHA-256"); + private static final String PREFIX = "HT"; + + private static final List HASH_FUNCTIONS = Arrays.asList("SHA-512", "SHA-256"); protected final ChannelBinding channelBinding; @@ -61,7 +63,7 @@ public static Mechanism of(final String mechanism) { if (last <= first || mechanism.length() <= last) { throw new IllegalArgumentException("Not a valid HashedToken name"); } - if (mechanism.substring(0, first).equals("HT")) { + if (mechanism.substring(0, first).equals(PREFIX)) { final String hashFunction = mechanism.substring(first + 1, last); final String cbShortName = mechanism.substring(last + 1); final ChannelBinding channelBinding = @@ -110,5 +112,11 @@ public String toString() { .add("channelBinding", channelBinding) .toString(); } + + public String name() { + return String.format( + "%s-%s-%s", + PREFIX, hashFunction, ChannelBinding.SHORT_NAMES.get(channelBinding)); + } } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha512.java b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha512.java new file mode 100644 index 000000000..fd1b7be51 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha512.java @@ -0,0 +1,24 @@ +package eu.siacs.conversations.crypto.sasl; + +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; + +import eu.siacs.conversations.entities.Account; + +public class HashedTokenSha512 extends HashedToken { + + public HashedTokenSha512(final Account account, final ChannelBinding channelBinding) { + super(account, channelBinding); + } + + @Override + protected HashFunction getHashFunction(final byte[] key) { + return Hashing.hmacSha512(key); + } + + @Override + public String getMechanism() { + final String cbShortName = ChannelBinding.SHORT_NAMES.get(this.channelBinding); + return String.format("HT-SHA-512-%s", cbShortName); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index f2b4c932a..2fa8d6df3 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -181,6 +181,7 @@ public class XmppConnection implements Runnable { private OnBindListener bindListener = null; private OnMessageAcknowledged acknowledgedListener = null; private SaslMechanism saslMechanism; + private HashedToken.Mechanism hashTokenRequest; private HttpUrl redirectionUrl = null; private String verifiedHostname = null; private volatile Thread mThread; @@ -761,6 +762,8 @@ private boolean processSuccess(final Element success) final Element bound = success.findChild("bound", Namespace.BIND2); final Element resumed = success.findChild("resumed", "urn:xmpp:sm:3"); final Element failed = success.findChild("failed", "urn:xmpp:sm:3"); + final Element tokenWrapper = success.findChild("token", Namespace.FAST); + final String token = tokenWrapper == null ? null : tokenWrapper.getAttribute("token"); if (bound != null && resumed != null) { Log.d( Config.LOGTAG, @@ -795,6 +798,10 @@ private boolean processSuccess(final Element success) } sendPostBindInitialization(waitForDisco, carbonsEnabled != null); } + //TODO figure out name either by the existence of hashTokenRequest or if scramMechanism is of instance HashedToken + if (this.hashTokenRequest != null && !Strings.isNullOrEmpty(token)) { + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": storing hashed token "+this.hashTokenRequest.name()+ " "+token); + } } this.quickStartInProgress = false; if (version == SaslMechanism.Version.SASL) { @@ -1345,7 +1352,7 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio final boolean sm = inline != null && inline.hasChild("sm", "urn:xmpp:sm:3"); final Element fast = inline == null ? null : inline.findChild("fast", Namespace.FAST); final Collection fastMechanisms = SaslMechanism.mechanisms(fast); - Log.d(Config.LOGTAG,"fast mechanism: "+ HashedToken.Mechanism.best(fastMechanisms, SSLSockets.version(this.socket))); + final HashedToken.Mechanism hashTokenRequest = HashedToken.Mechanism.best(fastMechanisms, SSLSockets.version(this.socket)); final Collection bindFeatures = Bind2.features(inline); quickStartAvailable = sm @@ -1362,7 +1369,8 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio return; } } - authenticate = generateAuthenticationRequest(firstMessage, bindFeatures, sm); + this.hashTokenRequest = hashTokenRequest; + authenticate = generateAuthenticationRequest(firstMessage, hashTokenRequest, bindFeatures, sm); } else { throw new AssertionError("Missing implementation for " + version); } @@ -1383,11 +1391,12 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio } private Element generateAuthenticationRequest(final String firstMessage) { - return generateAuthenticationRequest(firstMessage, Bind2.QUICKSTART_FEATURES, true); + return generateAuthenticationRequest(firstMessage, null, Bind2.QUICKSTART_FEATURES, true); } private Element generateAuthenticationRequest( final String firstMessage, + final HashedToken.Mechanism hashedTokenRequest, final Collection bind, final boolean inlineStreamManagement) { final Element authenticate = new Element("authenticate", Namespace.SASL_2); @@ -1413,6 +1422,9 @@ private Element generateAuthenticationRequest( this.mWaitingForSmCatchup.set(true); authenticate.addChild(resume); } + if (hashedTokenRequest != null) { + authenticate.addChild("request-token", Namespace.FAST).setAttribute("mechanism", hashedTokenRequest.name()); + } return authenticate; } From 24badda4c9d99f597cb12f24ad8bd236195c7ce2 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 15 Oct 2022 18:56:31 +0200 Subject: [PATCH 224/394] do quick start with HT-SHA-256-NONE --- .../java/eu/siacs/conversations/Config.java | 2 +- .../crypto/sasl/ChannelBinding.java | 2 +- .../crypto/sasl/ChannelBindingMechanism.java | 6 + .../crypto/sasl/HashedToken.java | 57 ++++++- .../crypto/sasl/HashedTokenSha256.java | 5 +- .../crypto/sasl/HashedTokenSha512.java | 5 +- .../crypto/sasl/SaslMechanism.java | 6 +- .../crypto/sasl/ScramPlusMechanism.java | 3 +- .../siacs/conversations/entities/Account.java | 144 ++++++++++++++---- .../persistance/DatabaseBackend.java | 9 +- .../conversations/xmpp/XmppConnection.java | 60 +++++--- 11 files changed, 237 insertions(+), 62 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 27d1f1097..812f6ae10 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -60,7 +60,7 @@ public static boolean multipleEncryptionChoices() { public static final long CONTACT_SYNC_RETRY_INTERVAL = 1000L * 60 * 5; - public static final boolean QUICKSTART_ENABLED = false; + public static final boolean QUICKSTART_ENABLED = true; //Notification settings public static final boolean HIDE_MESSAGE_TEXT_IN_NOTIFICATION = false; diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java index 26f9c9da0..216f3d7f8 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java @@ -97,7 +97,7 @@ public static ChannelBinding best( } } - public static boolean ensureBest( + public static boolean isAvailable( final ChannelBinding channelBinding, final SSLSockets.Version sslVersion) { return ChannelBinding.best(Collections.singleton(channelBinding), sslVersion) == channelBinding; diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java new file mode 100644 index 000000000..d4e34ba59 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java @@ -0,0 +1,6 @@ +package eu.siacs.conversations.crypto.sasl; + +public interface ChannelBindingMechanism { + + ChannelBinding getChannelBinding(); +} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java index b54864fea..1d8aeac69 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java @@ -1,12 +1,17 @@ package eu.siacs.conversations.crypto.sasl; +import android.util.Base64; + import com.google.common.base.MoreObjects; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.Multimap; import com.google.common.hash.HashFunction; +import com.google.common.primitives.Bytes; import org.jetbrains.annotations.NotNull; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -16,11 +21,13 @@ import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.utils.SSLSockets; -public abstract class HashedToken extends SaslMechanism { +public abstract class HashedToken extends SaslMechanism implements ChannelBindingMechanism { private static final String PREFIX = "HT"; private static final List HASH_FUNCTIONS = Arrays.asList("SHA-512", "SHA-256"); + private static final byte[] INITIATOR = "Initiator".getBytes(StandardCharsets.UTF_8); + private static final byte[] RESPONDER = "Responder".getBytes(StandardCharsets.UTF_8); protected final ChannelBinding channelBinding; @@ -36,18 +43,48 @@ public int getPriority() { @Override public String getClientFirstMessage() { - return null; // HMAC(token, "Initiator" || cb-data) + final String token = Strings.nullToEmpty(this.account.getFastToken()); + final HashFunction hashing = getHashFunction(token.getBytes(StandardCharsets.UTF_8)); + final byte[] cbData = new byte[0]; + final byte[] initiatorHashedToken = + hashing.hashBytes(Bytes.concat(INITIATOR, cbData)).asBytes(); + final byte[] firstMessage = + Bytes.concat( + account.getUsername().getBytes(StandardCharsets.UTF_8), + new byte[] {0x00}, + initiatorHashedToken); + return Base64.encodeToString(firstMessage, Base64.NO_WRAP); } @Override public String getResponse(final String challenge, final SSLSocket socket) throws AuthenticationException { - // todo verify that challenge matches HMAC(token, "Responder" || cb-data) - return null; + final byte[] responderMessage; + try { + responderMessage = Base64.decode(challenge, Base64.NO_WRAP); + } catch (final Exception e) { + throw new AuthenticationException("Unable to decode responder message", e); + } + final String token = Strings.nullToEmpty(this.account.getFastToken()); + final HashFunction hashing = getHashFunction(token.getBytes(StandardCharsets.UTF_8)); + final byte[] cbData = new byte[0]; + final byte[] expectedResponderMessage = + hashing.hashBytes(Bytes.concat(RESPONDER, cbData)).asBytes(); + if (Arrays.equals(responderMessage, expectedResponderMessage)) { + return null; + } + throw new AuthenticationException("Responder message did not match"); } protected abstract HashFunction getHashFunction(final byte[] key); + public abstract Mechanism getTokenMechanism(); + + @Override + public String getMechanism() { + return getTokenMechanism().name(); + } + public static final class Mechanism { public final String hashFunction; public final ChannelBinding channelBinding; @@ -77,6 +114,14 @@ public static Mechanism of(final String mechanism) { } } + public static Mechanism ofOrNull(final String mechanism) { + try { + return mechanism == null ? null : of(mechanism); + } catch (final IllegalArgumentException e) { + return null; + } + } + public static Multimap of(final Collection mechanisms) { final ImmutableMultimap.Builder builder = ImmutableMultimap.builder(); @@ -119,4 +164,8 @@ public String name() { PREFIX, hashFunction, ChannelBinding.SHORT_NAMES.get(channelBinding)); } } + + public ChannelBinding getChannelBinding() { + return this.channelBinding; + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha256.java b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha256.java index fae756485..aef19d72a 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha256.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha256.java @@ -17,8 +17,7 @@ protected HashFunction getHashFunction(final byte[] key) { } @Override - public String getMechanism() { - final String cbShortName = ChannelBinding.SHORT_NAMES.get(this.channelBinding); - return String.format("HT-SHA-256-%s", cbShortName); + public Mechanism getTokenMechanism() { + return new Mechanism("SHA-256", channelBinding); } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha512.java b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha512.java index fd1b7be51..6f48b5444 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha512.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha512.java @@ -17,8 +17,7 @@ protected HashFunction getHashFunction(final byte[] key) { } @Override - public String getMechanism() { - final String cbShortName = ChannelBinding.SHORT_NAMES.get(this.channelBinding); - return String.format("HT-SHA-512-%s", cbShortName); + public Mechanism getTokenMechanism() { + return new Mechanism("SHA-512", this.channelBinding); } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java index e0df3a2d4..48835f9df 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java @@ -166,9 +166,9 @@ public SaslMechanism of(final String mechanism, final ChannelBinding channelBind public static SaslMechanism ensureAvailable( final SaslMechanism mechanism, final SSLSockets.Version sslVersion) { - if (mechanism instanceof ScramPlusMechanism) { - final ChannelBinding cb = ((ScramPlusMechanism) mechanism).getChannelBinding(); - if (ChannelBinding.ensureBest(cb, sslVersion)) { + if (mechanism instanceof ChannelBindingMechanism) { + final ChannelBinding cb = ((ChannelBindingMechanism) mechanism).getChannelBinding(); + if (ChannelBinding.isAvailable(cb, sslVersion)) { return mechanism; } else { Log.d( diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java index 707883d73..c6a63ddbd 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java @@ -16,7 +16,7 @@ import eu.siacs.conversations.entities.Account; -public abstract class ScramPlusMechanism extends ScramMechanism { +public abstract class ScramPlusMechanism extends ScramMechanism implements ChannelBindingMechanism { private static final String EXPORTER_LABEL = "EXPORTER-Channel-Binding"; @@ -103,6 +103,7 @@ private byte[] getServerEndPointChannelBinding(final SSLSession session) return messageDigest.digest(); } + @Override public ChannelBinding getChannelBinding() { return this.channelBinding; } diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 1817c24bb..d570cbec3 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -26,6 +26,9 @@ import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; import eu.siacs.conversations.crypto.sasl.ChannelBinding; +import eu.siacs.conversations.crypto.sasl.HashedToken; +import eu.siacs.conversations.crypto.sasl.HashedTokenSha256; +import eu.siacs.conversations.crypto.sasl.HashedTokenSha512; import eu.siacs.conversations.crypto.sasl.SaslMechanism; import eu.siacs.conversations.crypto.sasl.ScramPlusMechanism; import eu.siacs.conversations.services.AvatarService; @@ -55,7 +58,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable public static final String RESOURCE = "resource"; public static final String PINNED_MECHANISM = "pinned_mechanism"; public static final String PINNED_CHANNEL_BINDING = "pinned_channel_binding"; - + public static final String FAST_MECHANISM = "fast_mechanism"; + public static final String FAST_TOKEN = "fast_token"; public static final int OPTION_DISABLED = 1; public static final int OPTION_REGISTER = 2; @@ -72,7 +76,6 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable private static final String KEY_PINNED_MECHANISM = "pinned_mechanism"; public static final String KEY_PRE_AUTH_REGISTRATION_TOKEN = "pre_auth_registration"; - protected final JSONObject keys; private final Roster roster = new Roster(this); private final Collection blocklist = new CopyOnWriteArraySet<>(); @@ -101,16 +104,46 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable private String presenceStatusMessage; private String pinnedMechanism; private String pinnedChannelBinding; + private String fastMechanism; + private String fastToken; public Account(final Jid jid, final String password) { - this(java.util.UUID.randomUUID().toString(), jid, - password, 0, null, "", null, null, null, 5222, Presence.Status.ONLINE, null, null, null); - } - - private Account(final String uuid, final Jid jid, - final String password, final int options, final String rosterVersion, final String keys, - final String avatar, String displayName, String hostname, int port, - final Presence.Status status, String statusMessage, final String pinnedMechanism, final String pinnedChannelBinding) { + this( + java.util.UUID.randomUUID().toString(), + jid, + password, + 0, + null, + "", + null, + null, + null, + 5222, + Presence.Status.ONLINE, + null, + null, + null, + null, + null); + } + + private Account( + final String uuid, + final Jid jid, + final String password, + final int options, + final String rosterVersion, + final String keys, + final String avatar, + String displayName, + String hostname, + int port, + final Presence.Status status, + String statusMessage, + final String pinnedMechanism, + final String pinnedChannelBinding, + final String fastMechanism, + final String fastToken) { this.uuid = uuid; this.jid = jid; this.password = password; @@ -131,21 +164,29 @@ private Account(final String uuid, final Jid jid, this.presenceStatusMessage = statusMessage; this.pinnedMechanism = pinnedMechanism; this.pinnedChannelBinding = pinnedChannelBinding; + this.fastMechanism = fastMechanism; + this.fastToken = fastToken; } public static Account fromCursor(final Cursor cursor) { final Jid jid; try { final String resource = cursor.getString(cursor.getColumnIndexOrThrow(RESOURCE)); - jid = Jid.of( - cursor.getString(cursor.getColumnIndexOrThrow(USERNAME)), - cursor.getString(cursor.getColumnIndexOrThrow(SERVER)), - resource == null || resource.trim().isEmpty() ? null : resource); + jid = + Jid.of( + cursor.getString(cursor.getColumnIndexOrThrow(USERNAME)), + cursor.getString(cursor.getColumnIndexOrThrow(SERVER)), + resource == null || resource.trim().isEmpty() ? null : resource); } catch (final IllegalArgumentException e) { - Log.d(Config.LOGTAG, cursor.getString(cursor.getColumnIndexOrThrow(USERNAME)) + "@" + cursor.getString(cursor.getColumnIndexOrThrow(SERVER))); + Log.d( + Config.LOGTAG, + cursor.getString(cursor.getColumnIndexOrThrow(USERNAME)) + + "@" + + cursor.getString(cursor.getColumnIndexOrThrow(SERVER))); throw new AssertionError(e); } - return new Account(cursor.getString(cursor.getColumnIndexOrThrow(UUID)), + return new Account( + cursor.getString(cursor.getColumnIndexOrThrow(UUID)), jid, cursor.getString(cursor.getColumnIndexOrThrow(PASSWORD)), cursor.getInt(cursor.getColumnIndexOrThrow(OPTIONS)), @@ -155,10 +196,13 @@ public static Account fromCursor(final Cursor cursor) { cursor.getString(cursor.getColumnIndexOrThrow(DISPLAY_NAME)), cursor.getString(cursor.getColumnIndexOrThrow(HOSTNAME)), cursor.getInt(cursor.getColumnIndexOrThrow(PORT)), - Presence.Status.fromShowString(cursor.getString(cursor.getColumnIndexOrThrow(STATUS))), + Presence.Status.fromShowString( + cursor.getString(cursor.getColumnIndexOrThrow(STATUS))), cursor.getString(cursor.getColumnIndexOrThrow(STATUS_MESSAGE)), cursor.getString(cursor.getColumnIndexOrThrow(PINNED_MECHANISM)), - cursor.getString(cursor.getColumnIndexOrThrow(PINNED_CHANNEL_BINDING))); + cursor.getString(cursor.getColumnIndexOrThrow(PINNED_CHANNEL_BINDING)), + cursor.getString(cursor.getColumnIndexOrThrow(FAST_MECHANISM)), + cursor.getString(cursor.getColumnIndexOrThrow(FAST_TOKEN))); } public boolean httpUploadAvailable(long size) { @@ -305,10 +349,18 @@ public void setStatus(final State status) { public void setPinnedMechanism(final SaslMechanism mechanism) { this.pinnedMechanism = mechanism.getMechanism(); if (mechanism instanceof ScramPlusMechanism) { - this.pinnedChannelBinding = ((ScramPlusMechanism) mechanism).getChannelBinding().toString(); + this.pinnedChannelBinding = + ((ScramPlusMechanism) mechanism).getChannelBinding().toString(); + } else { + this.pinnedChannelBinding = null; } } + public void setFastToken(final HashedToken.Mechanism mechanism, final String token) { + this.fastMechanism = mechanism.name(); + this.fastToken = token; + } + public void resetPinnedMechanism() { this.pinnedMechanism = null; this.pinnedChannelBinding = null; @@ -328,12 +380,39 @@ public int getPinnedMechanismPriority() { } } - public SaslMechanism getPinnedMechanism() { + private SaslMechanism getPinnedMechanism() { final String mechanism = Strings.nullToEmpty(this.pinnedMechanism); final ChannelBinding channelBinding = ChannelBinding.get(this.pinnedChannelBinding); return new SaslMechanism.Factory(this).of(mechanism, channelBinding); } + private HashedToken getFastMechanism() { + final HashedToken.Mechanism fastMechanism = HashedToken.Mechanism.ofOrNull(this.fastMechanism); + final String token = this.fastToken; + if (fastMechanism == null || Strings.isNullOrEmpty(token)) { + return null; + } + if (fastMechanism.hashFunction.equals("SHA-256")) { + return new HashedTokenSha256(this, fastMechanism.channelBinding); + } else if (fastMechanism.hashFunction.equals("SHA-512")) { + return new HashedTokenSha512(this, fastMechanism.channelBinding); + } else { + return null; + } + } + + public SaslMechanism getQuickStartMechanism() { + final HashedToken hashedTokenMechanism = getFastMechanism(); + if (hashedTokenMechanism != null) { + return hashedTokenMechanism; + } + return getPinnedMechanism(); + } + + public String getFastToken() { + return this.fastToken; + } + public State getTrueStatus() { return this.status; } @@ -435,6 +514,8 @@ public ContentValues getContentValues() { values.put(RESOURCE, jid.getResource()); values.put(PINNED_MECHANISM, pinnedMechanism); values.put(PINNED_CHANNEL_BINDING, pinnedChannelBinding); + values.put(FAST_MECHANISM, this.fastMechanism); + values.put(FAST_TOKEN, this.fastToken); return values; } @@ -480,7 +561,7 @@ public int countPresences() { public int activeDevicesWithRtpCapability() { int i = 0; - for(Presence presence : getSelfContact().getPresences().getPresences()) { + for (Presence presence : getSelfContact().getPresences().getPresences()) { if (RtpCapability.check(presence) != RtpCapability.Capability.NONE) { i++; } @@ -617,7 +698,9 @@ public String getShareableUri() { public String getShareableLink() { List fingerprints = this.getFingerprints(); - String uri = "https://conversations.im/i/" + XmppUri.lameUrlEncode(this.getJid().asBareJid().toEscapedString()); + String uri = + "https://conversations.im/i/" + + XmppUri.lameUrlEncode(this.getJid().asBareJid().toEscapedString()); if (fingerprints.size() > 0) { return XmppUri.getFingerprintUri(uri, fingerprints, '&'); } else { @@ -630,10 +713,18 @@ private List getFingerprints() { if (axolotlService == null) { return fingerprints; } - fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO, axolotlService.getOwnFingerprint().substring(2), axolotlService.getOwnDeviceId())); + fingerprints.add( + new XmppUri.Fingerprint( + XmppUri.FingerprintType.OMEMO, + axolotlService.getOwnFingerprint().substring(2), + axolotlService.getOwnDeviceId())); for (XmppAxolotlSession session : axolotlService.findOwnSessions()) { if (session.getTrust().isVerified() && session.getTrust().isActive()) { - fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO, session.getFingerprint().substring(2).replaceAll("\\s", ""), session.getRemoteAddress().getDeviceId())); + fingerprints.add( + new XmppUri.Fingerprint( + XmppUri.FingerprintType.OMEMO, + session.getFingerprint().substring(2).replaceAll("\\s", ""), + session.getRemoteAddress().getDeviceId())); } } return fingerprints; @@ -641,7 +732,8 @@ private List getFingerprints() { public boolean isBlocked(final ListItem contact) { final Jid jid = contact.getJid(); - return jid != null && (blocklist.contains(jid.asBareJid()) || blocklist.contains(jid.getDomain())); + return jid != null + && (blocklist.contains(jid.asBareJid()) || blocklist.contains(jid.getDomain())); } public boolean isBlocked(final Jid jid) { @@ -685,7 +777,7 @@ public enum State { REGISTRATION_CONFLICT(true, false), REGISTRATION_NOT_SUPPORTED(true, false), REGISTRATION_PLEASE_WAIT(true, false), - REGISTRATION_INVALID_TOKEN(true,false), + REGISTRATION_INVALID_TOKEN(true, false), REGISTRATION_PASSWORD_TOO_WEAK(true, false), TLS_ERROR, TLS_ERROR_DOMAIN, diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 49de553eb..3eb48a1c9 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -64,7 +64,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { private static final String DATABASE_NAME = "history"; - private static final int DATABASE_VERSION = 50; + private static final int DATABASE_VERSION = 51; private static boolean requiresMessageIndexRebuild = false; private static DatabaseBackend instance = null; @@ -232,6 +232,8 @@ public void onCreate(SQLiteDatabase db) { + Account.RESOURCE + " TEXT," + Account.PINNED_MECHANISM + " TEXT," + Account.PINNED_CHANNEL_BINDING + " TEXT," + + Account.FAST_MECHANISM + " TEXT," + + Account.FAST_TOKEN + " TEXT," + Account.PORT + " NUMBER DEFAULT 5222)"); db.execSQL("create table " + Conversation.TABLENAME + " (" + Conversation.UUID + " TEXT PRIMARY KEY, " + Conversation.NAME @@ -594,7 +596,10 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion < 50 && newVersion >= 50) { db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.PINNED_MECHANISM + " TEXT"); db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.PINNED_CHANNEL_BINDING + " TEXT"); - + } + if (oldVersion < 51 && newVersion >= 51) { + db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.FAST_MECHANISM + " TEXT"); + db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.FAST_TOKEN + " TEXT"); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 2fa8d6df3..2c60534dd 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -757,7 +757,6 @@ private boolean processSuccess(final Element success) Config.LOGTAG, account.getJid().asBareJid() + ": jid changed during SASL 2.0. updating database"); - mXmppConnectionService.databaseBackend.updateAccount(account); } final Element bound = success.findChild("bound", Namespace.BIND2); final Element resumed = success.findChild("resumed", "urn:xmpp:sm:3"); @@ -798,11 +797,21 @@ private boolean processSuccess(final Element success) } sendPostBindInitialization(waitForDisco, carbonsEnabled != null); } - //TODO figure out name either by the existence of hashTokenRequest or if scramMechanism is of instance HashedToken - if (this.hashTokenRequest != null && !Strings.isNullOrEmpty(token)) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": storing hashed token "+this.hashTokenRequest.name()+ " "+token); + final HashedToken.Mechanism tokenMechanism; + final SaslMechanism currentMechanism = this.saslMechanism; + if (currentMechanism instanceof HashedToken) { + tokenMechanism = ((HashedToken) currentMechanism).getTokenMechanism(); + } else if (this.hashTokenRequest != null) { + tokenMechanism = this.hashTokenRequest; + } else { + tokenMechanism = null; + } + if (tokenMechanism != null && !Strings.isNullOrEmpty(token)) { + this.account.setFastToken(tokenMechanism,token); + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": storing hashed token "+tokenMechanism); } } + mXmppConnectionService.databaseBackend.updateAccount(account); this.quickStartInProgress = false; if (version == SaslMechanism.Version.SASL) { tagReader.reset(); @@ -826,6 +835,7 @@ private void processFailure(final Element failure) throws StateChangingException } catch (final IllegalArgumentException e) { throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } + Log.d(Config.LOGTAG,failure.toString()); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": login failure " + version); if (failure.hasChild("temporary-auth-failure")) { throw new StateChangingException(Account.State.TEMPORARY_AUTH_FAILURE); @@ -1340,6 +1350,7 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio } final boolean quickStartAvailable; final String firstMessage = saslMechanism.getClientFirstMessage(); + final boolean usingFast = saslMechanism instanceof HashedToken; final Element authenticate; if (version == SaslMechanism.Version.SASL) { authenticate = new Element("auth", Namespace.SASL); @@ -1350,9 +1361,15 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio } else if (version == SaslMechanism.Version.SASL_2) { final Element inline = authElement.findChild("inline", Namespace.SASL_2); final boolean sm = inline != null && inline.hasChild("sm", "urn:xmpp:sm:3"); - final Element fast = inline == null ? null : inline.findChild("fast", Namespace.FAST); - final Collection fastMechanisms = SaslMechanism.mechanisms(fast); - final HashedToken.Mechanism hashTokenRequest = HashedToken.Mechanism.best(fastMechanisms, SSLSockets.version(this.socket)); + final HashedToken.Mechanism hashTokenRequest; + if (usingFast) { + hashTokenRequest = null; + } else { + final Element fast = inline == null ? null : inline.findChild("fast", Namespace.FAST); + final Collection fastMechanisms = SaslMechanism.mechanisms(fast); + hashTokenRequest = + HashedToken.Mechanism.best(fastMechanisms, SSLSockets.version(this.socket)); + } final Collection bindFeatures = Bind2.features(inline); quickStartAvailable = sm @@ -1370,7 +1387,7 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio } } this.hashTokenRequest = hashTokenRequest; - authenticate = generateAuthenticationRequest(firstMessage, hashTokenRequest, bindFeatures, sm); + authenticate = generateAuthenticationRequest(firstMessage, usingFast, hashTokenRequest, bindFeatures, sm); } else { throw new AssertionError("Missing implementation for " + version); } @@ -1390,12 +1407,13 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio tagWriter.writeElement(authenticate); } - private Element generateAuthenticationRequest(final String firstMessage) { - return generateAuthenticationRequest(firstMessage, null, Bind2.QUICKSTART_FEATURES, true); + private Element generateAuthenticationRequest(final String firstMessage, final boolean usingFast) { + return generateAuthenticationRequest(firstMessage, usingFast, null, Bind2.QUICKSTART_FEATURES, true); } private Element generateAuthenticationRequest( final String firstMessage, + final boolean usingFast, final HashedToken.Mechanism hashedTokenRequest, final Collection bind, final boolean inlineStreamManagement) { @@ -1423,7 +1441,12 @@ private Element generateAuthenticationRequest( authenticate.addChild(resume); } if (hashedTokenRequest != null) { - authenticate.addChild("request-token", Namespace.FAST).setAttribute("mechanism", hashedTokenRequest.name()); + authenticate + .addChild("request-token", Namespace.FAST) + .setAttribute("mechanism", hashedTokenRequest.name()); + } + if (usingFast) { + authenticate.addChild("fast", Namespace.FAST); } return authenticate; } @@ -2059,25 +2082,26 @@ private void failPendingMessages(final String error) { private boolean establishStream(final SSLSockets.Version sslVersion) throws IOException, InterruptedException { - final SaslMechanism pinnedMechanism = - SaslMechanism.ensureAvailable(account.getPinnedMechanism(), sslVersion); + final SaslMechanism quickStartMechanism = + SaslMechanism.ensureAvailable(account.getQuickStartMechanism(), sslVersion); final boolean secureConnection = sslVersion != SSLSockets.Version.NONE; if (secureConnection && Config.QUICKSTART_ENABLED - && pinnedMechanism != null + && quickStartMechanism != null && account.isOptionSet(Account.OPTION_QUICKSTART_AVAILABLE)) { mXmppConnectionService.restoredFromDatabaseLatch.await(); - this.saslMechanism = pinnedMechanism; + this.saslMechanism = quickStartMechanism; + final boolean usingFast = quickStartMechanism instanceof HashedToken; final Element authenticate = - generateAuthenticationRequest(pinnedMechanism.getClientFirstMessage()); - authenticate.setAttribute("mechanism", pinnedMechanism.getMechanism()); + generateAuthenticationRequest(quickStartMechanism.getClientFirstMessage(), usingFast); + authenticate.setAttribute("mechanism", quickStartMechanism.getMechanism()); sendStartStream(true, false); tagWriter.writeElement(authenticate); Log.d( Config.LOGTAG, account.getJid().toString() + ": quick start with " - + pinnedMechanism.getMechanism()); + + quickStartMechanism.getMechanism()); return true; } else { sendStartStream(secureConnection, true); From e2b9f0e77ac25a9ffa1cb173f8e2bdf121f53695 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 15 Oct 2022 20:53:59 +0200 Subject: [PATCH 225/394] add support for HashedToken channel binding --- .../conversations/crypto/sasl/Anonymous.java | 4 +- .../crypto/sasl/ChannelBindingMechanism.java | 94 +++++++++++++++++++ .../conversations/crypto/sasl/External.java | 4 +- .../crypto/sasl/HashedToken.java | 25 ++++- .../conversations/crypto/sasl/Plain.java | 4 +- .../crypto/sasl/SaslMechanism.java | 15 ++- .../crypto/sasl/ScramMechanism.java | 2 +- .../crypto/sasl/ScramPlusMechanism.java | 89 +----------------- .../siacs/conversations/entities/Account.java | 7 +- .../conversations/xmpp/XmppConnection.java | 75 ++++++++------- 10 files changed, 187 insertions(+), 132 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/Anonymous.java b/src/main/java/eu/siacs/conversations/crypto/sasl/Anonymous.java index 22cf80e65..6fc4b11ce 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/Anonymous.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/Anonymous.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.crypto.sasl; +import javax.net.ssl.SSLSocket; + import eu.siacs.conversations.entities.Account; public class Anonymous extends SaslMechanism { @@ -21,7 +23,7 @@ public String getMechanism() { } @Override - public String getClientFirstMessage() { + public String getClientFirstMessage(final SSLSocket sslSocket) { return ""; } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java index d4e34ba59..b94210a60 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java @@ -1,6 +1,100 @@ package eu.siacs.conversations.crypto.sasl; +import org.bouncycastle.jcajce.provider.digest.SHA256; +import org.conscrypt.Conscrypt; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; + +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; + public interface ChannelBindingMechanism { + String EXPORTER_LABEL = "EXPORTER-Channel-Binding"; + ChannelBinding getChannelBinding(); + + static byte[] getChannelBindingData(final SSLSocket sslSocket, final ChannelBinding channelBinding) + throws SaslMechanism.AuthenticationException { + if (sslSocket == null) { + throw new SaslMechanism.AuthenticationException("Channel binding attempt on non secure socket"); + } + if (channelBinding == ChannelBinding.TLS_EXPORTER) { + final byte[] keyingMaterial; + try { + keyingMaterial = + Conscrypt.exportKeyingMaterial(sslSocket, EXPORTER_LABEL, new byte[0], 32); + } catch (final SSLException e) { + throw new SaslMechanism.AuthenticationException("Could not export keying material"); + } + if (keyingMaterial == null) { + throw new SaslMechanism.AuthenticationException( + "Could not export keying material. Socket not ready"); + } + return keyingMaterial; + } else if (channelBinding == ChannelBinding.TLS_UNIQUE) { + final byte[] unique = Conscrypt.getTlsUnique(sslSocket); + if (unique == null) { + throw new SaslMechanism.AuthenticationException( + "Could not retrieve tls unique. Socket not ready"); + } + return unique; + } else if (channelBinding == ChannelBinding.TLS_SERVER_END_POINT) { + return getServerEndPointChannelBinding(sslSocket.getSession()); + } else { + throw new SaslMechanism.AuthenticationException( + String.format("%s is not a valid channel binding", channelBinding)); + } + } + + static byte[] getServerEndPointChannelBinding(final SSLSession session) + throws SaslMechanism.AuthenticationException { + final Certificate[] certificates; + try { + certificates = session.getPeerCertificates(); + } catch (final SSLPeerUnverifiedException e) { + throw new SaslMechanism.AuthenticationException("Could not verify peer certificates"); + } + if (certificates == null || certificates.length == 0) { + throw new SaslMechanism.AuthenticationException("Could not retrieve peer certificate"); + } + final X509Certificate certificate; + if (certificates[0] instanceof X509Certificate) { + certificate = (X509Certificate) certificates[0]; + } else { + throw new SaslMechanism.AuthenticationException("Certificate was not X509"); + } + final String algorithm = certificate.getSigAlgName(); + final int withIndex = algorithm.indexOf("with"); + if (withIndex <= 0) { + throw new SaslMechanism.AuthenticationException("Unable to parse SigAlgName"); + } + final String hashAlgorithm = algorithm.substring(0, withIndex); + final MessageDigest messageDigest; + // https://www.rfc-editor.org/rfc/rfc5929#section-4.1 + if ("MD5".equalsIgnoreCase(hashAlgorithm) || "SHA1".equalsIgnoreCase(hashAlgorithm)) { + messageDigest = new SHA256.Digest(); + } else { + try { + messageDigest = MessageDigest.getInstance(hashAlgorithm); + } catch (final NoSuchAlgorithmException e) { + throw new SaslMechanism.AuthenticationException( + "Could not instantiate message digest for " + hashAlgorithm); + } + } + final byte[] encodedCertificate; + try { + encodedCertificate = certificate.getEncoded(); + } catch (final CertificateEncodingException e) { + throw new SaslMechanism.AuthenticationException("Could not encode certificate"); + } + messageDigest.update(encodedCertificate); + return messageDigest.digest(); + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/External.java b/src/main/java/eu/siacs/conversations/crypto/sasl/External.java index 06323f039..6aba413a5 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/External.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/External.java @@ -2,6 +2,8 @@ import android.util.Base64; +import javax.net.ssl.SSLSocket; + import eu.siacs.conversations.entities.Account; public class External extends SaslMechanism { @@ -23,7 +25,7 @@ public String getMechanism() { } @Override - public String getClientFirstMessage() { + public String getClientFirstMessage(final SSLSocket sslSocket) { return Base64.encodeToString( account.getJid().asBareJid().toEscapedString().getBytes(), Base64.NO_WRAP); } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java index 1d8aeac69..d3595b9e4 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java @@ -1,6 +1,7 @@ package eu.siacs.conversations.crypto.sasl; import android.util.Base64; +import android.util.Log; import com.google.common.base.MoreObjects; import com.google.common.base.Strings; @@ -18,6 +19,7 @@ import javax.net.ssl.SSLSocket; +import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.utils.SSLSockets; @@ -42,10 +44,10 @@ public int getPriority() { } @Override - public String getClientFirstMessage() { + public String getClientFirstMessage(final SSLSocket sslSocket) { final String token = Strings.nullToEmpty(this.account.getFastToken()); final HashFunction hashing = getHashFunction(token.getBytes(StandardCharsets.UTF_8)); - final byte[] cbData = new byte[0]; + final byte[] cbData = getChannelBindingData(sslSocket); final byte[] initiatorHashedToken = hashing.hashBytes(Bytes.concat(INITIATOR, cbData)).asBytes(); final byte[] firstMessage = @@ -56,6 +58,23 @@ public String getClientFirstMessage() { return Base64.encodeToString(firstMessage, Base64.NO_WRAP); } + private byte[] getChannelBindingData(final SSLSocket sslSocket) { + if (this.channelBinding == ChannelBinding.NONE) { + return new byte[0]; + } + try { + return ChannelBindingMechanism.getChannelBindingData(sslSocket, this.channelBinding); + } catch (final AuthenticationException e) { + Log.e( + Config.LOGTAG, + account.getJid().asBareJid() + + ": unable to retrieve channel binding data for " + + getMechanism(), + e); + return new byte[0]; + } + } + @Override public String getResponse(final String challenge, final SSLSocket socket) throws AuthenticationException { @@ -67,7 +86,7 @@ public String getResponse(final String challenge, final SSLSocket socket) } final String token = Strings.nullToEmpty(this.account.getFastToken()); final HashFunction hashing = getHashFunction(token.getBytes(StandardCharsets.UTF_8)); - final byte[] cbData = new byte[0]; + final byte[] cbData = getChannelBindingData(socket); final byte[] expectedResponderMessage = hashing.hashBytes(Bytes.concat(RESPONDER, cbData)).asBytes(); if (Arrays.equals(responderMessage, expectedResponderMessage)) { diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java b/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java index 875538bec..2be5d0bcb 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java @@ -4,6 +4,8 @@ import java.nio.charset.Charset; +import javax.net.ssl.SSLSocket; + import eu.siacs.conversations.entities.Account; public class Plain extends SaslMechanism { @@ -30,7 +32,7 @@ public String getMechanism() { } @Override - public String getClientFirstMessage() { + public String getClientFirstMessage(final SSLSocket sslSocket) { return getMessage(account.getUsername(), account.getPassword()); } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java index 48835f9df..b8d1d0465 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java @@ -44,7 +44,7 @@ public static String namespace(final Version version) { public abstract String getMechanism(); - public String getClientFirstMessage() { + public String getClientFirstMessage(final SSLSocket sslSocket) { return ""; } @@ -154,7 +154,12 @@ private SaslMechanism of( public SaslMechanism of( final Collection mechanisms, final Collection bindings, + final Version version, final SSLSockets.Version sslVersion) { + final HashedToken fastMechanism = account.getFastMechanism(); + if (version == Version.SASL_2 && fastMechanism != null) { + return fastMechanism; + } final ChannelBinding channelBinding = ChannelBinding.best(bindings, sslVersion); return of(mechanisms, channelBinding); } @@ -180,4 +185,12 @@ public static SaslMechanism ensureAvailable( return mechanism; } } + + public static boolean hashedToken(final SaslMechanism saslMechanism) { + return saslMechanism instanceof HashedToken; + } + + public static boolean pin(final SaslMechanism saslMechanism) { + return !hashedToken(saslMechanism); + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java index aba434e3a..5825df29d 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java @@ -112,7 +112,7 @@ private byte[] hi(final byte[] key, final byte[] salt, final int iterations) } @Override - public String getClientFirstMessage() { + public String getClientFirstMessage(final SSLSocket sslSocket) { if (clientFirstMessageBare.isEmpty() && state == State.INITIAL) { clientFirstMessageBare = "n=" diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java index c6a63ddbd..0c836933f 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java @@ -1,25 +1,11 @@ package eu.siacs.conversations.crypto.sasl; -import org.bouncycastle.jcajce.provider.digest.SHA256; -import org.conscrypt.Conscrypt; - -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.cert.Certificate; -import java.security.cert.CertificateEncodingException; -import java.security.cert.X509Certificate; - -import javax.net.ssl.SSLException; -import javax.net.ssl.SSLPeerUnverifiedException; -import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; import eu.siacs.conversations.entities.Account; public abstract class ScramPlusMechanism extends ScramMechanism implements ChannelBindingMechanism { - private static final String EXPORTER_LABEL = "EXPORTER-Channel-Binding"; - ScramPlusMechanism(Account account, ChannelBinding channelBinding) { super(account, channelBinding); } @@ -27,80 +13,7 @@ public abstract class ScramPlusMechanism extends ScramMechanism implements Chann @Override protected byte[] getChannelBindingData(final SSLSocket sslSocket) throws AuthenticationException { - if (sslSocket == null) { - throw new AuthenticationException("Channel binding attempt on non secure socket"); - } - if (this.channelBinding == ChannelBinding.TLS_EXPORTER) { - final byte[] keyingMaterial; - try { - keyingMaterial = - Conscrypt.exportKeyingMaterial(sslSocket, EXPORTER_LABEL, new byte[0], 32); - } catch (final SSLException e) { - throw new AuthenticationException("Could not export keying material"); - } - if (keyingMaterial == null) { - throw new AuthenticationException( - "Could not export keying material. Socket not ready"); - } - return keyingMaterial; - } else if (this.channelBinding == ChannelBinding.TLS_UNIQUE) { - final byte[] unique = Conscrypt.getTlsUnique(sslSocket); - if (unique == null) { - throw new AuthenticationException( - "Could not retrieve tls unique. Socket not ready"); - } - return unique; - } else if (this.channelBinding == ChannelBinding.TLS_SERVER_END_POINT) { - return getServerEndPointChannelBinding(sslSocket.getSession()); - } else { - throw new AuthenticationException( - String.format("%s is not a valid channel binding", channelBinding)); - } - } - - private byte[] getServerEndPointChannelBinding(final SSLSession session) - throws AuthenticationException { - final Certificate[] certificates; - try { - certificates = session.getPeerCertificates(); - } catch (final SSLPeerUnverifiedException e) { - throw new AuthenticationException("Could not verify peer certificates"); - } - if (certificates == null || certificates.length == 0) { - throw new AuthenticationException("Could not retrieve peer certificate"); - } - final X509Certificate certificate; - if (certificates[0] instanceof X509Certificate) { - certificate = (X509Certificate) certificates[0]; - } else { - throw new AuthenticationException("Certificate was not X509"); - } - final String algorithm = certificate.getSigAlgName(); - final int withIndex = algorithm.indexOf("with"); - if (withIndex <= 0) { - throw new AuthenticationException("Unable to parse SigAlgName"); - } - final String hashAlgorithm = algorithm.substring(0, withIndex); - final MessageDigest messageDigest; - // https://www.rfc-editor.org/rfc/rfc5929#section-4.1 - if ("MD5".equalsIgnoreCase(hashAlgorithm) || "SHA1".equalsIgnoreCase(hashAlgorithm)) { - messageDigest = new SHA256.Digest(); - } else { - try { - messageDigest = MessageDigest.getInstance(hashAlgorithm); - } catch (final NoSuchAlgorithmException e) { - throw new AuthenticationException( - "Could not instantiate message digest for " + hashAlgorithm); - } - } - final byte[] encodedCertificate; - try { - encodedCertificate = certificate.getEncoded(); - } catch (final CertificateEncodingException e) { - throw new AuthenticationException("Could not encode certificate"); - } - messageDigest.update(encodedCertificate); - return messageDigest.digest(); + return ChannelBindingMechanism.getChannelBindingData(sslSocket, this.channelBinding); } @Override diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index d570cbec3..4457e4d1f 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -26,6 +26,7 @@ import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; import eu.siacs.conversations.crypto.sasl.ChannelBinding; +import eu.siacs.conversations.crypto.sasl.ChannelBindingMechanism; import eu.siacs.conversations.crypto.sasl.HashedToken; import eu.siacs.conversations.crypto.sasl.HashedTokenSha256; import eu.siacs.conversations.crypto.sasl.HashedTokenSha512; @@ -348,9 +349,9 @@ public void setStatus(final State status) { public void setPinnedMechanism(final SaslMechanism mechanism) { this.pinnedMechanism = mechanism.getMechanism(); - if (mechanism instanceof ScramPlusMechanism) { + if (mechanism instanceof ChannelBindingMechanism) { this.pinnedChannelBinding = - ((ScramPlusMechanism) mechanism).getChannelBinding().toString(); + ((ChannelBindingMechanism) mechanism).getChannelBinding().toString(); } else { this.pinnedChannelBinding = null; } @@ -386,7 +387,7 @@ private SaslMechanism getPinnedMechanism() { return new SaslMechanism.Factory(this).of(mechanism, channelBinding); } - private HashedToken getFastMechanism() { + public HashedToken getFastMechanism() { final HashedToken.Mechanism fastMechanism = HashedToken.Mechanism.ofOrNull(this.fastMechanism); final String token = this.fastToken; if (fastMechanism == null || Strings.isNullOrEmpty(token)) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 2c60534dd..fc753ec34 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -14,6 +14,7 @@ import android.util.SparseArray; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.common.base.Strings; @@ -704,7 +705,9 @@ private boolean processSuccess(final Element success) Log.d( Config.LOGTAG, account.getJid().asBareJid().toString() + ": logged in (using " + version + ")"); - account.setPinnedMechanism(saslMechanism); + if (SaslMechanism.pin(this.saslMechanism)) { + account.setPinnedMechanism(this.saslMechanism); + } if (version == SaslMechanism.Version.SASL_2) { final Tag tag = tagReader.readTag(); if (tag != null && tag.isStart("features", Namespace.STREAMS)) { @@ -837,6 +840,7 @@ private void processFailure(final Element failure) throws StateChangingException } Log.d(Config.LOGTAG,failure.toString()); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": login failure " + version); + //TODO check if we are doing FAST; reset token if (failure.hasChild("temporary-auth-failure")) { throw new StateChangingException(Account.State.TEMPORARY_AUTH_FAILURE); } else if (failure.hasChild("account-disabled")) { @@ -1242,6 +1246,7 @@ private void processStreamFeatures(final Tag currentTag) throws IOException { account.getJid().asBareJid() + ": quick start in progress. ignoring features: " + XmlHelper.printElementNames(this.streamFeatures)); + //TODO check if 'fast' is available but we are doing something else return; } Log.d(Config.LOGTAG,account.getJid().asBareJid()+": server lost support for SASL 2. quick start not possible"); @@ -1320,37 +1325,12 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio final Element cbElement = this.streamFeatures.findChild("sasl-channel-binding", Namespace.CHANNEL_BINDING); final Collection channelBindings = ChannelBinding.of(cbElement); - Log.d(Config.LOGTAG,"mechanisms: "+mechanisms); - Log.d(Config.LOGTAG, "channel bindings: " + channelBindings); final SaslMechanism.Factory factory = new SaslMechanism.Factory(account); - this.saslMechanism = factory.of(mechanisms, channelBindings, SSLSockets.version(this.socket)); - - //TODO externalize checks - - if (saslMechanism == null) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": unable to find supported SASL mechanism in " - + mechanisms); - throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); - } - final int pinnedMechanism = account.getPinnedMechanismPriority(); - if (pinnedMechanism > saslMechanism.getPriority()) { - Log.e( - Config.LOGTAG, - "Auth failed. Authentication mechanism " - + saslMechanism.getMechanism() - + " has lower priority (" - + saslMechanism.getPriority() - + ") than pinned priority (" - + pinnedMechanism - + "). Possible downgrade attack?"); - throw new StateChangingException(Account.State.DOWNGRADE_ATTACK); - } + final SaslMechanism saslMechanism = factory.of(mechanisms, channelBindings, version, SSLSockets.version(this.socket)); + this.saslMechanism = validate(saslMechanism, mechanisms); final boolean quickStartAvailable; - final String firstMessage = saslMechanism.getClientFirstMessage(); - final boolean usingFast = saslMechanism instanceof HashedToken; + final String firstMessage = this.saslMechanism.getClientFirstMessage(sslSocketOrNull(this.socket)); + final boolean usingFast = SaslMechanism.hashedToken(this.saslMechanism); final Element authenticate; if (version == SaslMechanism.Version.SASL) { authenticate = new Element("auth", Namespace.SASL); @@ -1402,11 +1382,40 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio + ": Authenticating with " + version + "/" - + saslMechanism.getMechanism()); - authenticate.setAttribute("mechanism", saslMechanism.getMechanism()); + + this.saslMechanism.getMechanism()); + authenticate.setAttribute("mechanism", this.saslMechanism.getMechanism()); tagWriter.writeElement(authenticate); } + @NonNull + private SaslMechanism validate(final @Nullable SaslMechanism saslMechanism, Collection mechanisms) throws StateChangingException { + if (saslMechanism == null) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": unable to find supported SASL mechanism in " + + mechanisms); + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } + if (SaslMechanism.hashedToken(saslMechanism)) { + return saslMechanism; + } + final int pinnedMechanism = account.getPinnedMechanismPriority(); + if (pinnedMechanism > saslMechanism.getPriority()) { + Log.e( + Config.LOGTAG, + "Auth failed. Authentication mechanism " + + saslMechanism.getMechanism() + + " has lower priority (" + + saslMechanism.getPriority() + + ") than pinned priority (" + + pinnedMechanism + + "). Possible downgrade attack?"); + throw new StateChangingException(Account.State.DOWNGRADE_ATTACK); + } + return saslMechanism; + } + private Element generateAuthenticationRequest(final String firstMessage, final boolean usingFast) { return generateAuthenticationRequest(firstMessage, usingFast, null, Bind2.QUICKSTART_FEATURES, true); } @@ -2093,7 +2102,7 @@ private boolean establishStream(final SSLSockets.Version sslVersion) this.saslMechanism = quickStartMechanism; final boolean usingFast = quickStartMechanism instanceof HashedToken; final Element authenticate = - generateAuthenticationRequest(quickStartMechanism.getClientFirstMessage(), usingFast); + generateAuthenticationRequest(quickStartMechanism.getClientFirstMessage(sslSocketOrNull(this.socket)), usingFast); authenticate.setAttribute("mechanism", quickStartMechanism.getMechanism()); sendStartStream(true, false); tagWriter.writeElement(authenticate); From a29c7c725ec64c5f5b20b8df75931db828b0ba8d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 24 Oct 2022 13:11:30 +0200 Subject: [PATCH 226/394] modify scram mechanisms to use guava hashing --- .../crypto/sasl/ScramMechanism.java | 29 ++++++------------- .../conversations/crypto/sasl/ScramSha1.java | 13 ++++----- .../crypto/sasl/ScramSha1Plus.java | 13 ++++----- .../crypto/sasl/ScramSha256.java | 12 ++++---- .../crypto/sasl/ScramSha256Plus.java | 13 ++++----- .../crypto/sasl/ScramSha512.java | 11 ++++--- .../crypto/sasl/ScramSha512Plus.java | 13 ++++----- 7 files changed, 47 insertions(+), 57 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java index 5825df29d..931debe01 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java @@ -1,15 +1,13 @@ package eu.siacs.conversations.crypto.sasl; import android.util.Base64; +import android.util.Log; import com.google.common.base.CaseFormat; import com.google.common.base.Objects; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; - -import org.bouncycastle.crypto.Digest; -import org.bouncycastle.crypto.macs.HMac; -import org.bouncycastle.crypto.params.KeyParameter; +import com.google.common.hash.HashFunction; import java.nio.charset.Charset; import java.security.InvalidKeyException; @@ -17,6 +15,7 @@ import javax.net.ssl.SSLSocket; +import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.utils.CryptoHelper; @@ -54,14 +53,14 @@ abstract class ScramMechanism extends SaslMechanism { clientFirstMessageBare = ""; } - protected abstract HMac getHMAC(); + protected abstract HashFunction getHMac(final byte[] key); - protected abstract Digest getDigest(); + protected abstract HashFunction getDigest(); private KeyPair getKeyPair(final String password, final String salt, final int iterations) throws ExecutionException { return CACHE.get( - new CacheKey(getHMAC().getAlgorithmName(), password, salt, iterations), + new CacheKey(getMechanism(), password, salt, iterations), () -> { final byte[] saltedPassword, serverKey, clientKey; saltedPassword = @@ -76,21 +75,11 @@ private KeyPair getKeyPair(final String password, final String salt, final int i } private byte[] hmac(final byte[] key, final byte[] input) throws InvalidKeyException { - final HMac hMac = getHMAC(); - hMac.init(new KeyParameter(key)); - hMac.update(input, 0, input.length); - final byte[] out = new byte[hMac.getMacSize()]; - hMac.doFinal(out, 0); - return out; + return getHMac(key).hashBytes(input).asBytes(); } - public byte[] digest(final byte[] bytes) { - final Digest digest = getDigest(); - digest.reset(); - digest.update(bytes, 0, bytes.length); - final byte[] out = new byte[digest.getDigestSize()]; - digest.doFinal(out, 0); - return out; + private byte[] digest(final byte[] bytes) { + return getDigest().hashBytes(bytes).asBytes(); } /* diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java index 9bcc8ad47..6f00c49eb 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java @@ -1,8 +1,7 @@ package eu.siacs.conversations.crypto.sasl; -import org.bouncycastle.crypto.Digest; -import org.bouncycastle.crypto.digests.SHA1Digest; -import org.bouncycastle.crypto.macs.HMac; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; import eu.siacs.conversations.entities.Account; @@ -15,13 +14,13 @@ public ScramSha1(final Account account) { } @Override - protected HMac getHMAC() { - return new HMac(new SHA1Digest()); + protected HashFunction getHMac(final byte[] key) { + return Hashing.hmacSha1(key); } @Override - protected Digest getDigest() { - return new SHA1Digest(); + protected HashFunction getDigest() { + return Hashing.sha1(); } @Override diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java index d4f2fcb0b..d353bd9ee 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java @@ -1,8 +1,7 @@ package eu.siacs.conversations.crypto.sasl; -import org.bouncycastle.crypto.Digest; -import org.bouncycastle.crypto.digests.SHA1Digest; -import org.bouncycastle.crypto.macs.HMac; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; import eu.siacs.conversations.entities.Account; @@ -15,13 +14,13 @@ public ScramSha1Plus(final Account account, final ChannelBinding channelBinding) } @Override - protected HMac getHMAC() { - return new HMac(new SHA1Digest()); + protected HashFunction getHMac(final byte[] key) { + return Hashing.hmacSha1(key); } @Override - protected Digest getDigest() { - return new SHA1Digest(); + protected HashFunction getDigest() { + return Hashing.sha1(); } @Override diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java index 610ed788b..9d7d62c36 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java @@ -1,5 +1,8 @@ package eu.siacs.conversations.crypto.sasl; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; + import org.bouncycastle.crypto.Digest; import org.bouncycastle.crypto.digests.SHA256Digest; import org.bouncycastle.crypto.macs.HMac; @@ -15,15 +18,14 @@ public ScramSha256(final Account account) { } @Override - protected HMac getHMAC() { - return new HMac(new SHA256Digest()); + protected HashFunction getHMac(final byte[] key) { + return Hashing.hmacSha256(key); } @Override - protected Digest getDigest() { - return new SHA256Digest(); + protected HashFunction getDigest() { + return Hashing.sha256(); } - @Override public int getPriority() { return 25; diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256Plus.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256Plus.java index f48a052ab..5f15e9bf1 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256Plus.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256Plus.java @@ -1,8 +1,7 @@ package eu.siacs.conversations.crypto.sasl; -import org.bouncycastle.crypto.Digest; -import org.bouncycastle.crypto.digests.SHA256Digest; -import org.bouncycastle.crypto.macs.HMac; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; import eu.siacs.conversations.entities.Account; @@ -15,13 +14,13 @@ public ScramSha256Plus(final Account account, final ChannelBinding channelBindin } @Override - protected HMac getHMAC() { - return new HMac(new SHA256Digest()); + protected HashFunction getHMac(final byte[] key) { + return Hashing.hmacSha256(key); } @Override - protected Digest getDigest() { - return new SHA256Digest(); + protected HashFunction getDigest() { + return Hashing.sha256(); } @Override diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java index 3d54b39e9..8194ac0ac 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java @@ -1,5 +1,8 @@ package eu.siacs.conversations.crypto.sasl; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; + import org.bouncycastle.crypto.Digest; import org.bouncycastle.crypto.digests.SHA512Digest; import org.bouncycastle.crypto.macs.HMac; @@ -15,13 +18,13 @@ public ScramSha512(final Account account) { } @Override - protected HMac getHMAC() { - return new HMac(new SHA512Digest()); + protected HashFunction getHMac(final byte[] key) { + return Hashing.hmacSha512(key); } @Override - protected Digest getDigest() { - return new SHA512Digest(); + protected HashFunction getDigest() { + return Hashing.sha512(); } @Override diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512Plus.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512Plus.java index 8cec1f33f..610c87e23 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512Plus.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512Plus.java @@ -1,8 +1,7 @@ package eu.siacs.conversations.crypto.sasl; -import org.bouncycastle.crypto.Digest; -import org.bouncycastle.crypto.digests.SHA512Digest; -import org.bouncycastle.crypto.macs.HMac; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; import eu.siacs.conversations.entities.Account; @@ -15,13 +14,13 @@ public ScramSha512Plus(final Account account, final ChannelBinding channelBindin } @Override - protected HMac getHMAC() { - return new HMac(new SHA512Digest()); + protected HashFunction getHMac(final byte[] key) { + return Hashing.hmacSha512(key); } @Override - protected Digest getDigest() { - return new SHA512Digest(); + protected HashFunction getDigest() { + return Hashing.sha512(); } @Override From 6e562d4cf906e50a793bb0a968028dc197051ebf Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 25 Oct 2022 12:44:51 +0200 Subject: [PATCH 227/394] Update README.md --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 5f62f6195..037e04106 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,6 @@

Conversations: the very last word in instant messaging

- - chat on our conference room - build status From 35ee01cb285bc0c711aaedbdb057de86979cbc58 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 1 Nov 2022 16:44:05 +0100 Subject: [PATCH 228/394] reset fast token on login failure --- src/main/java/eu/siacs/conversations/entities/Account.java | 5 +++++ .../java/eu/siacs/conversations/xmpp/XmppConnection.java | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 4457e4d1f..7c5f22b27 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -362,6 +362,11 @@ public void setFastToken(final HashedToken.Mechanism mechanism, final String tok this.fastToken = token; } + public void resetFastToken() { + this.fastMechanism = null; + this.fastToken = null; + } + public void resetPinnedMechanism() { this.pinnedMechanism = null; this.pinnedChannelBinding = null; diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index fc753ec34..1793c9f29 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -840,7 +840,11 @@ private void processFailure(final Element failure) throws StateChangingException } Log.d(Config.LOGTAG,failure.toString()); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": login failure " + version); - //TODO check if we are doing FAST; reset token + if (this.saslMechanism instanceof HashedToken) { + Log.d(Config.LOGTAG,account.getJid().asBareJid() + ": resetting token"); + account.resetFastToken(); + mXmppConnectionService.databaseBackend.updateAccount(account); + } if (failure.hasChild("temporary-auth-failure")) { throw new StateChangingException(Account.State.TEMPORARY_AUTH_FAILURE); } else if (failure.hasChild("account-disabled")) { From 7e29d1d86258adc904017a8dd9b3965da3c1374e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 1 Nov 2022 16:44:22 +0100 Subject: [PATCH 229/394] update gradle --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 27e620963..6ccdcf705 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.2.2' + classpath 'com.android.tools.build:gradle:7.3.1' } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e639f29f3..d33b7f161 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip From dac2e1713384a3458dcceef18ae8e3de1fd4abc4 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 1 Nov 2022 18:06:32 +0100 Subject: [PATCH 230/394] =?UTF-8?q?disable=20quick=20start=20if=20fast=20i?= =?UTF-8?q?s=20available=20but=20we=20didn=E2=80=99t=20use=20fast?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../conversations/xmpp/XmppConnection.java | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 1793c9f29..ce3731536 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -802,7 +802,7 @@ private boolean processSuccess(final Element success) } final HashedToken.Mechanism tokenMechanism; final SaslMechanism currentMechanism = this.saslMechanism; - if (currentMechanism instanceof HashedToken) { + if (SaslMechanism.hashedToken(currentMechanism)) { tokenMechanism = ((HashedToken) currentMechanism).getTokenMechanism(); } else if (this.hashTokenRequest != null) { tokenMechanism = this.hashTokenRequest; @@ -840,7 +840,7 @@ private void processFailure(final Element failure) throws StateChangingException } Log.d(Config.LOGTAG,failure.toString()); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": login failure " + version); - if (this.saslMechanism instanceof HashedToken) { + if (SaslMechanism.hashedToken(this.saslMechanism)) { Log.d(Config.LOGTAG,account.getJid().asBareJid() + ": resetting token"); account.resetFastToken(); mXmppConnectionService.databaseBackend.updateAccount(account); @@ -1250,12 +1250,26 @@ private void processStreamFeatures(final Tag currentTag) throws IOException { account.getJid().asBareJid() + ": quick start in progress. ignoring features: " + XmlHelper.printElementNames(this.streamFeatures)); - //TODO check if 'fast' is available but we are doing something else + if (SaslMechanism.hashedToken(this.saslMechanism)) { + return; + } + if (isFastTokenAvailable( + this.streamFeatures.findChild("authentication", Namespace.SASL_2))) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": fast token available; resetting quick start"); + account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, false); + mXmppConnectionService.databaseBackend.updateAccount(account); + } return; } - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": server lost support for SASL 2. quick start not possible"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": server lost support for SASL 2. quick start not possible"); this.account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, false); - mXmppConnectionService.updateAccount(account); + mXmppConnectionService.databaseBackend.updateAccount(account); throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } if (this.streamFeatures.hasChild("starttls", Namespace.TLS) @@ -1377,7 +1391,7 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio } if (account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, quickStartAvailable)) { - mXmppConnectionService.updateAccount(account); + mXmppConnectionService.databaseBackend.updateAccount(account); } Log.d( @@ -1391,6 +1405,11 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio tagWriter.writeElement(authenticate); } + private static boolean isFastTokenAvailable(final Element authentication) { + final Element inline = authentication == null ? null : authentication.findChild("inline"); + return inline != null && inline.hasChild("fast", Namespace.FAST); + } + @NonNull private SaslMechanism validate(final @Nullable SaslMechanism saslMechanism, Collection mechanisms) throws StateChangingException { if (saslMechanism == null) { From 5dbd86155fa0eda09124f23db67a75a5ad99b542 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 10 Nov 2022 07:54:47 +0100 Subject: [PATCH 231/394] show help button only if Config.HELP is set --- src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 302fbf81d..f9c7177a2 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -164,7 +164,7 @@ public boolean onCreateOptionsMenu(final Menu menu) { getMenuInflater().inflate(R.menu.activity_rtp_session, menu); final MenuItem help = menu.findItem(R.id.action_help); final MenuItem gotoChat = menu.findItem(R.id.action_goto_chat); - help.setVisible(isHelpButtonVisible()); + help.setVisible(Config.HELP != null && isHelpButtonVisible()); gotoChat.setVisible(isSwitchToConversationVisible()); return super.onCreateOptionsMenu(menu); } From 6ececb4d2bda38e95c8085bb2a9e927a40ebf39d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 12 Nov 2022 13:37:18 +0100 Subject: [PATCH 232/394] refactor webrtc video source + capture code --- .../xmpp/jingle/TrackWrapper.java | 31 + .../xmpp/jingle/VideoSourceWrapper.java | 181 +++++ .../xmpp/jingle/WebRTCWrapper.java | 635 +++++++++--------- 3 files changed, 518 insertions(+), 329 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/TrackWrapper.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/VideoSourceWrapper.java diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/TrackWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/TrackWrapper.java new file mode 100644 index 000000000..4e2952127 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/TrackWrapper.java @@ -0,0 +1,31 @@ +package eu.siacs.conversations.xmpp.jingle; + +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; + +import org.webrtc.MediaStreamTrack; +import org.webrtc.PeerConnection; +import org.webrtc.RtpSender; + +class TrackWrapper { + private final T track; + private final RtpSender rtpSender; + + private TrackWrapper(final T track, final RtpSender rtpSender) { + Preconditions.checkNotNull(track); + Preconditions.checkNotNull(rtpSender); + this.track = track; + this.rtpSender = rtpSender; + } + + public static TrackWrapper addTrack( + final PeerConnection peerConnection, final T mediaStreamTrack) { + final RtpSender rtpSender = peerConnection.addTrack(mediaStreamTrack); + return new TrackWrapper<>(mediaStreamTrack, rtpSender); + } + + public static Optional get( + final TrackWrapper trackWrapper) { + return trackWrapper == null ? Optional.absent() : Optional.of(trackWrapper.track); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/VideoSourceWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/VideoSourceWrapper.java new file mode 100644 index 000000000..5e83f2ba9 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/VideoSourceWrapper.java @@ -0,0 +1,181 @@ +package eu.siacs.conversations.xmpp.jingle; + +import android.content.Context; +import android.util.Log; + +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; + +import org.webrtc.Camera2Enumerator; +import org.webrtc.CameraEnumerationAndroid; +import org.webrtc.CameraEnumerator; +import org.webrtc.CameraVideoCapturer; +import org.webrtc.EglBase; +import org.webrtc.PeerConnectionFactory; +import org.webrtc.SurfaceTextureHelper; +import org.webrtc.VideoSource; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Set; + +import javax.annotation.Nullable; + +import eu.siacs.conversations.Config; + +class VideoSourceWrapper { + + private static final int CAPTURING_RESOLUTION = 1920; + private static final int CAPTURING_MAX_FRAME_RATE = 30; + + private final CameraVideoCapturer cameraVideoCapturer; + private final CameraEnumerationAndroid.CaptureFormat captureFormat; + private final Set availableCameras; + private boolean isFrontCamera = false; + private VideoSource videoSource; + + VideoSourceWrapper( + CameraVideoCapturer cameraVideoCapturer, + CameraEnumerationAndroid.CaptureFormat captureFormat, + Set cameras) { + this.cameraVideoCapturer = cameraVideoCapturer; + this.captureFormat = captureFormat; + this.availableCameras = cameras; + } + + private int getFrameRate() { + return Math.max( + captureFormat.framerate.min, + Math.min(CAPTURING_MAX_FRAME_RATE, captureFormat.framerate.max)); + } + + public void initialize( + final PeerConnectionFactory peerConnectionFactory, + final Context context, + final EglBase.Context eglBaseContext) { + final SurfaceTextureHelper surfaceTextureHelper = + SurfaceTextureHelper.create("webrtc", eglBaseContext); + this.videoSource = peerConnectionFactory.createVideoSource(false); + this.cameraVideoCapturer.initialize( + surfaceTextureHelper, context, this.videoSource.getCapturerObserver()); + } + + public VideoSource getVideoSource() { + final VideoSource videoSource = this.videoSource; + if (videoSource == null) { + throw new IllegalStateException("VideoSourceWrapper was not initialized"); + } + return videoSource; + } + + public void startCapture() { + final int frameRate = getFrameRate(); + Log.d( + Config.LOGTAG, + String.format( + "start capturing at %dx%d@%d", + captureFormat.width, captureFormat.height, frameRate)); + this.cameraVideoCapturer.startCapture(captureFormat.width, captureFormat.height, frameRate); + } + + public void stopCapture() throws InterruptedException { + this.cameraVideoCapturer.stopCapture(); + } + + public void dispose() { + this.cameraVideoCapturer.dispose(); + if (this.videoSource != null) { + this.videoSource.dispose(); + } + } + + public ListenableFuture switchCamera() { + final SettableFuture future = SettableFuture.create(); + this.cameraVideoCapturer.switchCamera( + new CameraVideoCapturer.CameraSwitchHandler() { + @Override + public void onCameraSwitchDone(final boolean isFrontCamera) { + VideoSourceWrapper.this.isFrontCamera = isFrontCamera; + future.set(isFrontCamera); + } + + @Override + public void onCameraSwitchError(final String message) { + future.setException( + new IllegalStateException( + String.format("Unable to switch camera %s", message))); + } + }); + return future; + } + + public boolean isFrontCamera() { + return this.isFrontCamera; + } + + public boolean isCameraSwitchable() { + return this.availableCameras.size() > 1; + } + + public static class Factory { + final Context context; + + public Factory(final Context context) { + this.context = context; + } + + public Optional create() { + final CameraEnumerator enumerator = new Camera2Enumerator(context); + final Set deviceNames = ImmutableSet.copyOf(enumerator.getDeviceNames()); + for (final String deviceName : deviceNames) { + if (isFrontFacing(enumerator, deviceName)) { + final VideoSourceWrapper videoSourceWrapper = + of(enumerator, deviceName, deviceNames); + if (videoSourceWrapper == null) { + return Optional.absent(); + } + videoSourceWrapper.isFrontCamera = true; + return Optional.of(videoSourceWrapper); + } + } + if (deviceNames.size() == 0) { + return Optional.absent(); + } else { + return Optional.fromNullable( + of(enumerator, Iterables.get(deviceNames, 0), deviceNames)); + } + } + + @Nullable + private VideoSourceWrapper of( + final CameraEnumerator enumerator, + final String deviceName, + final Set availableCameras) { + final CameraVideoCapturer capturer = enumerator.createCapturer(deviceName, null); + if (capturer == null) { + return null; + } + final ArrayList choices = + new ArrayList<>(enumerator.getSupportedFormats(deviceName)); + Collections.sort(choices, (a, b) -> b.width - a.width); + for (final CameraEnumerationAndroid.CaptureFormat captureFormat : choices) { + if (captureFormat.width <= CAPTURING_RESOLUTION) { + return new VideoSourceWrapper(capturer, captureFormat, availableCameras); + } + } + return null; + } + + private static boolean isFrontFacing( + final CameraEnumerator cameraEnumerator, final String deviceName) { + try { + return cameraEnumerator.isFrontFacing(deviceName); + } catch (final NullPointerException e) { + return false; + } + } + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 8cd65447b..f71799bdf 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -9,7 +9,6 @@ import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Iterables; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; @@ -17,10 +16,6 @@ import org.webrtc.AudioSource; import org.webrtc.AudioTrack; -import org.webrtc.Camera2Enumerator; -import org.webrtc.CameraEnumerationAndroid; -import org.webrtc.CameraEnumerator; -import org.webrtc.CameraVideoCapturer; import org.webrtc.CandidatePairChangeEvent; import org.webrtc.DataChannel; import org.webrtc.DefaultVideoDecoderFactory; @@ -36,14 +31,10 @@ import org.webrtc.RtpTransceiver; import org.webrtc.SdpObserver; import org.webrtc.SessionDescription; -import org.webrtc.SurfaceTextureHelper; -import org.webrtc.VideoSource; import org.webrtc.VideoTrack; import org.webrtc.audio.JavaAudioDeviceModule; import org.webrtc.voiceengine.WebRtcAudioEffects; -import java.util.ArrayList; -import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Queue; @@ -59,140 +50,158 @@ import eu.siacs.conversations.services.AppRTCAudioManager; import eu.siacs.conversations.services.XmppConnectionService; +@SuppressWarnings("UnstableApiUsage") public class WebRTCWrapper { private static final String EXTENDED_LOGGING_TAG = WebRTCWrapper.class.getSimpleName(); private final ExecutorService executorService = Executors.newSingleThreadExecutor(); - - private static final Set HARDWARE_AEC_BLACKLIST = new ImmutableSet.Builder() - .add("Pixel") - .add("Pixel XL") - .add("Moto G5") - .add("Moto G (5S) Plus") - .add("Moto G4") - .add("TA-1053") - .add("Mi A1") - .add("Mi A2") - .add("E5823") // Sony z5 compact - .add("Redmi Note 5") - .add("FP2") // Fairphone FP2 - .add("MI 5") - .add("GT-I9515") // Samsung Galaxy S4 Value Edition (jfvelte) - .add("GT-I9515L") // Samsung Galaxy S4 Value Edition (jfvelte) - .add("GT-I9505") // Samsung Galaxy S4 (jfltexx) - .build(); - - private static final int CAPTURING_RESOLUTION = 1920; - private static final int CAPTURING_MAX_FRAME_RATE = 30; + + private static final Set HARDWARE_AEC_BLACKLIST = + new ImmutableSet.Builder() + .add("Pixel") + .add("Pixel XL") + .add("Moto G5") + .add("Moto G (5S) Plus") + .add("Moto G4") + .add("TA-1053") + .add("Mi A1") + .add("Mi A2") + .add("E5823") // Sony z5 compact + .add("Redmi Note 5") + .add("FP2") // Fairphone FP2 + .add("MI 5") + .add("GT-I9515") // Samsung Galaxy S4 Value Edition (jfvelte) + .add("GT-I9515L") // Samsung Galaxy S4 Value Edition (jfvelte) + .add("GT-I9505") // Samsung Galaxy S4 (jfltexx) + .build(); private final EventCallback eventCallback; private final AtomicBoolean readyToReceivedIceCandidates = new AtomicBoolean(false); private final Queue iceCandidates = new LinkedList<>(); - private final AppRTCAudioManager.AudioManagerEvents audioManagerEvents = new AppRTCAudioManager.AudioManagerEvents() { - @Override - public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices) { - eventCallback.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices); - } - }; + private final AppRTCAudioManager.AudioManagerEvents audioManagerEvents = + new AppRTCAudioManager.AudioManagerEvents() { + @Override + public void onAudioDeviceChanged( + AppRTCAudioManager.AudioDevice selectedAudioDevice, + Set availableAudioDevices) { + eventCallback.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices); + } + }; private final Handler mainHandler = new Handler(Looper.getMainLooper()); - private VideoTrack localVideoTrack = null; + private TrackWrapper localAudioTrack = null; + private TrackWrapper localVideoTrack = null; private VideoTrack remoteVideoTrack = null; - private final PeerConnection.Observer peerConnectionObserver = new PeerConnection.Observer() { - @Override - public void onSignalingChange(PeerConnection.SignalingState signalingState) { - Log.d(EXTENDED_LOGGING_TAG, "onSignalingChange(" + signalingState + ")"); - //this is called after removeTrack or addTrack - //and should then trigger a content-add or content-remove or something - //https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/removeTrack - } - - @Override - public void onConnectionChange(final PeerConnection.PeerConnectionState newState) { - eventCallback.onConnectionChange(newState); - } - - @Override - public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) { - Log.d(EXTENDED_LOGGING_TAG, "onIceConnectionChange(" + iceConnectionState + ")"); - } - - @Override - public void onSelectedCandidatePairChanged(CandidatePairChangeEvent event) { - Log.d(Config.LOGTAG, "remote candidate selected: " + event.remote); - Log.d(Config.LOGTAG, "local candidate selected: " + event.local); - } - - @Override - public void onIceConnectionReceivingChange(boolean b) { + private final PeerConnection.Observer peerConnectionObserver = + new PeerConnection.Observer() { + @Override + public void onSignalingChange(PeerConnection.SignalingState signalingState) { + Log.d(EXTENDED_LOGGING_TAG, "onSignalingChange(" + signalingState + ")"); + // this is called after removeTrack or addTrack + // and should then trigger a content-add or content-remove or something + // https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/removeTrack + } - } + @Override + public void onConnectionChange(final PeerConnection.PeerConnectionState newState) { + eventCallback.onConnectionChange(newState); + } - @Override - public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) { - Log.d(EXTENDED_LOGGING_TAG, "onIceGatheringChange(" + iceGatheringState + ")"); - } + @Override + public void onIceConnectionChange( + PeerConnection.IceConnectionState iceConnectionState) { + Log.d( + EXTENDED_LOGGING_TAG, + "onIceConnectionChange(" + iceConnectionState + ")"); + } - @Override - public void onIceCandidate(IceCandidate iceCandidate) { - if (readyToReceivedIceCandidates.get()) { - eventCallback.onIceCandidate(iceCandidate); - } else { - iceCandidates.add(iceCandidate); - } - } + @Override + public void onSelectedCandidatePairChanged(CandidatePairChangeEvent event) { + Log.d(Config.LOGTAG, "remote candidate selected: " + event.remote); + Log.d(Config.LOGTAG, "local candidate selected: " + event.local); + } - @Override - public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) { + @Override + public void onIceConnectionReceivingChange(boolean b) {} - } + @Override + public void onIceGatheringChange( + PeerConnection.IceGatheringState iceGatheringState) { + Log.d(EXTENDED_LOGGING_TAG, "onIceGatheringChange(" + iceGatheringState + ")"); + } - @Override - public void onAddStream(MediaStream mediaStream) { - Log.d(EXTENDED_LOGGING_TAG, "onAddStream(numAudioTracks=" + mediaStream.audioTracks.size() + ",numVideoTracks=" + mediaStream.videoTracks.size() + ")"); - } + @Override + public void onIceCandidate(IceCandidate iceCandidate) { + if (readyToReceivedIceCandidates.get()) { + eventCallback.onIceCandidate(iceCandidate); + } else { + iceCandidates.add(iceCandidate); + } + } - @Override - public void onRemoveStream(MediaStream mediaStream) { + @Override + public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {} - } + @Override + public void onAddStream(MediaStream mediaStream) { + Log.d( + EXTENDED_LOGGING_TAG, + "onAddStream(numAudioTracks=" + + mediaStream.audioTracks.size() + + ",numVideoTracks=" + + mediaStream.videoTracks.size() + + ")"); + } - @Override - public void onDataChannel(DataChannel dataChannel) { + @Override + public void onRemoveStream(MediaStream mediaStream) {} - } + @Override + public void onDataChannel(DataChannel dataChannel) {} - @Override - public void onRenegotiationNeeded() { - Log.d(EXTENDED_LOGGING_TAG, "onRenegotiationNeeded()"); - final PeerConnection.PeerConnectionState currentState = peerConnection == null ? null : peerConnection.connectionState(); - if (currentState != null && currentState != PeerConnection.PeerConnectionState.NEW) { - eventCallback.onRenegotiationNeeded(); - } - } + @Override + public void onRenegotiationNeeded() { + Log.d(EXTENDED_LOGGING_TAG, "onRenegotiationNeeded()"); + final PeerConnection.PeerConnectionState currentState = + peerConnection == null ? null : peerConnection.connectionState(); + if (currentState != null + && currentState != PeerConnection.PeerConnectionState.NEW) { + eventCallback.onRenegotiationNeeded(); + } + } - @Override - public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) { - final MediaStreamTrack track = rtpReceiver.track(); - Log.d(EXTENDED_LOGGING_TAG, "onAddTrack(kind=" + (track == null ? "null" : track.kind()) + ",numMediaStreams=" + mediaStreams.length + ")"); - if (track instanceof VideoTrack) { - remoteVideoTrack = (VideoTrack) track; - } - } + @Override + public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) { + final MediaStreamTrack track = rtpReceiver.track(); + Log.d( + EXTENDED_LOGGING_TAG, + "onAddTrack(kind=" + + (track == null ? "null" : track.kind()) + + ",numMediaStreams=" + + mediaStreams.length + + ")"); + if (track instanceof VideoTrack) { + remoteVideoTrack = (VideoTrack) track; + } + } - @Override - public void onTrack(RtpTransceiver transceiver) { - Log.d(EXTENDED_LOGGING_TAG, "onTrack(mid=" + transceiver.getMid() + ",media=" + transceiver.getMediaType() + ")"); - } - }; - @Nullable - private PeerConnection peerConnection = null; - private AudioTrack localAudioTrack = null; + @Override + public void onTrack(RtpTransceiver transceiver) { + Log.d( + EXTENDED_LOGGING_TAG, + "onTrack(mid=" + + transceiver.getMid() + + ",media=" + + transceiver.getMediaType() + + ")"); + } + }; + @Nullable private PeerConnection peerConnection = null; private AppRTCAudioManager appRTCAudioManager = null; private ToneManager toneManager = null; private Context context = null; private EglBase eglBase = null; - private CapturerChoice capturerChoice; + private VideoSourceWrapper videoSourceWrapper; WebRTCWrapper(final EventCallback eventCallback) { this.eventCallback = eventCallback; @@ -206,37 +215,15 @@ private static void dispose(final PeerConnection peerConnection) { } } - @Nullable - private static CapturerChoice of(CameraEnumerator enumerator, final String deviceName, Set availableCameras) { - final CameraVideoCapturer capturer = enumerator.createCapturer(deviceName, null); - if (capturer == null) { - return null; - } - final ArrayList choices = new ArrayList<>(enumerator.getSupportedFormats(deviceName)); - Collections.sort(choices, (a, b) -> b.width - a.width); - for (final CameraEnumerationAndroid.CaptureFormat captureFormat : choices) { - if (captureFormat.width <= CAPTURING_RESOLUTION) { - return new CapturerChoice(capturer, captureFormat, availableCameras); - } - } - return null; - } - - private static boolean isFrontFacing(final CameraEnumerator cameraEnumerator, final String deviceName) { - try { - return cameraEnumerator.isFrontFacing(deviceName); - } catch (final NullPointerException e) { - return false; - } - } - - public void setup(final XmppConnectionService service, final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference) throws InitializationException { + public void setup( + final XmppConnectionService service, + final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference) + throws InitializationException { try { PeerConnectionFactory.initialize( PeerConnectionFactory.InitializationOptions.builder(service) - .setFieldTrials("WebRTC-BindUsingInterfaceName/Enabled/") - .createInitializationOptions() - ); + .setFieldTrials("WebRTC-BindUsingInterfaceName/Enabled/") + .createInitializationOptions()); } catch (final UnsatisfiedLinkError e) { throw new InitializationException("Unable to initialize PeerConnectionFactory", e); } @@ -247,68 +234,93 @@ public void setup(final XmppConnectionService service, final AppRTCAudioManager. } this.context = service; this.toneManager = service.getJingleConnectionManager().toneManager; - mainHandler.post(() -> { - appRTCAudioManager = AppRTCAudioManager.create(service, speakerPhonePreference); - toneManager.setAppRtcAudioManagerHasControl(true); - appRTCAudioManager.start(audioManagerEvents); - eventCallback.onAudioDeviceChanged(appRTCAudioManager.getSelectedAudioDevice(), appRTCAudioManager.getAudioDevices()); - }); - } - - synchronized void initializePeerConnection(final Set media, final List iceServers) throws InitializationException { + mainHandler.post( + () -> { + appRTCAudioManager = AppRTCAudioManager.create(service, speakerPhonePreference); + toneManager.setAppRtcAudioManagerHasControl(true); + appRTCAudioManager.start(audioManagerEvents); + eventCallback.onAudioDeviceChanged( + appRTCAudioManager.getSelectedAudioDevice(), + appRTCAudioManager.getAudioDevices()); + }); + } + + synchronized void initializePeerConnection( + final Set media, final List iceServers) + throws InitializationException { Preconditions.checkState(this.eglBase != null); Preconditions.checkNotNull(media); - Preconditions.checkArgument(media.size() > 0, "media can not be empty when initializing peer connection"); - final boolean setUseHardwareAcousticEchoCanceler = WebRtcAudioEffects.canUseAcousticEchoCanceler() && !HARDWARE_AEC_BLACKLIST.contains(Build.MODEL); - Log.d(Config.LOGTAG, String.format("setUseHardwareAcousticEchoCanceler(%s) model=%s", setUseHardwareAcousticEchoCanceler, Build.MODEL)); - PeerConnectionFactory peerConnectionFactory = PeerConnectionFactory.builder() - .setVideoDecoderFactory(new DefaultVideoDecoderFactory(eglBase.getEglBaseContext())) - .setVideoEncoderFactory(new DefaultVideoEncoderFactory(eglBase.getEglBaseContext(), true, true)) - .setAudioDeviceModule(JavaAudioDeviceModule.builder(context) - .setUseHardwareAcousticEchoCanceler(setUseHardwareAcousticEchoCanceler) - .createAudioDeviceModule() - ) - .createPeerConnectionFactory(); - + Preconditions.checkArgument( + media.size() > 0, "media can not be empty when initializing peer connection"); + final boolean setUseHardwareAcousticEchoCanceler = + WebRtcAudioEffects.canUseAcousticEchoCanceler() + && !HARDWARE_AEC_BLACKLIST.contains(Build.MODEL); + Log.d( + Config.LOGTAG, + String.format( + "setUseHardwareAcousticEchoCanceler(%s) model=%s", + setUseHardwareAcousticEchoCanceler, Build.MODEL)); + PeerConnectionFactory peerConnectionFactory = + PeerConnectionFactory.builder() + .setVideoDecoderFactory( + new DefaultVideoDecoderFactory(eglBase.getEglBaseContext())) + .setVideoEncoderFactory( + new DefaultVideoEncoderFactory( + eglBase.getEglBaseContext(), true, true)) + .setAudioDeviceModule( + JavaAudioDeviceModule.builder(context) + .setUseHardwareAcousticEchoCanceler( + setUseHardwareAcousticEchoCanceler) + .createAudioDeviceModule()) + .createPeerConnectionFactory(); final PeerConnection.RTCConfiguration rtcConfig = buildConfiguration(iceServers); - final PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, peerConnectionObserver); + final PeerConnection peerConnection = + peerConnectionFactory.createPeerConnection(rtcConfig, peerConnectionObserver); if (peerConnection == null) { throw new InitializationException("Unable to create PeerConnection"); } - final Optional optionalCapturerChoice = media.contains(Media.VIDEO) ? getVideoCapturer() : Optional.absent(); + final Optional optionalVideoSourceWrapper = + media.contains(Media.VIDEO) + ? new VideoSourceWrapper.Factory(requireContext()).create() + : Optional.absent(); - if (optionalCapturerChoice.isPresent()) { - this.capturerChoice = optionalCapturerChoice.get(); - final CameraVideoCapturer capturer = this.capturerChoice.cameraVideoCapturer; - final VideoSource videoSource = peerConnectionFactory.createVideoSource(false); - SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create("webrtc", eglBase.getEglBaseContext()); - capturer.initialize(surfaceTextureHelper, requireContext(), videoSource.getCapturerObserver()); - Log.d(Config.LOGTAG, String.format("start capturing at %dx%d@%d", capturerChoice.captureFormat.width, capturerChoice.captureFormat.height, capturerChoice.getFrameRate())); - capturer.startCapture(capturerChoice.captureFormat.width, capturerChoice.captureFormat.height, capturerChoice.getFrameRate()); + if (optionalVideoSourceWrapper.isPresent()) { + this.videoSourceWrapper = optionalVideoSourceWrapper.get(); + this.videoSourceWrapper.initialize( + peerConnectionFactory, context, eglBase.getEglBaseContext()); + this.videoSourceWrapper.startCapture(); - this.localVideoTrack = peerConnectionFactory.createVideoTrack("my-video-track", videoSource); + final VideoTrack videoTrack = + peerConnectionFactory.createVideoTrack( + "my-video-track", this.videoSourceWrapper.getVideoSource()); - peerConnection.addTrack(this.localVideoTrack); + this.localVideoTrack = TrackWrapper.addTrack(peerConnection, videoTrack); } - if (media.contains(Media.AUDIO)) { - //set up audio track - final AudioSource audioSource = peerConnectionFactory.createAudioSource(new MediaConstraints()); - this.localAudioTrack = peerConnectionFactory.createAudioTrack("my-audio-track", audioSource); - peerConnection.addTrack(this.localAudioTrack); + // set up audio track + final AudioSource audioSource = + peerConnectionFactory.createAudioSource(new MediaConstraints()); + final AudioTrack audioTrack = + peerConnectionFactory.createAudioTrack("my-audio-track", audioSource); + this.localAudioTrack = TrackWrapper.addTrack(peerConnection, audioTrack); } peerConnection.setAudioPlayout(true); peerConnection.setAudioRecording(true); + this.peerConnection = peerConnection; } - private static PeerConnection.RTCConfiguration buildConfiguration(final List iceServers) { - final PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers); - rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED; //XEP-0176 doesn't support tcp - rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY; + private static PeerConnection.RTCConfiguration buildConfiguration( + final List iceServers) { + final PeerConnection.RTCConfiguration rtcConfig = + new PeerConnection.RTCConfiguration(iceServers); + rtcConfig.tcpCandidatePolicy = + PeerConnection.TcpCandidatePolicy.DISABLED; // XEP-0176 doesn't support tcp + rtcConfig.continualGatheringPolicy = + PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY; rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN; rtcConfig.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.NEGOTIATE; rtcConfig.enableImplicitRollback = true; @@ -332,7 +344,7 @@ public void setIsReadyToReceiveIceCandidates(final boolean ready) { synchronized void close() { final PeerConnection peerConnection = this.peerConnection; - final CapturerChoice capturerChoice = this.capturerChoice; + final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper; final AppRTCAudioManager audioManager = this.appRTCAudioManager; final EglBase eglBase = this.eglBase; if (peerConnection != null) { @@ -345,12 +357,13 @@ synchronized void close() { } this.localVideoTrack = null; this.remoteVideoTrack = null; - if (capturerChoice != null) { + if (videoSourceWrapper != null) { try { - capturerChoice.cameraVideoCapturer.stopCapture(); - } catch (InterruptedException e) { + videoSourceWrapper.stopCapture(); + } catch (final InterruptedException e) { Log.e(Config.LOGTAG, "unable to stop capturing"); } + // TODO call dispose } if (eglBase != null) { eglBase.release(); @@ -363,132 +376,148 @@ synchronized void verifyClosed() { || this.eglBase != null || this.localVideoTrack != null || this.remoteVideoTrack != null) { - final IllegalStateException e = new IllegalStateException("WebRTCWrapper hasn't been closed properly"); + final IllegalStateException e = + new IllegalStateException("WebRTCWrapper hasn't been closed properly"); Log.e(Config.LOGTAG, "verifyClosed() failed. Going to throw", e); throw e; } } boolean isCameraSwitchable() { - final CapturerChoice capturerChoice = this.capturerChoice; - return capturerChoice != null && capturerChoice.availableCameras.size() > 1; + final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper; + return videoSourceWrapper != null && videoSourceWrapper.isCameraSwitchable(); } boolean isFrontCamera() { - final CapturerChoice capturerChoice = this.capturerChoice; - return capturerChoice == null || capturerChoice.isFrontCamera; + final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper; + return videoSourceWrapper == null || videoSourceWrapper.isFrontCamera(); } ListenableFuture switchCamera() { - final CapturerChoice capturerChoice = this.capturerChoice; - if (capturerChoice == null) { - return Futures.immediateFailedFuture(new IllegalStateException("CameraCapturer has not been initialized")); + final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper; + if (videoSourceWrapper == null) { + return Futures.immediateFailedFuture( + new IllegalStateException("VideoSourceWrapper has not been initialized")); } - final SettableFuture future = SettableFuture.create(); - capturerChoice.cameraVideoCapturer.switchCamera(new CameraVideoCapturer.CameraSwitchHandler() { - @Override - public void onCameraSwitchDone(boolean isFrontCamera) { - capturerChoice.isFrontCamera = isFrontCamera; - future.set(isFrontCamera); - } - - @Override - public void onCameraSwitchError(final String message) { - future.setException(new IllegalStateException(String.format("Unable to switch camera %s", message))); - } - }); - return future; + return videoSourceWrapper.switchCamera(); } boolean isMicrophoneEnabled() { - final AudioTrack audioTrack = this.localAudioTrack; - if (audioTrack == null) { + final Optional audioTrack = TrackWrapper.get(this.localAudioTrack); + if (audioTrack.isPresent()) { + try { + return audioTrack.get().enabled(); + } catch (final IllegalStateException e) { + // sometimes UI might still be rendering the buttons when a background thread has + // already ended the call + return false; + } + } else { throw new IllegalStateException("Local audio track does not exist (yet)"); } - try { - return audioTrack.enabled(); - } catch (final IllegalStateException e) { - //sometimes UI might still be rendering the buttons when a background thread has already ended the call - return false; - } } boolean setMicrophoneEnabled(final boolean enabled) { - final AudioTrack audioTrack = this.localAudioTrack; - if (audioTrack == null) { + final Optional audioTrack = TrackWrapper.get(this.localAudioTrack); + if (audioTrack.isPresent()) { + try { + audioTrack.get().setEnabled(enabled); + return true; + } catch (final IllegalStateException e) { + Log.d(Config.LOGTAG, "unable to toggle microphone", e); + // ignoring race condition in case MediaStreamTrack has been disposed + return false; + } + } else { throw new IllegalStateException("Local audio track does not exist (yet)"); } - try { - audioTrack.setEnabled(enabled); - return true; - } catch (final IllegalStateException e) { - Log.d(Config.LOGTAG, "unable to toggle microphone", e); - //ignoring race condition in case MediaStreamTrack has been disposed - return false; - } } boolean isVideoEnabled() { - final VideoTrack videoTrack = this.localVideoTrack; - if (videoTrack == null) { - return false; + final Optional videoTrack = TrackWrapper.get(this.localVideoTrack); + if (videoTrack.isPresent()) { + return videoTrack.get().enabled(); } - return videoTrack.enabled(); + return false; } void setVideoEnabled(final boolean enabled) { - final VideoTrack videoTrack = this.localVideoTrack; - if (videoTrack == null) { - throw new IllegalStateException("Local video track does not exist"); + final Optional videoTrack = TrackWrapper.get(this.localVideoTrack); + if (videoTrack.isPresent()) { + videoTrack.get().setEnabled(enabled); + return; } - videoTrack.setEnabled(enabled); + throw new IllegalStateException("Local video track does not exist"); } synchronized ListenableFuture setLocalDescription() { - return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { - final SettableFuture future = SettableFuture.create(); - peerConnection.setLocalDescription(new SetSdpObserver() { - @Override - public void onSetSuccess() { - final SessionDescription description = peerConnection.getLocalDescription(); - Log.d(EXTENDED_LOGGING_TAG, "set local description:"); - logDescription(description); - future.set(description); - } - - @Override - public void onSetFailure(final String message) { - future.setException(new FailureToSetDescriptionException(message)); - } - }); - return future; - }, MoreExecutors.directExecutor()); + return Futures.transformAsync( + getPeerConnectionFuture(), + peerConnection -> { + if (peerConnection == null) { + return Futures.immediateFailedFuture( + new IllegalStateException("PeerConnection was null")); + } + final SettableFuture future = SettableFuture.create(); + peerConnection.setLocalDescription( + new SetSdpObserver() { + @Override + public void onSetSuccess() { + final SessionDescription description = + peerConnection.getLocalDescription(); + Log.d(EXTENDED_LOGGING_TAG, "set local description:"); + logDescription(description); + future.set(description); + } + + @Override + public void onSetFailure(final String message) { + future.setException( + new FailureToSetDescriptionException(message)); + } + }); + return future; + }, + MoreExecutors.directExecutor()); } private static void logDescription(final SessionDescription sessionDescription) { - for (final String line : sessionDescription.description.split(eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) { + for (final String line : + sessionDescription.description.split( + eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) { Log.d(EXTENDED_LOGGING_TAG, line); } } - synchronized ListenableFuture setRemoteDescription(final SessionDescription sessionDescription) { + synchronized ListenableFuture setRemoteDescription( + final SessionDescription sessionDescription) { Log.d(EXTENDED_LOGGING_TAG, "setting remote description:"); logDescription(sessionDescription); - return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { - final SettableFuture future = SettableFuture.create(); - peerConnection.setRemoteDescription(new SetSdpObserver() { - @Override - public void onSetSuccess() { - future.set(null); - } - - @Override - public void onSetFailure(final String message) { - future.setException(new FailureToSetDescriptionException(message)); - } - }, sessionDescription); - return future; - }, MoreExecutors.directExecutor()); + return Futures.transformAsync( + getPeerConnectionFuture(), + peerConnection -> { + if (peerConnection == null) { + return Futures.immediateFailedFuture( + new IllegalStateException("PeerConnection was null")); + } + final SettableFuture future = SettableFuture.create(); + peerConnection.setRemoteDescription( + new SetSdpObserver() { + @Override + public void onSetSuccess() { + future.set(null); + } + + @Override + public void onSetFailure(final String message) { + future.setException( + new FailureToSetDescriptionException(message)); + } + }, + sessionDescription); + return future; + }, + MoreExecutors.directExecutor()); } @Nonnull @@ -513,26 +542,6 @@ void addIceCandidate(IceCandidate iceCandidate) { requirePeerConnection().addIceCandidate(iceCandidate); } - private Optional getVideoCapturer() { - final CameraEnumerator enumerator = new Camera2Enumerator(requireContext()); - final Set deviceNames = ImmutableSet.copyOf(enumerator.getDeviceNames()); - for (final String deviceName : deviceNames) { - if (isFrontFacing(enumerator, deviceName)) { - final CapturerChoice capturerChoice = of(enumerator, deviceName, deviceNames); - if (capturerChoice == null) { - return Optional.absent(); - } - capturerChoice.isFrontCamera = true; - return Optional.of(capturerChoice); - } - } - if (deviceNames.size() == 0) { - return Optional.absent(); - } else { - return Optional.fromNullable(of(enumerator, Iterables.get(deviceNames, 0), deviceNames)); - } - } - PeerConnection.PeerConnectionState getState() { return requirePeerConnection().connectionState(); } @@ -541,13 +550,12 @@ public PeerConnection.SignalingState getSignalingState() { return requirePeerConnection().signalingState(); } - EglBase.Context getEglBaseContext() { return this.eglBase.getEglBaseContext(); } Optional getLocalVideoTrack() { - return Optional.fromNullable(this.localVideoTrack); + return TrackWrapper.get(this.localVideoTrack); } Optional getRemoteVideoTrack() { @@ -575,12 +583,14 @@ public interface EventCallback { void onConnectionChange(PeerConnection.PeerConnectionState newState); - void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices); + void onAudioDeviceChanged( + AppRTCAudioManager.AudioDevice selectedAudioDevice, + Set availableAudioDevices); void onRenegotiationNeeded(); } - private static abstract class SetSdpObserver implements SdpObserver { + private abstract static class SetSdpObserver implements SdpObserver { @Override public void onCreateSuccess(org.webrtc.SessionDescription sessionDescription) { @@ -591,22 +601,6 @@ public void onCreateSuccess(org.webrtc.SessionDescription sessionDescription) { public void onCreateFailure(String s) { throw new IllegalStateException("Not able to use SetSdpObserver"); } - - } - - private static abstract class CreateSdpObserver implements SdpObserver { - - - @Override - public void onSetSuccess() { - throw new IllegalStateException("Not able to use CreateSdpObserver"); - } - - - @Override - public void onSetFailure(String s) { - throw new IllegalStateException("Not able to use CreateSdpObserver"); - } } static class InitializationException extends Exception { @@ -625,7 +619,6 @@ public static class PeerConnectionNotInitialized extends IllegalStateException { private PeerConnectionNotInitialized() { super("initialize PeerConnection first"); } - } private static class FailureToSetDescriptionException extends IllegalArgumentException { @@ -634,20 +627,4 @@ public FailureToSetDescriptionException(String message) { } } - private static class CapturerChoice { - private final CameraVideoCapturer cameraVideoCapturer; - private final CameraEnumerationAndroid.CaptureFormat captureFormat; - private final Set availableCameras; - private boolean isFrontCamera = false; - - CapturerChoice(CameraVideoCapturer cameraVideoCapturer, CameraEnumerationAndroid.CaptureFormat captureFormat, Set cameras) { - this.cameraVideoCapturer = cameraVideoCapturer; - this.captureFormat = captureFormat; - this.availableCameras = cameras; - } - - int getFrameRate() { - return Math.max(captureFormat.framerate.min, Math.min(CAPTURING_MAX_FRAME_RATE, captureFormat.framerate.max)); - } - } } From 44bfff7e490496d02c3c22a7400848b6679f614e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 16 Nov 2022 11:00:43 +0100 Subject: [PATCH 233/394] fall back to regular authentication if fast fails --- .../conversations/xmpp/XmppConnection.java | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index ce3731536..01c7cb3b3 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -831,17 +831,17 @@ private boolean processSuccess(final Element success) } } - private void processFailure(final Element failure) throws StateChangingException { + private void processFailure(final Element failure) throws IOException { final SaslMechanism.Version version; try { version = SaslMechanism.Version.of(failure); } catch (final IllegalArgumentException e) { throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } - Log.d(Config.LOGTAG,failure.toString()); + Log.d(Config.LOGTAG, failure.toString()); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": login failure " + version); if (SaslMechanism.hashedToken(this.saslMechanism)) { - Log.d(Config.LOGTAG,account.getJid().asBareJid() + ": resetting token"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resetting token"); account.resetFastToken(); mXmppConnectionService.databaseBackend.updateAccount(account); } @@ -866,7 +866,15 @@ private void processFailure(final Element failure) throws StateChangingException } } } - throw new StateChangingException(Account.State.UNAUTHORIZED); + if (SaslMechanism.hashedToken(this.saslMechanism)) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": fast authentication failed. falling back to regular authentication"); + authenticate(); + } else { + throw new StateChangingException(Account.State.UNAUTHORIZED); + } } private static SSLSocket sslSocketOrNull(final Socket socket) { @@ -1332,6 +1340,17 @@ private void processStreamFeatures(final Tag currentTag) throws IOException { } } + private void authenticate() throws IOException { + final boolean isSecure = + features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion(); + if (isSecure && this.streamFeatures.hasChild("authentication", Namespace.SASL_2)) {authenticate(SaslMechanism.Version.SASL_2); + } else if (isSecure && this.streamFeatures.hasChild("mechanisms", Namespace.SASL)) { + authenticate(SaslMechanism.Version.SASL); + } else { + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } + } + private void authenticate(final SaslMechanism.Version version) throws IOException { final Element authElement; if (version == SaslMechanism.Version.SASL) { From 29461edf40d4f5f40b7e587fd0d7020098060f22 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 17 Nov 2022 07:48:09 +0100 Subject: [PATCH 234/394] process challenge only on secure connection --- .../conversations/xmpp/XmppConnection.java | 79 ++++++++++++------- 1 file changed, 51 insertions(+), 28 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 01c7cb3b3..823e0747b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -577,8 +577,16 @@ private void processStream() throws XmlPullParserException, IOException { // two step sasl2 - we don’t support this yet throw new StateChangingException(Account.State.INCOMPATIBLE_CLIENT); } else if (nextTag.isStart("challenge")) { - final Element challenge = tagReader.readElement(nextTag); - processChallenge(challenge); + if (isSecure() && this.saslMechanism != null) { + final Element challenge = tagReader.readElement(nextTag); + processChallenge(challenge); + } else { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": received 'challenge on an unsecure connection"); + throw new StateChangingException(Account.State.INCOMPATIBLE_CLIENT); + } } else if (nextTag.isStart("enabled", Namespace.STREAM_MANAGEMENT)) { final Element enabled = tagReader.readElement(nextTag); processEnabled(enabled); @@ -655,7 +663,7 @@ private void processStream() throws XmlPullParserException, IOException { } } - private void processChallenge(Element challenge) throws IOException { + private void processChallenge(final Element challenge) throws IOException { final SaslMechanism.Version version; try { version = SaslMechanism.Version.of(challenge); @@ -688,6 +696,10 @@ private boolean processSuccess(final Element success) } catch (final IllegalArgumentException e) { throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } + final SaslMechanism currentSaslMechanism = this.saslMechanism; + if (currentSaslMechanism == null) { + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } final String challenge; if (version == SaslMechanism.Version.SASL) { challenge = success.getContent(); @@ -697,7 +709,7 @@ private boolean processSuccess(final Element success) throw new AssertionError("Missing implementation for " + version); } try { - saslMechanism.getResponse(challenge, sslSocketOrNull(socket)); + currentSaslMechanism.getResponse(challenge, sslSocketOrNull(socket)); } catch (final SaslMechanism.AuthenticationException e) { Log.e(Config.LOGTAG, String.valueOf(e)); throw new StateChangingException(Account.State.UNAUTHORIZED); @@ -705,25 +717,10 @@ private boolean processSuccess(final Element success) Log.d( Config.LOGTAG, account.getJid().asBareJid().toString() + ": logged in (using " + version + ")"); - if (SaslMechanism.pin(this.saslMechanism)) { - account.setPinnedMechanism(this.saslMechanism); + if (SaslMechanism.pin(currentSaslMechanism)) { + account.setPinnedMechanism(currentSaslMechanism); } if (version == SaslMechanism.Version.SASL_2) { - final Tag tag = tagReader.readTag(); - if (tag != null && tag.isStart("features", Namespace.STREAMS)) { - this.streamFeatures = tagReader.readElement(tag); - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": processed NOP stream features after success " - + XmlHelper.printElementNames(this.streamFeatures)); - } else { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": server did not send stream features after SASL2 success"); - throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); - } final String authorizationIdentifier = success.findChildContent("authorization-identifier"); final Jid authorizationJid; @@ -761,6 +758,7 @@ private boolean processSuccess(final Element success) account.getJid().asBareJid() + ": jid changed during SASL 2.0. updating database"); } + final boolean nopStreamFeatures; final Element bound = success.findChild("bound", Namespace.BIND2); final Element resumed = success.findChild("resumed", "urn:xmpp:sm:3"); final Element failed = success.findChild("failed", "urn:xmpp:sm:3"); @@ -773,6 +771,7 @@ private boolean processSuccess(final Element success) + ": server sent bound and resumed in SASL2 success"); throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } + final boolean processNopStreamFeatures = (resumed != null && streamId != null) || bound != null; if (resumed != null && streamId != null) { processResumed(resumed); } else if (failed != null) { @@ -801,9 +800,8 @@ private boolean processSuccess(final Element success) sendPostBindInitialization(waitForDisco, carbonsEnabled != null); } final HashedToken.Mechanism tokenMechanism; - final SaslMechanism currentMechanism = this.saslMechanism; - if (SaslMechanism.hashedToken(currentMechanism)) { - tokenMechanism = ((HashedToken) currentMechanism).getTokenMechanism(); + if (SaslMechanism.hashedToken(currentSaslMechanism)) { + tokenMechanism = ((HashedToken) currentSaslMechanism).getTokenMechanism(); } else if (this.hashTokenRequest != null) { tokenMechanism = this.hashTokenRequest; } else { @@ -813,6 +811,9 @@ private boolean processSuccess(final Element success) this.account.setFastToken(tokenMechanism,token); Log.d(Config.LOGTAG,account.getJid().asBareJid()+": storing hashed token "+tokenMechanism); } + if (processNopStreamFeatures) { + processNopStreamFeatures(); + } } mXmppConnectionService.databaseBackend.updateAccount(account); this.quickStartInProgress = false; @@ -831,6 +832,25 @@ private boolean processSuccess(final Element success) } } + private void processNopStreamFeatures() throws IOException { + final Tag tag = tagReader.readTag(); + if (tag != null && tag.isStart("features", Namespace.STREAMS)) { + this.streamFeatures = tagReader.readElement(tag); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": processed NOP stream features after success: " + + XmlHelper.printElementNames(this.streamFeatures)); + } else { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received " + tag); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": server did not send stream features after SASL2 success"); + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } + } + private void processFailure(final Element failure) throws IOException { final SaslMechanism.Version version; try { @@ -1248,8 +1268,7 @@ private SSLSocket upgradeSocketToTls(final Socket socket) throws IOException { private void processStreamFeatures(final Tag currentTag) throws IOException { this.streamFeatures = tagReader.readElement(currentTag); - final boolean isSecure = - features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion(); + final boolean isSecure = isSecure(); final boolean needsBinding = !isBound && !account.isOptionSet(Account.OPTION_REGISTER); if (this.quickStartInProgress) { if (this.streamFeatures.hasChild("authentication", Namespace.SASL_2)) { @@ -1341,8 +1360,7 @@ private void processStreamFeatures(final Tag currentTag) throws IOException { } private void authenticate() throws IOException { - final boolean isSecure = - features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion(); + final boolean isSecure = isSecure(); if (isSecure && this.streamFeatures.hasChild("authentication", Namespace.SASL_2)) {authenticate(SaslMechanism.Version.SASL_2); } else if (isSecure && this.streamFeatures.hasChild("mechanisms", Namespace.SASL)) { authenticate(SaslMechanism.Version.SASL); @@ -1351,6 +1369,10 @@ private void authenticate() throws IOException { } } + private boolean isSecure() { + return features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion(); + } + private void authenticate(final SaslMechanism.Version version) throws IOException { final Element authElement; if (version == SaslMechanism.Version.SASL) { @@ -1658,6 +1680,7 @@ public void resetEverything() { synchronized (this.commands) { this.commands.clear(); } + this.saslMechanism = null; } private void sendBindRequest() { From 109a20ca4045f7e8bfc5d28b4e4de9f7ad86b766 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 17 Nov 2022 10:52:30 +0100 Subject: [PATCH 235/394] do not expect stream features after inline resume --- .../java/eu/siacs/conversations/xmpp/XmppConnection.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 823e0747b..0c2c718a3 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -771,7 +771,7 @@ private boolean processSuccess(final Element success) + ": server sent bound and resumed in SASL2 success"); throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } - final boolean processNopStreamFeatures = (resumed != null && streamId != null) || bound != null; + final boolean processNopStreamFeatures; if (resumed != null && streamId != null) { processResumed(resumed); } else if (failed != null) { @@ -798,6 +798,9 @@ private boolean processSuccess(final Element success) features.carbonsEnabled = true; } sendPostBindInitialization(waitForDisco, carbonsEnabled != null); + processNopStreamFeatures = true; + } else { + processNopStreamFeatures = false; } final HashedToken.Mechanism tokenMechanism; if (SaslMechanism.hashedToken(currentSaslMechanism)) { @@ -811,6 +814,7 @@ private boolean processSuccess(final Element success) this.account.setFastToken(tokenMechanism,token); Log.d(Config.LOGTAG,account.getJid().asBareJid()+": storing hashed token "+tokenMechanism); } + // TODO it is currently unclear if a successful resume triggers new stream features or not if (processNopStreamFeatures) { processNopStreamFeatures(); } @@ -1354,7 +1358,7 @@ private void processStreamFeatures(final Tag currentTag) throws IOException { Log.d( Config.LOGTAG, account.getJid().asBareJid() - + ": received NOP stream features " + + ": received NOP stream features: " + XmlHelper.printElementNames(this.streamFeatures)); } } From e74e2652d70f5613aa103ba9f9638b2f0a43b108 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 17 Nov 2022 11:03:56 +0100 Subject: [PATCH 236/394] bump various dependencies --- build.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 6ccdcf705..b48db295f 100644 --- a/build.gradle +++ b/build.gradle @@ -33,7 +33,7 @@ configurations { dependencies { implementation 'androidx.viewpager:viewpager:1.0.0' - playstoreImplementation('com.google.firebase:firebase-messaging:23.0.8') { + playstoreImplementation('com.google.firebase:firebase-messaging:23.1.0') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' @@ -43,10 +43,10 @@ dependencies { implementation 'org.sufficientlysecure:openpgp-api:10.0' implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0' implementation 'androidx.appcompat:appcompat:1.5.1' - implementation 'androidx.exifinterface:exifinterface:1.3.3' + implementation 'androidx.exifinterface:exifinterface:1.3.5' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' - implementation 'com.google.android.material:material:1.6.1' + implementation 'com.google.android.material:material:1.7.0' implementation "androidx.emoji2:emoji2:1.2.0" freeImplementation "androidx.emoji2:emoji2-bundled:1.2.0" @@ -77,7 +77,7 @@ dependencies { implementation 'com.google.guava:guava:31.1-android' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.49' freeImplementation 'ch.threema:webrtc-android:100.0.0' - playstoreImplementation fileTree(include: ['libwebrtc-m104.aar'], dir: 'libs') + playstoreImplementation fileTree(include: ['libwebrtc-m107.aar'], dir: 'libs') } ext { From c03d7b84215a9b5c3ea9f6546c8360082cf192fc Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 17 Nov 2022 11:05:34 +0100 Subject: [PATCH 237/394] update build instructions --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 037e04106..5bed4cd31 100644 --- a/README.md +++ b/README.md @@ -431,11 +431,11 @@ Then issue the following commands in order to build the apk. git clone https://github.com/inputmice/Conversations.git cd Conversations - ./gradlew assembleConversationsFreeSystemDebug + ./gradlew assembleConversationsFreeDebug There are two build flavors available. *free* and *playstore*. Unless you know what you are doing you only need *free*. -You will find the apks in the `./build/outputs/apk/conversationsFreeSystem/debug/` directory. +You will find the apks in the `./build/outputs/apk/conversationsFree/debug/` directory. Be careful, the resulting apks will not install unless you delete your existing Conversations installation (which will delete all the messages from your phone, and if you have used OMEMO, you will not be able to restore them from the server). Do it at your own risk. @@ -447,8 +447,6 @@ Then the resulting APK can be installed ALONGSIDE normal Conversations. And have WARNING: DO NOT REPLACE ANYTHING ELSE ANYWHERE ELSE, DO NOT REPLACE THIS PROJECT WIDE. JUST 2 strings in THAT specific file! -[![Build Status](https://travis-ci.org/inputmice/Conversations.svg?branch=development)](https://travis-ci.org/inputmice/Conversations) - #### How do I debug Conversations If something goes wrong Conversations usually exposes very little information in From c3410bae82be517eb9a0df47d564f16b7aef7715 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 18 Nov 2022 10:34:29 +0100 Subject: [PATCH 238/394] pulled translations from transifex --- src/conversations/res/values-hr/strings.xml | 16 +++++ src/main/res/values-da-rDK/strings.xml | 4 ++ src/main/res/values-de/strings.xml | 10 ++- src/main/res/values-es/strings.xml | 5 ++ src/main/res/values-gl/strings.xml | 4 ++ src/main/res/values-hr/strings.xml | 76 +++++++++++++++++++++ src/main/res/values-it/strings.xml | 5 ++ src/main/res/values-ja/strings.xml | 4 ++ src/main/res/values-pl/strings.xml | 4 ++ src/main/res/values-pt-rBR/strings.xml | 4 ++ src/main/res/values-ro-rRO/strings.xml | 4 ++ src/main/res/values-zh-rCN/strings.xml | 4 ++ src/main/res/values-zh-rTW/strings.xml | 45 ++++++++---- 13 files changed, 167 insertions(+), 18 deletions(-) create mode 100644 src/conversations/res/values-hr/strings.xml create mode 100644 src/main/res/values-hr/strings.xml diff --git a/src/conversations/res/values-hr/strings.xml b/src/conversations/res/values-hr/strings.xml new file mode 100644 index 000000000..093c6d0b9 --- /dev/null +++ b/src/conversations/res/values-hr/strings.xml @@ -0,0 +1,16 @@ + + + Odaberite svog XMPP davatelja usluga. + Koristite conversations.im + Napravi novi račun + Već imate XMPP račun? To može biti slučaj ako već koristite drugi XMPP klijent ili ste prije koristili Razgovore. Ako niste, možete odmah stvoriti novi XMPP račun.\nSavjet: Neki pružatelji usluga e-pošte također nude XMPP račune. + XMPP je mreža za razmjenu izravnih poruka neovisna o pružatelju usluga. Možete koristiti ovaj klijent s bilo kojim XMPP poslužiteljem koji odaberete.\nMeđutim, radi vaše udobnosti olakšali smo kreiranje računa na conversations.im; pružatelj usluga posebno prilagođen za korištenje s Conversations. + Pozvani ste na %1$s. Vodit ćemo vas kroz postupak kreiranja računa.\nPrilikom odabira %1$s pružatelja moći ćete komunicirati s korisnicima drugih pružatelja dajući im svoju punu XMPP adresu. + Pozvani ste na %1$s. Korisničko ime je već odabrano za vas. Vodit ćemo vas kroz postupak kreiranja računa.\nMoći ćete komunicirati s korisnicima drugih pružatelja tako da im date svoju punu XMPP adresu. + Vaša pozivnica za poslužitelj + Neispravno formatiran kod za dodjelu + Dodirnite gumb za dijeljenje kako biste svom kontaktu poslali pozivnicu na %1$s. + Ako je vaš kontakt u blizini, također može skenirati kod u nastavku kako bi prihvatio vašu pozivnicu. + Pridružite se %1$s i razgovarajte sa mnom: %2$s + Podijelite pozivnicu s... + \ No newline at end of file diff --git a/src/main/res/values-da-rDK/strings.xml b/src/main/res/values-da-rDK/strings.xml index 80e81485c..fa1c87bd1 100644 --- a/src/main/res/values-da-rDK/strings.xml +++ b/src/main/res/values-da-rDK/strings.xml @@ -294,6 +294,8 @@ Aktiver stilletid Notifikationer vil være lydløs under stilletid Andre + Synkroniser bogmærker + Indstil \"autojoin\"-flag, når du går ind i eller forlader en MUC, og reager på ændringer foretaget af andre klienter. OMEMO-fingeraftryk kopieret til udklipsholder Du er udelukket fra denne gruppechat Denne gruppechat er kun for medlemmer @@ -301,6 +303,7 @@ Du er blevet smidt ud af denne gruppechat Gruppechatten er lukket ned Du er ikke længere i denne gruppechat + Du forlod denne gruppechat af tekniske årsager anvender konto %s hostet på %s Tjekker %s på HTTP vært @@ -976,4 +979,5 @@ Kontoregistrering er ikke understøttet Ingen XMPP-adresse fundet Midlertidig godkendelsesfejl + Slet avatar diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 928c8311f..7fbc7ec10 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -292,8 +292,10 @@ Beginn Ende Ruhige Stunden aktivieren - Benachrichtigungen sind während der ruhigen Stunden stumm. + Benachrichtigungen sind während der ruhigen Stunden stumm Sonstiges + Lesezeichen synchronisieren + Setzt das \"Autojoin\"-Kennzeichen beim Betreten oder Verlassen eines Gruppenchats/Channels und reagiert auf Änderungen durch andere Clients. OMEMO-Fingerabdruck in die Zwischenablage kopiert Du wurdest aus diesem Gruppenchat ausgeschlossen Dieser Gruppenchat ist nur für Mitglieder @@ -301,6 +303,7 @@ Du wurdest aus diesem Gruppchat geworfen Gruppenchat wurde geschlossen Du bist nicht länger in diesem Gruppenchat + Du hast diesen Gruppenchat aus technischen Gründen verlassen verwende Konto %s gehostet bei %s %s auf HTTP-Host wird überprüft @@ -471,7 +474,7 @@ Fehlerhaft Status Abwesend bei gesperrtem Gerät - Als abwesend anzeigen, wenn das Gerät gesperrt ist. + Als abwesend anzeigen, wenn das Gerät gesperrt ist Beschäftigt im lautlosen Modus Als Beschäftigt anzeigen, wenn sich das Gerät im lautlosen Modus befindet Vibration als Lautlos behandeln @@ -554,7 +557,7 @@ Nicht verfügbar Beschäftigt Ein sicheres Passwort wurde erstellt - Dein Gerät unterstützt kein Ausschalten der Akkuoptimierung + Dein Gerät unterstützt nicht das Ausschalten der Akkuoptimierung Registrierung fehlgeschlagen: Bitte später versuchen Registrierung fehlgeschlagen: Passwort zu schwach Teilnehmer wählen @@ -976,4 +979,5 @@ Kontoregistrierungen werden nicht unterstützt Keine XMPP-Adresse gefunden Temporärer Authentifizierungsfehler + Profilbild löschen diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index fce2d4736..04a5cc170 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -297,6 +297,8 @@ Habilitar horario de silencio Las notificaciones serán silenciadas durante el horario de silencio Otros + Sincronizar marcadores + Establecer la opción \"unirse automáticamente\" cuando entras o sales de un MUC y reaccionar a las modificaciones realizadas por otros clientes. Huella digital OMEMO copiada al portapapeles Tu entrada a esta conversación en grupo ha sido prohibida Esta conversación en grupo es solo para miembros @@ -304,6 +306,7 @@ Has sido expulsado de esta conversación La conversación en grupo ha sido cerrada Ya no estás dentro de esta conversación en grupo + Has dejado esta conversación en grupo debido a razones técnicas. Usando cuenta %s alojado en %s Comprobando %s en servidor HTTP @@ -418,6 +421,7 @@ vídeo imagen gráfico de vectores + archivo multimedia documento PDF Android App Contacto @@ -988,4 +992,5 @@ Los registros de cuenta no están soportados Dirección XMPP no encontrada Fallo temporal de autenticación + Eliminar imagen de perfil diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 2b52435fe..8c8c00f60 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -294,6 +294,8 @@ Establecer horario sen notificacións As notificacións serán silenciadas durante estas horas Outro + Sincronizar marcadores + Poñer marca de \"autojoin\" ao entrar ou deixar unha MUC e reaccionar ás modificacións feitas desde outros clientes. Copiouse a impresión dixital OMEMO ao portapapeis Non podes acceder a esta conversa en grupo Esta conversa en grupo é so para membros @@ -301,6 +303,7 @@ Xa foi expulsado de esta conversa en grupo A conversa en grupo foi apagada Xa non estás nesta conversa en grupo + Deixaches esta conversa en grupo por razóns técnicas utilizando a conta %s hospedado en %s Comprobando %s no servidor HTTP @@ -976,4 +979,5 @@ Non está permitido o rexistro de novas contas Non se atopa un enderezo XMPP Fallo temporal da autenticación + Eliminar avatar diff --git a/src/main/res/values-hr/strings.xml b/src/main/res/values-hr/strings.xml new file mode 100644 index 000000000..440ee1fed --- /dev/null +++ b/src/main/res/values-hr/strings.xml @@ -0,0 +1,76 @@ + + + Postavke + Novi razgovor + Upravljanje računima + Upravljaj računom + Zatvori razgovor + Kontakt podaci + Pojedinosti grupnog razgovora + Detalji kanala + Dodaj račun + Uredi ime + Dodaj u adresar + Izbriši s popisa + Blokiraj kontakt + Odblokiraj kontakt + Blokiraj domenu + Odblokiraj domenu + Blokiraj sudionika + Deblokiraj sudionika + Upravljanje računima + Postavke + Dijeli s Conversation + Započni razgovor + Odaberite Kontakt + Odaberite kontakte + Dijeli putem računa + Lista blokiranih + upravo sad + prije 1 min + prije %d min + + %d nepročitan razgovor + + + %d nepročitanih razgovora + + + %d nepročitani razgovori + + + slanje… + Dešifriranje poruke. Molimo pričekajte… + OpenPGP šifrirana poruka + Nadimak je već u upotrebi + Nevažeći nadimak + Admin + Vlasnik + Moderator + Sudionik + Posjetitelj + Želite li ukloniti %s s popisa kontakata? Razgovori s ovim kontaktom neće biti uklonjeni. + Želite li blokirati %s da vam šalje poruke? + Želite li deblokirati %s i dopustiti im da vam šalju poruke? + Blokirati sve kontakte iz %s? + Deblokirati sve kontakte iz %s? + Kontakt blokiran + Blokiran + Želite li ukloniti %s kao oznaku? Razgovori s ovom knjižnom oznakom neće biti uklonjeni. + Registrirajte novi račun na poslužitelju + Promjena lozinke na poslužitelju + Podijeli s… + Započni razgovor + Pozovi kontakt + Pozovi + Kontakti + Kontakt + Otkazati + Dodati + Uredi + Obriši + Blok + Odblokiraj + Sačuvaj + Ok + diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index 37ef96f79..df1331f46 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -297,6 +297,8 @@ Attiva ore di quiete Le notifiche verranno silenziate durante le ore di quiete Altro + Sincronizza i segnalibri + Imposta il flag \"auto-entrata\" quando entri o esci da un MUC e reagisci alle modifiche fatte dagli altri client. Impronta OMEMO copiata negli appunti Sei stato bandito da questa chat di gruppo Questa chat di gruppo è solo per membri @@ -304,6 +306,7 @@ Sei stato buttato fuori da questa chat di gruppo La chat di gruppo è stata chiusa Non sei più in questa chat di gruppo + Hai lasciato questa chat di gruppo per motivi tecnici usando il profilo %s ospitato su %s Controllo %s su host HTTP @@ -418,6 +421,7 @@ video immagine grafica vettoriale + file multimediale Documento PDF Applicazione Android Contatto @@ -988,4 +992,5 @@ Le registrazioni di profili non sono supportate Nessun indirizzo XMPP trovato Errore di autenticazione temporaneo + Elimina avatar diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml index 94335a359..5f1edfc60 100644 --- a/src/main/res/values-ja/strings.xml +++ b/src/main/res/values-ja/strings.xml @@ -291,6 +291,7 @@ 消音時間を有効化 消音時間の間、通知は無音になります その他 + ブックマーク同期 OMEMO フィンガープリントをクリップボードにコピーしました このグループチャットから出禁にされています このグループチャットはメンバー制です @@ -298,6 +299,7 @@ このグループチャットから蹴り出されています このグループチャットは閉鎖されました あなたはもうこのグループチャットに参加していません + 技術的理由の為、あなたはこのグループチャットを離れました アカウント %s を使用 %s 上でホストされた HTTP ホスト上の %s を確認中 @@ -412,6 +414,7 @@ ビデオ 画像 ベクター画像 + マルチメディアファイル PDF 文書 Android アプリ 連絡先 @@ -956,4 +959,5 @@ アカウント登録はサポートされていません XMPPアドレスがみつかりません 一時的な認証失敗 + アバターを削除 diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 53158925f..75c46057a 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -300,6 +300,8 @@ Włącz godziny ciszy Powiadomienia będą wyciszone w wybranym przedziale czasu Inne + Synchronizuj zakładki + Ustaw flagę automatycznego dołączania przy wchodzeniu lub opuszczaniu pokoju i reaguj na zmiany innych klientów Odcisk klucza OMEMO został skopiowany do schowka Zbanowany Konferencja tylko dla użytkowników @@ -307,6 +309,7 @@ Wykopany Konferencja została zamknięta Nie uczestniczysz już w tej konferencji + Opuszczono rozmowę grupową z powodu usterki technicznej używając konta %s udostępnione na %s Sprawdzanie %s na hoście HTTP @@ -1003,4 +1006,5 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Rejestracja kont nie jest wspierana Nie znaleziono adresu XMPP Tymczasowy błąd uwierzytelniania + Usuń awatar diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index fd4b29cc2..58e17a785 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -297,6 +297,8 @@ Habilitar horário de sossego As notificações serão silenciadas no horário de sossego. Outras + Sincronizar favoritos + Define a flag \"autojoin\" ao entrar ou sair de uma sala e reage a modificações feitas por outros clientes. Impressão digital OMEMO copiada para a área de transferência Você foi banido desta conversa em grupo Somente membros podem entrar nessa conversa em grupo @@ -304,6 +306,7 @@ Você foi retirado desta conversa em grupo A conversa em grupo foi encerrada Você não está mais nesta conversa em grupo + Você saiu desta conversa em grupo devido a razões técnicas usando a conta %s hospedado em %s Verificando %s no host HTTP @@ -989,4 +992,5 @@ O registro de contas não está ativo Não foi encontrado nenhum endereço XMPP Falha temporária na autenticação + Excluir avatar diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index 34c5c155f..2f35075a5 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -297,6 +297,8 @@ Activează orar de liniște Notificările vor fi reduse la tăcere în timpul orelor de liniște Altele + Sincronizare semne de carte + Setați \"autojoin\" la intrarea sau ieșirea dintr-o discuție de grup și reacționați la modificările efectuate de alți clienți. Amprentă OMEMO copiată în memorie V-a fost interzis accesul la această discuție de grup Această discuție de grup este rezervată membrilor @@ -304,6 +306,7 @@ Ați fost dat(ă) afară din această discuție de grup Discuția de grup a fost închisă Nu mai sunteți în această discuție de grup + Ați părăsit această discuție de grup din motive tehnice folosind cont %s găzduit pe %s Verifica %s pe gazda HTTP @@ -989,4 +992,5 @@ Nu este posibilă înregistrarea unui cont Nu a fost găsită o adresă XMPP Eroare temporară de autentificare + Șterge avatar diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index ad19591cf..cca20889f 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -291,6 +291,8 @@ 启用静默时间段 在静默时间段内通知将保持静音 其他 + 同步书签 + 加入或离开多用户聊天时设置 “autojoin\" 标志,并回应其他客户端所做更改。 OMEMO指纹已拷贝到剪贴板 您被封禁了 这个群聊只允许成员聊天 @@ -298,6 +300,7 @@ 您被从此群聊踢出 这个群聊已被关闭 您已不在该群组 + 你出于技术原因离开了群聊 使用帐户%s 托管于%s 正在HTTP服务器中检查%s @@ -963,4 +966,5 @@ 不支持注册账户 未找到 XMPP 地址 临时认证失败 + 删除群聊 diff --git a/src/main/res/values-zh-rTW/strings.xml b/src/main/res/values-zh-rTW/strings.xml index edcacf374..5c21adb4f 100644 --- a/src/main/res/values-zh-rTW/strings.xml +++ b/src/main/res/values-zh-rTW/strings.xml @@ -145,8 +145,10 @@ 未找到伺服器 未連接網路 註冊失敗 - 用戶名已存在 + 使用者名稱已被使用 註冊完成 + 伺服器不支援註冊 + 無效的註冊權杖 違反政策 伺服器不相容 串流錯誤 @@ -162,6 +164,7 @@ 確定要移除上線狀態中的 OpenPGP 公開金鑰嗎?\n這樣一來,你的聯絡人就無法傳送以 OpenPGP 加密的訊息給你了。 啟用帳戶 確定? + 刪除帳戶將清除您全部的會話記錄 錄音 XMPP 位址 封鎖 XMPP 位址 @@ -186,18 +189,22 @@ 缺少公開金鑰通知 剛剛查看過 %d 分鐘前查看過 + 一小時前查看過 %d 小時前查看過 + 一天前查看過 %d 天前查看過 OpenPGP 金鑰 ID OMEMO 指紋 v\\OMEMO 指紋 + OMEMO 指紋 (訊息來源) + v\\OMEMO 指紋 (訊息來源) 其他裝置 信任的 OMEMO 指紋 正在擷取金鑰… 完成 解密 書籤 - 尋找 + 搜尋 輸入聯絡人 刪除聯絡人 檢視聯絡人詳細資料 @@ -310,7 +317,7 @@ 離線 拋棄 成員 - 高級模式 + 進階模式 授予管理員許可權 吊銷管理員許可權 不能修改 %s 的從屬關係 @@ -320,28 +327,34 @@ 您尚未參與 從不 直到新的通知 + 延遲 + 回覆 + 標示為已讀 輸入 - 回車是發送 - 顯示回車鍵 - 改變表情鍵為回車鍵 + Enter 鍵傳送 + 顯示 Enter 鍵 + 變更表情符號鍵為 Enter 鍵 音訊 影片 - 圖像 - PDF 文檔 - Android App - 連絡人 + 圖片 + 向量圖形 + 多媒體檔案 + PDF 文件 + Android 應用程式 + 聯絡人 頭像已經發佈! 發送中 %s 提供中 %s 隱藏離線連絡人 - %s 正在輸入中… - %s 停止輸入了 - %s 正在輸入中… - %s 停止輸入了 + %s 正在輸入… + %s 已停止輸入 + %s 正在輸入… + %s 已停止輸入 鍵盤輸入通知 讓聯絡人知道你正在寫訊息送給它們 - 發送位置 + 傳送位置 顯示位置 + 找不到可以顯示位置的應用程式 位置 Conversation 已關閉 不信任系統的憑證機構 @@ -394,6 +407,8 @@ %d 則訊息 載入更多訊息 + 與 %s 分享的檔案 + 與 %s 分享的圖片 與連絡人同步 為所有訊息顯示通知 關閉通知 From 6b9ebb3abfe17703d8b292443f56bcca6444848f Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 18 Nov 2022 10:40:16 +0100 Subject: [PATCH 239/394] remove TODO --- src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 0c2c718a3..54305cdb7 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -814,7 +814,7 @@ private boolean processSuccess(final Element success) this.account.setFastToken(tokenMechanism,token); Log.d(Config.LOGTAG,account.getJid().asBareJid()+": storing hashed token "+tokenMechanism); } - // TODO it is currently unclear if a successful resume triggers new stream features or not + // a successful resume will not send stream features if (processNopStreamFeatures) { processNopStreamFeatures(); } From d51682a9bc63048db4536a788ac51cc6ad75b23b Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 18 Nov 2022 10:45:12 +0100 Subject: [PATCH 240/394] version bump to 2.11.0-beta --- CHANGELOG.md | 7 +++++++ build.gradle | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a10c8088..7bed8eedb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +### Version 2.11.0 + +* Implement Extensible SASL Profile, Bind 2.0 and Fast for faster reconnects +* Implement Channel Binding +* Add ability to delete own avatar +* Add notification for missed calls + ### Version 2.10.10 * Minor bug fixes diff --git a/build.gradle b/build.gradle index b48db295f..f5c5fda51 100644 --- a/build.gradle +++ b/build.gradle @@ -93,8 +93,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 32 - versionCode 42038 - versionName "2.10.10" + versionCode 42039 + versionName "2.11.0-beta" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId From 8fb2c11771d8e7a419682da7be8a38e5068c4460 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 19 Nov 2022 08:14:50 +0100 Subject: [PATCH 241/394] use plurals for missed call strings --- .../services/NotificationService.java | 41 +++++++++++-------- .../xmpp/jingle/JingleRtpConnection.java | 1 + src/main/res/values/strings.xml | 16 ++++++-- 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index 55e220f62..715cafe42 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -987,12 +987,17 @@ private Builder buildMissedCallsSummary(boolean publicVersion) { (totalCalls == 1) ? mXmppConnectionService.getString(R.string.missed_call) : (mMissedCalls.size() == 1) - ? mXmppConnectionService.getString( - R.string.n_missed_calls, totalCalls) - : mXmppConnectionService.getString( - R.string.n_missed_calls_from_m_contacts, - totalCalls, - mMissedCalls.size()); + ? mXmppConnectionService + .getResources() + .getQuantityString( + R.plurals.n_missed_calls, totalCalls, totalCalls) + : mXmppConnectionService + .getResources() + .getQuantityString( + R.plurals.n_missed_calls_from_m_contacts, + mMissedCalls.size(), + totalCalls, + mMissedCalls.size()); builder.setContentTitle(title); builder.setTicker(title); if (!publicVersion) { @@ -1027,21 +1032,25 @@ private Builder buildMissedCall( final String title = (info.getNumberOfCalls() == 1) ? mXmppConnectionService.getString(R.string.missed_call) - : mXmppConnectionService.getString( - R.string.n_missed_calls, info.getNumberOfCalls()); + : mXmppConnectionService + .getResources() + .getQuantityString( + R.plurals.n_missed_calls, + info.getNumberOfCalls(), + info.getNumberOfCalls()); builder.setContentTitle(title); final String name = conversation.getContact().getDisplayName(); if (publicVersion) { builder.setTicker(title); } else { - if (info.getNumberOfCalls() == 1) { - builder.setTicker( - mXmppConnectionService.getString(R.string.missed_call_from_x, name)); - } else { - builder.setTicker( - mXmppConnectionService.getString( - R.string.n_missed_calls_from_x, info.getNumberOfCalls(), name)); - } + builder.setTicker( + mXmppConnectionService + .getResources() + .getQuantityString( + R.plurals.n_missed_calls_from_x, + info.getNumberOfCalls(), + info.getNumberOfCalls(), + name)); builder.setContentText(name); } builder.setSmallIcon(R.drawable.ic_call_missed_white_24db); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index c69fc6b02..e2832355d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -1291,6 +1291,7 @@ private void prepareSessionInitiate( SessionDescription.parse(webRTCSessionDescription.description); final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); this.initiatorRtpContentMap = rtpContentMap; + //TODO delay ready to receive ice until after session-init this.webRTCWrapper.setIsReadyToReceiveIceCandidates(true); final ListenableFuture outgoingContentMapFuture = encryptSessionInitiate(rtpContentMap); diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index f668e3f25..d14a0c971 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -936,10 +936,18 @@ Outgoing call Outgoing call · %s Missed call - Missed call from %s - %1$d missed calls from %2$s - %d missed calls - %1$d missed calls from %2$d contacts + + %1$d missed call from %2$s + %1$d missed calls from %2$s + + + %d missed call + %d missed calls + + + %1$d missed calls from %2$d contact + %1$d missed calls from %2$d contacts + Audio call Video call Help From 27d8da2ab4e927a4b81c266a2501ed666eaedd4b Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 19 Nov 2022 13:03:34 +0100 Subject: [PATCH 242/394] refactor WebRTCWrapper to allow for track adds --- .../xmpp/jingle/VideoSourceWrapper.java | 12 +-- .../xmpp/jingle/WebRTCWrapper.java | 102 +++++++++++++----- 2 files changed, 80 insertions(+), 34 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/VideoSourceWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/VideoSourceWrapper.java index 5e83f2ba9..b837131e8 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/VideoSourceWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/VideoSourceWrapper.java @@ -3,7 +3,6 @@ import android.content.Context; import android.util.Log; -import com.google.common.base.Optional; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.util.concurrent.ListenableFuture; @@ -127,7 +126,7 @@ public Factory(final Context context) { this.context = context; } - public Optional create() { + public VideoSourceWrapper create() { final CameraEnumerator enumerator = new Camera2Enumerator(context); final Set deviceNames = ImmutableSet.copyOf(enumerator.getDeviceNames()); for (final String deviceName : deviceNames) { @@ -135,17 +134,16 @@ public Optional create() { final VideoSourceWrapper videoSourceWrapper = of(enumerator, deviceName, deviceNames); if (videoSourceWrapper == null) { - return Optional.absent(); + return null; } videoSourceWrapper.isFrontCamera = true; - return Optional.of(videoSourceWrapper); + return videoSourceWrapper; } } if (deviceNames.size() == 0) { - return Optional.absent(); + return null; } else { - return Optional.fromNullable( - of(enumerator, Iterables.get(deviceNames, 0), deviceNames)); + return of(enumerator, Iterables.get(deviceNames, 0), deviceNames); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index f71799bdf..4ce9c1478 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -196,6 +196,7 @@ public void onTrack(RtpTransceiver transceiver) { + ")"); } }; + @Nullable private PeerConnectionFactory peerConnectionFactory = null; @Nullable private PeerConnection peerConnection = null; private AppRTCAudioManager appRTCAudioManager = null; private ToneManager toneManager = null; @@ -260,7 +261,7 @@ synchronized void initializePeerConnection( String.format( "setUseHardwareAcousticEchoCanceler(%s) model=%s", setUseHardwareAcousticEchoCanceler, Build.MODEL)); - PeerConnectionFactory peerConnectionFactory = + this.peerConnectionFactory = PeerConnectionFactory.builder() .setVideoDecoderFactory( new DefaultVideoDecoderFactory(eglBase.getEglBaseContext())) @@ -268,7 +269,7 @@ synchronized void initializePeerConnection( new DefaultVideoEncoderFactory( eglBase.getEglBaseContext(), true, true)) .setAudioDeviceModule( - JavaAudioDeviceModule.builder(context) + JavaAudioDeviceModule.builder(requireContext()) .setUseHardwareAcousticEchoCanceler( setUseHardwareAcousticEchoCanceler) .createAudioDeviceModule()) @@ -276,36 +277,18 @@ synchronized void initializePeerConnection( final PeerConnection.RTCConfiguration rtcConfig = buildConfiguration(iceServers); final PeerConnection peerConnection = - peerConnectionFactory.createPeerConnection(rtcConfig, peerConnectionObserver); + requirePeerConnectionFactory() + .createPeerConnection(rtcConfig, peerConnectionObserver); if (peerConnection == null) { throw new InitializationException("Unable to create PeerConnection"); } - final Optional optionalVideoSourceWrapper = - media.contains(Media.VIDEO) - ? new VideoSourceWrapper.Factory(requireContext()).create() - : Optional.absent(); - - if (optionalVideoSourceWrapper.isPresent()) { - this.videoSourceWrapper = optionalVideoSourceWrapper.get(); - this.videoSourceWrapper.initialize( - peerConnectionFactory, context, eglBase.getEglBaseContext()); - this.videoSourceWrapper.startCapture(); - - final VideoTrack videoTrack = - peerConnectionFactory.createVideoTrack( - "my-video-track", this.videoSourceWrapper.getVideoSource()); - - this.localVideoTrack = TrackWrapper.addTrack(peerConnection, videoTrack); + if (media.contains(Media.VIDEO)) { + addVideoTrack(peerConnection); } if (media.contains(Media.AUDIO)) { - // set up audio track - final AudioSource audioSource = - peerConnectionFactory.createAudioSource(new MediaConstraints()); - final AudioTrack audioTrack = - peerConnectionFactory.createAudioTrack("my-audio-track", audioSource); - this.localAudioTrack = TrackWrapper.addTrack(peerConnection, audioTrack); + addAudioTrack(peerConnection); } peerConnection.setAudioPlayout(true); peerConnection.setAudioRecording(true); @@ -313,6 +296,58 @@ synchronized void initializePeerConnection( this.peerConnection = peerConnection; } + private VideoSourceWrapper initializeVideoSourceWrapper() { + final VideoSourceWrapper existingVideoSourceWrapper = this.videoSourceWrapper; + if (existingVideoSourceWrapper != null) { + existingVideoSourceWrapper.startCapture(); + return existingVideoSourceWrapper; + } + final VideoSourceWrapper videoSourceWrapper = + new VideoSourceWrapper.Factory(requireContext()).create(); + if (videoSourceWrapper == null) { + throw new IllegalStateException("Could not instantiate VideoSourceWrapper"); + } + videoSourceWrapper.initialize( + requirePeerConnectionFactory(), requireContext(), eglBase.getEglBaseContext()); + videoSourceWrapper.startCapture(); + return videoSourceWrapper; + } + + public synchronized boolean addTrack(final Media media) { + if (media == Media.VIDEO) { + return addVideoTrack(requirePeerConnection()); + } else if (media == Media.AUDIO) { + return addAudioTrack(requirePeerConnection()); + } + throw new IllegalStateException(String.format("Could not add track for %s", media)); + } + + private boolean addAudioTrack(final PeerConnection peerConnection) { + final AudioSource audioSource = + requirePeerConnectionFactory().createAudioSource(new MediaConstraints()); + final AudioTrack audioTrack = + requirePeerConnectionFactory().createAudioTrack("my-audio-track", audioSource); + this.localAudioTrack = TrackWrapper.addTrack(peerConnection, audioTrack); + return true; + } + + private boolean addVideoTrack(final PeerConnection peerConnection) { + Preconditions.checkState( + this.localVideoTrack == null, "A local video track already exists"); + final VideoSourceWrapper videoSourceWrapper; + try { + videoSourceWrapper = initializeVideoSourceWrapper(); + } catch (final IllegalStateException e) { + Log.d(Config.LOGTAG, "could not add video track", e); + return false; + } + final VideoTrack videoTrack = + requirePeerConnectionFactory() + .createVideoTrack("my-video-track", videoSourceWrapper.getVideoSource()); + this.localVideoTrack = TrackWrapper.addTrack(peerConnection, videoTrack); + return true; + } + private static PeerConnection.RTCConfiguration buildConfiguration( final List iceServers) { final PeerConnection.RTCConfiguration rtcConfig = @@ -344,6 +379,7 @@ public void setIsReadyToReceiveIceCandidates(final boolean ready) { synchronized void close() { final PeerConnection peerConnection = this.peerConnection; + final PeerConnectionFactory peerConnectionFactory = this.peerConnectionFactory; final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper; final AppRTCAudioManager audioManager = this.appRTCAudioManager; final EglBase eglBase = this.eglBase; @@ -363,12 +399,15 @@ synchronized void close() { } catch (final InterruptedException e) { Log.e(Config.LOGTAG, "unable to stop capturing"); } - // TODO call dispose + videoSourceWrapper.dispose(); } if (eglBase != null) { eglBase.release(); this.eglBase = null; } + if (peerConnectionFactory != null) { + peerConnectionFactory.dispose(); + } } synchronized void verifyClosed() { @@ -530,6 +569,7 @@ private ListenableFuture getPeerConnectionFuture() { } } + @Nonnull private PeerConnection requirePeerConnection() { final PeerConnection peerConnection = this.peerConnection; if (peerConnection == null) { @@ -538,6 +578,15 @@ private PeerConnection requirePeerConnection() { return peerConnection; } + @Nonnull + private PeerConnectionFactory requirePeerConnectionFactory() { + final PeerConnectionFactory peerConnectionFactory = this.peerConnectionFactory; + if (peerConnectionFactory == null) { + throw new IllegalStateException("Make sure PeerConnectionFactory is initialized"); + } + return peerConnectionFactory; + } + void addIceCandidate(IceCandidate iceCandidate) { requirePeerConnection().addIceCandidate(iceCandidate); } @@ -626,5 +675,4 @@ public FailureToSetDescriptionException(String message) { super(message); } } - } From 59ea66ca78dde11f196c9764a82752c9145458b7 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 19 Nov 2022 14:19:07 +0100 Subject: [PATCH 243/394] make sure VideoSourceWrapper is stored in property --- .../java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 4ce9c1478..de26f1afc 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -310,6 +310,7 @@ private VideoSourceWrapper initializeVideoSourceWrapper() { videoSourceWrapper.initialize( requirePeerConnectionFactory(), requireContext(), eglBase.getEglBaseContext()); videoSourceWrapper.startCapture(); + this.videoSourceWrapper = videoSourceWrapper; return videoSourceWrapper; } From 304205b2e344ae1c1b6b17e230589109a230b121 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 20 Nov 2022 17:00:40 +0100 Subject: [PATCH 244/394] take senders attr into account when converting to and from sdp --- .../crypto/axolotl/AxolotlService.java | 4 +- .../jingle/JingleFileTransferConnection.java | 27 +-- .../xmpp/jingle/JingleRtpConnection.java | 14 +- .../xmpp/jingle/RtpContentMap.java | 104 +++++++++--- .../xmpp/jingle/SessionDescription.java | 158 ++++++++++++------ .../xmpp/jingle/stanzas/Content.java | 74 ++++++-- 6 files changed, 274 insertions(+), 107 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 3d4f23360..05ffdbdca 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -1272,7 +1272,7 @@ private ListenableFuture> encry } descriptionTransportBuilder.put( content.getKey(), - new RtpContentMap.DescriptionTransport(descriptionTransport.description, encryptedTransportInfo) + new RtpContentMap.DescriptionTransport(descriptionTransport.senders, descriptionTransport.description, encryptedTransportInfo) ); } return Futures.immediateFuture( @@ -1306,7 +1306,7 @@ public ListenableFuture> decrypt(OmemoVerifi omemoVerification.setOrEnsureEqual(decryptedTransport); descriptionTransportBuilder.put( content.getKey(), - new RtpContentMap.DescriptionTransport(descriptionTransport.description, decryptedTransport.payload) + new RtpContentMap.DescriptionTransport(descriptionTransport.senders, descriptionTransport.description, decryptedTransport.payload) ); } processPostponed(); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java index 43aaa54b5..c4ed04bd0 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java @@ -577,8 +577,7 @@ private void setupDescription(final FileTransferDescription.Version version) { private void sendInitRequest() { final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.SESSION_INITIATE); - final Content content = new Content(this.contentCreator, this.contentName); - content.setSenders(this.contentSenders); + final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName); if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL && remoteSupportsOmemoJet) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": remote announced support for JET"); final Element security = new Element("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT); @@ -656,8 +655,7 @@ private void sendAcceptSocks() { gatherAndConnectDirectCandidates(); this.jingleConnectionManager.getPrimaryCandidate(this.id.account, isInitiator(), (success, candidate) -> { final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT); - final Content content = new Content(contentCreator, contentName); - content.setSenders(this.contentSenders); + final Content content = new Content(contentCreator, contentSenders, contentName); content.setDescription(this.description); if (success && candidate != null && !equalCandidateExists(candidate)) { final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, candidate); @@ -696,8 +694,7 @@ public void established() { private void sendAcceptIbb() { this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize); final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT); - final Content content = new Content(contentCreator, contentName); - content.setSenders(this.contentSenders); + final Content content = new Content(contentCreator, contentSenders, contentName); content.setDescription(this.description); content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize)); packet.addJingleContent(content); @@ -910,8 +907,7 @@ private void sendSuccess() { private void sendFallbackToIbb() { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending fallback to ibb"); final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.TRANSPORT_REPLACE); - final Content content = new Content(this.contentCreator, this.contentName); - content.setSenders(this.contentSenders); + final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName); this.transportId = JingleConnectionManager.nextRandomId(); content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize)); packet.addJingleContent(content); @@ -944,8 +940,7 @@ private void receiveFallbackToIbb(final JinglePacket packet, final IbbTransportI final JinglePacket answer = bootstrapPacket(JinglePacket.Action.TRANSPORT_ACCEPT); - final Content content = new Content(contentCreator, contentName); - content.setSenders(this.contentSenders); + final Content content = new Content(contentCreator, contentSenders, contentName); content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize)); answer.addJingleContent(content); @@ -1124,8 +1119,7 @@ private void disconnectSocks5Connections() { private void sendProxyActivated(String cid) { final JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO); - final Content content = new Content(this.contentCreator, this.contentName); - content.setSenders(this.contentSenders); + final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName); content.setTransport(new S5BTransportInfo(this.transportId, new Element("activated").setAttribute("cid", cid))); packet.addJingleContent(content); this.sendJinglePacket(packet); @@ -1133,8 +1127,7 @@ private void sendProxyActivated(String cid) { private void sendProxyError() { final JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO); - final Content content = new Content(this.contentCreator, this.contentName); - content.setSenders(this.contentSenders); + final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName); content.setTransport(new S5BTransportInfo(this.transportId, new Element("proxy-error"))); packet.addJingleContent(content); this.sendJinglePacket(packet); @@ -1142,8 +1135,7 @@ private void sendProxyError() { private void sendCandidateUsed(final String cid) { JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO); - final Content content = new Content(this.contentCreator, this.contentName); - content.setSenders(this.contentSenders); + final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName); content.setTransport(new S5BTransportInfo(this.transportId, new Element("candidate-used").setAttribute("cid", cid))); packet.addJingleContent(content); this.sentCandidate = true; @@ -1156,8 +1148,7 @@ private void sendCandidateUsed(final String cid) { private void sendCandidateError() { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending candidate error"); JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO); - Content content = new Content(this.contentCreator, this.contentName); - content.setSenders(this.contentSenders); + Content content = new Content(this.contentCreator, this.contentSenders, this.contentName); content.setTransport(new S5BTransportInfo(this.transportId, new Element("candidate-error"))); packet.addJingleContent(content); this.sentCandidate = true; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index e2832355d..5cccafa5a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -425,7 +425,7 @@ private boolean applyIceRestart( final RtpContentMap restartContentMap, final boolean isOffer) throws ExecutionException, InterruptedException { - final SessionDescription sessionDescription = SessionDescription.of(restartContentMap); + final SessionDescription sessionDescription = SessionDescription.of(restartContentMap, !isInitiator()); final org.webrtc.SessionDescription.Type type = isOffer ? org.webrtc.SessionDescription.Type.OFFER @@ -444,7 +444,7 @@ private boolean applyIceRestart( if (isOffer) { webRTCWrapper.setIsReadyToReceiveIceCandidates(false); final SessionDescription localSessionDescription = setLocalSessionDescription(); - setLocalContentMap(RtpContentMap.of(localSessionDescription)); + setLocalContentMap(RtpContentMap.of(localSessionDescription, isInitiator())); // We need to respond OK before sending any candidates respondOk(jinglePacket); webRTCWrapper.setIsReadyToReceiveIceCandidates(true); @@ -726,7 +726,7 @@ private void receiveSessionAccept(final RtpContentMap contentMap) { this.storePeerDtlsSetup(contentMap.getDtlsSetup()); final SessionDescription sessionDescription; try { - sessionDescription = SessionDescription.of(contentMap); + sessionDescription = SessionDescription.of(contentMap, false); } catch (final IllegalArgumentException | NullPointerException e) { Log.d( Config.LOGTAG, @@ -763,7 +763,7 @@ private void sendSessionAccept() { } final SessionDescription offer; try { - offer = SessionDescription.of(rtpContentMap); + offer = SessionDescription.of(rtpContentMap, true); } catch (final IllegalArgumentException | NullPointerException e) { Log.d( Config.LOGTAG, @@ -838,7 +838,7 @@ private void prepareSessionAccept( final org.webrtc.SessionDescription webRTCSessionDescription) { final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); - final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription); + final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription, false); this.responderRtpContentMap = respondingRtpContentMap; storePeerDtlsSetup(respondingRtpContentMap.getDtlsSetup().flip()); webRTCWrapper.setIsReadyToReceiveIceCandidates(true); @@ -1289,7 +1289,7 @@ private void prepareSessionInitiate( final org.webrtc.SessionDescription webRTCSessionDescription, final State targetState) { final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); - final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); + final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription, true); this.initiatorRtpContentMap = rtpContentMap; //TODO delay ready to receive ice until after session-init this.webRTCWrapper.setIsReadyToReceiveIceCandidates(true); @@ -1922,7 +1922,7 @@ private void initiateIceRestart() { sendSessionTerminate(Reason.FAILED_APPLICATION, cause.getMessage()); return; } - final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); + final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription, isInitiator()); final RtpContentMap transportInfo = rtpContentMap.transportInfo(); final JinglePacket jinglePacket = transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java index bba44f963..f9c245b0e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.xmpp.jingle; +import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.Collections2; @@ -15,6 +16,8 @@ import java.util.Map; import java.util.Set; +import javax.annotation.Nonnull; + import eu.siacs.conversations.xmpp.jingle.stanzas.Content; import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo; @@ -58,13 +61,15 @@ private static boolean isOmemoVerified(Map content return true; } - public static RtpContentMap of(final SessionDescription sessionDescription) { + public static RtpContentMap of( + final SessionDescription sessionDescription, final boolean isInitiator) { final ImmutableMap.Builder contentMapBuilder = new ImmutableMap.Builder<>(); for (SessionDescription.Media media : sessionDescription.media) { final String id = Iterables.getFirst(media.attributes.get("mid"), null); Preconditions.checkNotNull(id, "media has no mid"); - contentMapBuilder.put(id, DescriptionTransport.of(sessionDescription, media)); + contentMapBuilder.put( + id, DescriptionTransport.of(sessionDescription, isInitiator, media)); } final String groupAttribute = Iterables.getFirst(sessionDescription.attributes.get("group"), null); @@ -140,11 +145,16 @@ JinglePacket toJinglePacket(final JinglePacket.Action action, final String sessi jinglePacket.addGroup(this.group); } for (Map.Entry entry : this.contents.entrySet()) { - final Content content = new Content(Content.Creator.INITIATOR, entry.getKey()); - if (entry.getValue().description != null) { - content.addChild(entry.getValue().description); + final DescriptionTransport descriptionTransport = entry.getValue(); + final Content content = + new Content( + Content.Creator.INITIATOR, + descriptionTransport.senders, + entry.getKey()); + if (descriptionTransport.description != null) { + content.addChild(descriptionTransport.description); } - content.addChild(entry.getValue().transport); + content.addChild(descriptionTransport.transport); jinglePacket.addJingleContent(content); } return jinglePacket; @@ -163,7 +173,10 @@ RtpContentMap transportInfo( newTransportInfo.addChild(candidate); return new RtpContentMap( null, - ImmutableMap.of(contentName, new DescriptionTransport(null, newTransportInfo))); + ImmutableMap.of( + contentName, + new DescriptionTransport( + descriptionTransport.senders, null, newTransportInfo))); } RtpContentMap transportInfo() { @@ -171,7 +184,9 @@ RtpContentMap transportInfo() { null, Maps.transformValues( contents, - dt -> new DescriptionTransport(null, dt.transport.cloneWrapper()))); + dt -> + new DescriptionTransport( + dt.senders, null, dt.transport.cloneWrapper()))); } public IceUdpTransportInfo.Credentials getDistinctCredentials() { @@ -179,7 +194,8 @@ public IceUdpTransportInfo.Credentials getDistinctCredentials() { final IceUdpTransportInfo.Credentials credentials = Iterables.getFirst(allCredentials, null); if (allCredentials.size() == 1 && credentials != null) { - if (Strings.isNullOrEmpty(credentials.password) || Strings.isNullOrEmpty(credentials.ufrag)) { + if (Strings.isNullOrEmpty(credentials.password) + || Strings.isNullOrEmpty(credentials.ufrag)) { throw new IllegalStateException("Credentials are missing password or ufrag"); } return credentials; @@ -233,23 +249,45 @@ public RtpContentMap modifiedCredentials( final ImmutableMap.Builder contentMapBuilder = new ImmutableMap.Builder<>(); for (final Map.Entry content : contents.entrySet()) { - final RtpDescription rtpDescription = content.getValue().description; - IceUdpTransportInfo transportInfo = content.getValue().transport; + final DescriptionTransport descriptionTransport = content.getValue(); + final RtpDescription rtpDescription = descriptionTransport.description; + final IceUdpTransportInfo transportInfo = descriptionTransport.transport; final IceUdpTransportInfo modifiedTransportInfo = transportInfo.modifyCredentials(credentials, setup); contentMapBuilder.put( content.getKey(), - new DescriptionTransport(rtpDescription, modifiedTransportInfo)); + new DescriptionTransport( + descriptionTransport.senders, rtpDescription, modifiedTransportInfo)); } return new RtpContentMap(this.group, contentMapBuilder.build()); } + public Diff diff(final RtpContentMap rtpContentMap) { + final Set existingContentIds = this.contents.keySet(); + final Set newContentIds = rtpContentMap.contents.keySet(); + return new Diff( + Sets.difference(newContentIds, existingContentIds), + Sets.difference(existingContentIds, newContentIds)); + } + + public boolean iceRestart(final RtpContentMap rtpContentMap) { + try { + return !getDistinctCredentials().equals(rtpContentMap.getDistinctCredentials()); + } catch (final IllegalStateException e) { + return false; + } + } + public static class DescriptionTransport { + public final Content.Senders senders; public final RtpDescription description; public final IceUdpTransportInfo transport; public DescriptionTransport( - final RtpDescription description, final IceUdpTransportInfo transport) { + final Content.Senders senders, + final RtpDescription description, + final IceUdpTransportInfo transport) { + this.senders = senders; this.description = description; this.transport = transport; } @@ -257,6 +295,7 @@ public DescriptionTransport( public static DescriptionTransport of(final Content content) { final GenericDescription description = content.getDescription(); final GenericTransportInfo transportInfo = content.getTransport(); + final Content.Senders senders = content.getSenders(); final RtpDescription rtpDescription; final IceUdpTransportInfo iceUdpTransportInfo; if (description == null) { @@ -274,22 +313,26 @@ public static DescriptionTransport of(final Content content) { "Content does not contain ICE-UDP transport"); } return new DescriptionTransport( - rtpDescription, OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo)); + senders, + rtpDescription, + OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo)); } - public static DescriptionTransport of( - final SessionDescription sessionDescription, final SessionDescription.Media media) { + private static DescriptionTransport of( + final SessionDescription sessionDescription, + final boolean isInitiator, + final SessionDescription.Media media) { + final Content.Senders senders = Content.Senders.of(media, isInitiator); final RtpDescription rtpDescription = RtpDescription.of(sessionDescription, media); final IceUdpTransportInfo transportInfo = IceUdpTransportInfo.of(sessionDescription, media); - return new DescriptionTransport(rtpDescription, transportInfo); + return new DescriptionTransport(senders, rtpDescription, transportInfo); } public static Map of(final Map contents) { return ImmutableMap.copyOf( Maps.transformValues( - contents, - content -> content == null ? null : of(content))); + contents, content -> content == null ? null : of(content))); } } @@ -304,4 +347,27 @@ public static class UnsupportedTransportException extends IllegalArgumentExcepti super(message); } } + + public static final class Diff { + public final Set added; + public final Set removed; + + private Diff(final Set added, final Set removed) { + this.added = added; + this.removed = removed; + } + + public boolean hasModifications() { + return !this.added.isEmpty() || !this.removed.isEmpty(); + } + + @Override + @Nonnull + public String toString() { + return MoreObjects.toStringHelper(this) + .add("added", added) + .add("removed", removed) + .toString(); + } + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java index e113146b1..eef7ae0da 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java @@ -3,6 +3,8 @@ import android.util.Log; import android.util.Pair; +import androidx.annotation.NonNull; + import com.google.common.base.CharMatcher; import com.google.common.base.Joiner; import com.google.common.base.Strings; @@ -21,11 +23,12 @@ public class SessionDescription { - public final static String LINE_DIVIDER = "\r\n"; - private final static String HARDCODED_MEDIA_PROTOCOL = "UDP/TLS/RTP/SAVPF"; //probably only true for DTLS-SRTP aka when we have a fingerprint - private final static int HARDCODED_MEDIA_PORT = 9; - private final static String HARDCODED_ICE_OPTIONS = "trickle"; - private final static String HARDCODED_CONNECTION = "IN IP4 0.0.0.0"; + public static final String LINE_DIVIDER = "\r\n"; + private static final String HARDCODED_MEDIA_PROTOCOL = + "UDP/TLS/RTP/SAVPF"; // probably only true for DTLS-SRTP aka when we have a fingerprint + private static final int HARDCODED_MEDIA_PORT = 9; + private static final String HARDCODED_ICE_OPTIONS = "trickle"; + private static final String HARDCODED_CONNECTION = "IN IP4 0.0.0.0"; public final int version; public final String name; @@ -33,8 +36,12 @@ public class SessionDescription { public final ArrayListMultimap attributes; public final List media; - - public SessionDescription(int version, String name, String connectionData, ArrayListMultimap attributes, List media) { + public SessionDescription( + int version, + String name, + String connectionData, + ArrayListMultimap attributes, + List media) { this.version = version; this.name = name; this.connectionData = connectionData; @@ -42,7 +49,8 @@ public SessionDescription(int version, String name, String connectionData, Array this.media = media; } - private static void appendAttributes(StringBuilder s, ArrayListMultimap attributes) { + private static void appendAttributes( + StringBuilder s, ArrayListMultimap attributes) { for (Map.Entry attribute : attributes.entries()) { final String key = attribute.getKey(); final String value = attribute.getValue(); @@ -109,7 +117,6 @@ public static SessionDescription parse(final String input) { } break; } - } if (currentMediaBuilder != null) { currentMediaBuilder.setAttributes(attributeMap); @@ -121,7 +128,7 @@ public static SessionDescription parse(final String input) { return sessionDescriptionBuilder.createSessionDescription(); } - public static SessionDescription of(final RtpContentMap contentMap) { + public static SessionDescription of(final RtpContentMap contentMap, final boolean isInitiatorContentMap) { final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder(); final ArrayListMultimap attributeMap = ArrayListMultimap.create(); final ImmutableList.Builder mediaListBuilder = new ImmutableList.Builder<>(); @@ -129,12 +136,17 @@ public static SessionDescription of(final RtpContentMap contentMap) { if (group != null) { final String semantics = group.getSemantics(); checkNoWhitespace(semantics, "group semantics value must not contain any whitespace"); - attributeMap.put("group", group.getSemantics() + " " + Joiner.on(' ').join(group.getIdentificationTags())); + attributeMap.put( + "group", + group.getSemantics() + + " " + + Joiner.on(' ').join(group.getIdentificationTags())); } attributeMap.put("msid-semantic", " WMS my-media-stream"); - for (final Map.Entry entry : contentMap.contents.entrySet()) { + for (final Map.Entry entry : + contentMap.contents.entrySet()) { final String name = entry.getKey(); RtpContentMap.DescriptionTransport descriptionTransport = entry.getValue(); RtpDescription description = descriptionTransport.description; @@ -143,19 +155,22 @@ public static SessionDescription of(final RtpContentMap contentMap) { final String ufrag = transport.getAttribute("ufrag"); final String pwd = transport.getAttribute("pwd"); if (Strings.isNullOrEmpty(ufrag)) { - throw new IllegalArgumentException("Transport element is missing required ufrag attribute"); + throw new IllegalArgumentException( + "Transport element is missing required ufrag attribute"); } checkNoWhitespace(ufrag, "ufrag value must not contain any whitespaces"); mediaAttributes.put("ice-ufrag", ufrag); if (Strings.isNullOrEmpty(pwd)) { - throw new IllegalArgumentException("Transport element is missing required pwd attribute"); + throw new IllegalArgumentException( + "Transport element is missing required pwd attribute"); } checkNoWhitespace(pwd, "pwd value must not contain any whitespaces"); mediaAttributes.put("ice-pwd", pwd); mediaAttributes.put("ice-options", HARDCODED_ICE_OPTIONS); final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint(); if (fingerprint != null) { - mediaAttributes.put("fingerprint", fingerprint.getHash() + " " + fingerprint.getContent()); + mediaAttributes.put( + "fingerprint", fingerprint.getHash() + " " + fingerprint.getContent()); final IceUdpTransportInfo.Setup setup = fingerprint.getSetup(); if (setup != null) { mediaAttributes.put("setup", setup.toString().toLowerCase(Locale.ROOT)); @@ -174,37 +189,56 @@ public static SessionDescription of(final RtpContentMap contentMap) { mediaAttributes.put("rtpmap", payloadType.toSdpAttribute()); final List parameters = payloadType.getParameters(); if (parameters.size() == 1) { - mediaAttributes.put("fmtp", RtpDescription.Parameter.toSdpString(id, parameters.get(0))); + mediaAttributes.put( + "fmtp", RtpDescription.Parameter.toSdpString(id, parameters.get(0))); } else if (parameters.size() > 0) { - mediaAttributes.put("fmtp", RtpDescription.Parameter.toSdpString(id, parameters)); + mediaAttributes.put( + "fmtp", RtpDescription.Parameter.toSdpString(id, parameters)); } - for (RtpDescription.FeedbackNegotiation feedbackNegotiation : payloadType.getFeedbackNegotiations()) { + for (RtpDescription.FeedbackNegotiation feedbackNegotiation : + payloadType.getFeedbackNegotiations()) { final String type = feedbackNegotiation.getType(); final String subtype = feedbackNegotiation.getSubType(); if (Strings.isNullOrEmpty(type)) { - throw new IllegalArgumentException("a feedback for payload-type " + id + " negotiation is missing type"); + throw new IllegalArgumentException( + "a feedback for payload-type " + + id + + " negotiation is missing type"); } - checkNoWhitespace(type, "feedback negotiation type must not contain whitespace"); - mediaAttributes.put("rtcp-fb", id + " " + type + (Strings.isNullOrEmpty(subtype) ? "" : " " + subtype)); + checkNoWhitespace( + type, "feedback negotiation type must not contain whitespace"); + mediaAttributes.put( + "rtcp-fb", + id + + " " + + type + + (Strings.isNullOrEmpty(subtype) ? "" : " " + subtype)); } - for (RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt : payloadType.feedbackNegotiationTrrInts()) { - mediaAttributes.put("rtcp-fb", id + " trr-int " + feedbackNegotiationTrrInt.getValue()); + for (RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt : + payloadType.feedbackNegotiationTrrInts()) { + mediaAttributes.put( + "rtcp-fb", id + " trr-int " + feedbackNegotiationTrrInt.getValue()); } } - for (RtpDescription.FeedbackNegotiation feedbackNegotiation : description.getFeedbackNegotiations()) { + for (RtpDescription.FeedbackNegotiation feedbackNegotiation : + description.getFeedbackNegotiations()) { final String type = feedbackNegotiation.getType(); final String subtype = feedbackNegotiation.getSubType(); if (Strings.isNullOrEmpty(type)) { throw new IllegalArgumentException("a feedback negotiation is missing type"); } checkNoWhitespace(type, "feedback negotiation type must not contain whitespace"); - mediaAttributes.put("rtcp-fb", "* " + type + (Strings.isNullOrEmpty(subtype) ? "" : " " + subtype)); + mediaAttributes.put( + "rtcp-fb", + "* " + type + (Strings.isNullOrEmpty(subtype) ? "" : " " + subtype)); } - for (final RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt : description.feedbackNegotiationTrrInts()) { + for (final RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt : + description.feedbackNegotiationTrrInts()) { mediaAttributes.put("rtcp-fb", "* trr-int " + feedbackNegotiationTrrInt.getValue()); } - for (final RtpDescription.RtpHeaderExtension extension : description.getHeaderExtensions()) { + for (final RtpDescription.RtpHeaderExtension extension : + description.getHeaderExtensions()) { final String id = extension.getId(); final String uri = extension.getUri(); if (Strings.isNullOrEmpty(id)) { @@ -218,7 +252,8 @@ public static SessionDescription of(final RtpContentMap contentMap) { mediaAttributes.put("extmap", id + " " + uri); } - if (description.hasChild("extmap-allow-mixed", Namespace.JINGLE_RTP_HEADER_EXTENSIONS)) { + if (description.hasChild( + "extmap-allow-mixed", Namespace.JINGLE_RTP_HEADER_EXTENSIONS)) { mediaAttributes.put("extmap-allow-mixed", ""); } @@ -226,13 +261,16 @@ public static SessionDescription of(final RtpContentMap contentMap) { final String semantics = sourceGroup.getSemantics(); final List groups = sourceGroup.getSsrcs(); if (Strings.isNullOrEmpty(semantics)) { - throw new IllegalArgumentException("A SSRC group is missing semantics attribute"); + throw new IllegalArgumentException( + "A SSRC group is missing semantics attribute"); } checkNoWhitespace(semantics, "source group semantics must not contain whitespace"); if (groups.size() == 0) { throw new IllegalArgumentException("A SSRC group is missing SSRC ids"); } - mediaAttributes.put("ssrc-group", String.format("%s %s", semantics, Joiner.on(' ').join(groups))); + mediaAttributes.put( + "ssrc-group", + String.format("%s %s", semantics, Joiner.on(' ').join(groups))); } for (final RtpDescription.Source source : description.getSources()) { for (final RtpDescription.Source.Parameter parameter : source.getParameters()) { @@ -240,14 +278,18 @@ public static SessionDescription of(final RtpContentMap contentMap) { final String parameterName = parameter.getParameterName(); final String parameterValue = parameter.getParameterValue(); if (Strings.isNullOrEmpty(id)) { - throw new IllegalArgumentException("A source specific media attribute is missing the id"); + throw new IllegalArgumentException( + "A source specific media attribute is missing the id"); } - checkNoWhitespace(id, "A source specific media attributes must not contain whitespaces"); + checkNoWhitespace( + id, "A source specific media attributes must not contain whitespaces"); if (Strings.isNullOrEmpty(parameterName)) { - throw new IllegalArgumentException("A source specific media attribute is missing its name"); + throw new IllegalArgumentException( + "A source specific media attribute is missing its name"); } if (Strings.isNullOrEmpty(parameterValue)) { - throw new IllegalArgumentException("A source specific media attribute is missing its value"); + throw new IllegalArgumentException( + "A source specific media attribute is missing its value"); } mediaAttributes.put("ssrc", id + " " + parameterName + ":" + parameterValue); } @@ -255,14 +297,14 @@ public static SessionDescription of(final RtpContentMap contentMap) { mediaAttributes.put("mid", name); - //random additional attributes - mediaAttributes.put("rtcp", "9 IN IP4 0.0.0.0"); - mediaAttributes.put("sendrecv", ""); - + mediaAttributes.put(descriptionTransport.senders.asMediaAttribute(isInitiatorContentMap), ""); if (description.hasChild("rtcp-mux", Namespace.JINGLE_APPS_RTP)) { mediaAttributes.put("rtcp-mux", ""); } + // random additional attributes + mediaAttributes.put("rtcp", "9 IN IP4 0.0.0.0"); + final MediaBuilder mediaBuilder = new MediaBuilder(); mediaBuilder.setMedia(description.getMedia().toString().toLowerCase(Locale.ROOT)); mediaBuilder.setConnectionData(HARDCODED_CONNECTION); @@ -271,7 +313,6 @@ public static SessionDescription of(final RtpContentMap contentMap) { mediaBuilder.setAttributes(mediaAttributes); mediaBuilder.setFormats(formatBuilder.build()); mediaListBuilder.add(mediaBuilder.createMedia()); - } sessionDescriptionBuilder.setVersion(0); sessionDescriptionBuilder.setName("-"); @@ -317,17 +358,33 @@ public static Pair parseAttribute(final String input) { } } + @NonNull @Override public String toString() { - final StringBuilder s = new StringBuilder() - .append("v=").append(version).append(LINE_DIVIDER) - //TODO randomize or static - .append("o=- 8770656990916039506 2 IN IP4 127.0.0.1").append(LINE_DIVIDER) //what ever that means - .append("s=").append(name).append(LINE_DIVIDER) - .append("t=0 0").append(LINE_DIVIDER); + final StringBuilder s = + new StringBuilder() + .append("v=") + .append(version) + .append(LINE_DIVIDER) + // TODO randomize or static + .append("o=- 8770656990916039506 2 IN IP4 127.0.0.1") + .append(LINE_DIVIDER) // what ever that means + .append("s=") + .append(name) + .append(LINE_DIVIDER) + .append("t=0 0") + .append(LINE_DIVIDER); appendAttributes(s, attributes); for (Media media : this.media) { - s.append("m=").append(media.media).append(' ').append(media.port).append(' ').append(media.protocol).append(' ').append(Joiner.on(' ').join(media.formats)).append(LINE_DIVIDER); + s.append("m=") + .append(media.media) + .append(' ') + .append(media.port) + .append(' ') + .append(media.protocol) + .append(' ') + .append(Joiner.on(' ').join(media.formats)) + .append(LINE_DIVIDER); s.append("c=").append(media.connectionData).append(LINE_DIVIDER); appendAttributes(s, media.attributes); } @@ -342,7 +399,13 @@ public static class Media { public final String connectionData; public final ArrayListMultimap attributes; - public Media(String media, int port, String protocol, List formats, String connectionData, ArrayListMultimap attributes) { + public Media( + String media, + int port, + String protocol, + List formats, + String connectionData, + ArrayListMultimap attributes) { this.media = media; this.port = port; this.protocol = protocol; @@ -351,5 +414,4 @@ public Media(String media, int port, String protocol, List formats, Str this.attributes = attributes; } } - } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java index e21c38968..962515293 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java @@ -1,20 +1,27 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; +import android.util.Log; + import androidx.annotation.NonNull; import com.google.common.base.Preconditions; +import com.google.common.base.Strings; import java.util.Locale; +import java.util.Set; +import eu.siacs.conversations.Config; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.jingle.SessionDescription; public class Content extends Element { - public Content(final Creator creator, final String name) { + public Content(final Creator creator, final Senders senders, final String name) { super("content", Namespace.JINGLE); this.setAttribute("creator", creator.toString()); this.setAttribute("name", name); + this.setSenders(senders); } private Content() { @@ -38,11 +45,17 @@ public Creator getCreator() { } public Senders getSenders() { + final String attribute = getAttribute("senders"); + if (Strings.isNullOrEmpty(attribute)) { + return Senders.BOTH; + } return Senders.of(getAttribute("senders")); } - public void setSenders(Senders senders) { - this.setAttribute("senders", senders.toString()); + public void setSenders(final Senders senders) { + if (senders != null && senders != Senders.BOTH) { + this.setAttribute("senders", senders.toString()); + } } public GenericDescription getDescription() { @@ -51,9 +64,7 @@ public GenericDescription getDescription() { return null; } final String namespace = description.getNamespace(); - if (FileTransferDescription.NAMESPACES.contains(namespace)) { - return FileTransferDescription.upgrade(description); - } else if (Namespace.JINGLE_APPS_RTP.equals(namespace)) { + if (Namespace.JINGLE_APPS_RTP.equals(namespace)) { return RtpDescription.upgrade(description); } else { return GenericDescription.upgrade(description); @@ -73,11 +84,7 @@ public String getDescriptionNamespace() { public GenericTransportInfo getTransport() { final Element transport = this.findChild("transport"); final String namespace = transport == null ? null : transport.getNamespace(); - if (Namespace.JINGLE_TRANSPORTS_IBB.equals(namespace)) { - return IbbTransportInfo.upgrade(transport); - } else if (Namespace.JINGLE_TRANSPORTS_S5B.equals(namespace)) { - return S5BTransportInfo.upgrade(transport); - } else if (Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(namespace)) { + if (Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(namespace)) { return IceUdpTransportInfo.upgrade(transport); } else if (transport != null) { return GenericTransportInfo.upgrade(transport); @@ -91,7 +98,8 @@ public void setTransport(GenericTransportInfo transportInfo) { } public enum Creator { - INITIATOR, RESPONDER; + INITIATOR, + RESPONDER; public static Creator of(final String value) { return Creator.valueOf(value.toUpperCase(Locale.ROOT)); @@ -105,16 +113,56 @@ public String toString() { } public enum Senders { - BOTH, INITIATOR, NONE, RESPONDER; + BOTH, + INITIATOR, + NONE, + RESPONDER; public static Senders of(final String value) { return Senders.valueOf(value.toUpperCase(Locale.ROOT)); } + public static Senders of(final SessionDescription.Media media, final boolean initiator) { + final Set attributes = media.attributes.keySet(); + if (attributes.contains("sendrecv")) { + return BOTH; + } else if (attributes.contains("inactive")) { + return NONE; + } else if (attributes.contains("sendonly")) { + return initiator ? INITIATOR : RESPONDER; + } else if (attributes.contains("recvonly")) { + return initiator ? RESPONDER : INITIATOR; + } + Log.w(Config.LOGTAG,"assuming default value for senders"); + // If none of the attributes "sendonly", "recvonly", "inactive", and "sendrecv" is + // present, "sendrecv" SHOULD be assumed as the default + // https://www.rfc-editor.org/rfc/rfc4566 + return BOTH; + } + @Override @NonNull public String toString() { return super.toString().toLowerCase(Locale.ROOT); } + + public String asMediaAttribute(final boolean initiator) { + final boolean responder = !initiator; + if (this == Content.Senders.BOTH) { + return "sendrecv"; + } else if (this == Content.Senders.NONE) { + return "inactive"; + } else if ((initiator && this == Content.Senders.INITIATOR) + || (responder && this == Content.Senders.RESPONDER)) { + return "sendonly"; + } else if ((initiator && this == Content.Senders.RESPONDER) + || (responder && this == Content.Senders.INITIATOR)) { + return "recvonly"; + } else { + throw new IllegalStateException( + String.format( + "illegal combination of initiator=%s and %s", initiator, this)); + } + } } } From 9897fa3a456f11b5603a662a3367a817e94ec3ba Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 21 Nov 2022 09:10:01 +0100 Subject: [PATCH 245/394] rename initiateIceRestart to renegotiate to handle content adds --- .../xmpp/jingle/JingleRtpConnection.java | 47 +++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 5cccafa5a..0c68f663c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -7,12 +7,14 @@ import com.google.common.base.Optional; import com.google.common.base.Preconditions; +import com.google.common.base.Predicates; import com.google.common.base.Stopwatch; import com.google.common.base.Strings; import com.google.common.base.Throwables; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.primitives.Ints; import com.google.common.util.concurrent.FutureCallback; @@ -499,6 +501,10 @@ private RtpContentMap getRemoteContentMap() { return isInitiator() ? this.responderRtpContentMap : this.initiatorRtpContentMap; } + private RtpContentMap getLocalContentMap() { + return isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap; + } + private List toIdentificationTags(final RtpContentMap rtpContentMap) { final Group originalGroup = rtpContentMap.group; final List identificationTags = @@ -1906,11 +1912,11 @@ public void onConnectionChange(final PeerConnection.PeerConnectionState newState @Override public void onRenegotiationNeeded() { - this.webRTCWrapper.execute(this::initiateIceRestart); + this.webRTCWrapper.execute(this::renegotiate); } - private void initiateIceRestart() { - // TODO discover new TURN/STUN credentials + private void renegotiate() { + //TODO needs to be called only for ice restarts; maybe in the call to restartICe() this.stateHistory.clear(); this.webRTCWrapper.setIsReadyToReceiveIceCandidates(false); final SessionDescription sessionDescription; @@ -1919,10 +1925,41 @@ private void initiateIceRestart() { } catch (final Exception e) { final Throwable cause = Throwables.getRootCause(e); Log.d(Config.LOGTAG, "failed to renegotiate", cause); + webRTCWrapper.close(); sendSessionTerminate(Reason.FAILED_APPLICATION, cause.getMessage()); return; } final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription, isInitiator()); + final RtpContentMap currentContentMap = getLocalContentMap(); + final boolean iceRestart = currentContentMap.iceRestart(rtpContentMap); + final RtpContentMap.Diff diff = currentContentMap.diff(rtpContentMap); + + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + + ": renegotiate. iceRestart=" + + iceRestart + + " content id diff=" + + diff); + + if (diff.hasModifications() && iceRestart) { + webRTCWrapper.close(); + sendSessionTerminate(Reason.FAILED_APPLICATION, "WebRTC unexpectedly tried to modify content and transport at once"); + return; + } + + if (iceRestart) { + initiateIceRestart(rtpContentMap); + return; + } + + if (diff.added.size() > 0) { + sendContentAdd(rtpContentMap); + } + + } + + private void initiateIceRestart(final RtpContentMap rtpContentMap) { final RtpContentMap transportInfo = rtpContentMap.transportInfo(); final JinglePacket jinglePacket = transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); @@ -1952,6 +1989,10 @@ private void initiateIceRestart() { }); } + private void sendContentAdd(final RtpContentMap rtpContentMap) { + + } + private void setLocalContentMap(final RtpContentMap rtpContentMap) { if (isInitiator()) { this.initiatorRtpContentMap = rtpContentMap; From e2f98f6bbc819bbacd96d7b4e69aef7f740de745 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 22 Nov 2022 10:13:07 +0100 Subject: [PATCH 246/394] ensure cc-ed proceed is equivalent to accept --- .../xmpp/jingle/JingleRtpConnection.java | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 0c68f663c..ad5aeac0e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -952,16 +952,7 @@ private void receiveAccept(final Jid from, final String serverMsgId, final long from.asBareJid().equals(id.account.getJid().asBareJid()); if (originatedFromMyself) { if (transition(State.ACCEPTED)) { - if (serverMsgId != null) { - this.message.setServerMsgId(serverMsgId); - } - this.message.setTime(timestamp); - this.message.setCarbon(true); // indicate that call was accepted on other device - this.writeLogMessageSuccess(0); - this.xmppConnectionService - .getNotificationService() - .cancelIncomingCallNotification(); - this.finish(); + acceptedOnOtherDevice(serverMsgId, timestamp); } else { Log.d( Config.LOGTAG, @@ -976,6 +967,19 @@ private void receiveAccept(final Jid from, final String serverMsgId, final long } } + private void acceptedOnOtherDevice(final String serverMsgId, final long timestamp) { + if (serverMsgId != null) { + this.message.setServerMsgId(serverMsgId); + } + this.message.setTime(timestamp); + this.message.setCarbon(true); // indicate that call was accepted on other device + this.writeLogMessageSuccess(0); + this.xmppConnectionService + .getNotificationService() + .cancelIncomingCallNotification(); + this.finish(); + } + private void receiveReject(final Jid from, final String serverMsgId, final long timestamp) { final boolean originatedFromMyself = from.asBareJid().equals(id.account.getJid().asBareJid()); @@ -1173,11 +1177,8 @@ private void receiveProceed( id.account.getJid().asBareJid() + ": moved session with " + id.with - + " into state accepted after received carbon copied procced"); - this.xmppConnectionService - .getNotificationService() - .cancelIncomingCallNotification(); - this.finish(); + + " into state accepted after received carbon copied proceed"); + acceptedOnOtherDevice(serverMsgId, timestamp); } } else { Log.d( From f4be142e4d70b90f15bd0793e2870e9ceb13629b Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 22 Nov 2022 10:13:48 +0100 Subject: [PATCH 247/394] add helper methods for content modification to RtpContentMap --- .../xmpp/jingle/RtpContentMap.java | 80 ++++++++++++++++++- .../xmpp/jingle/WebRTCWrapper.java | 9 ++- .../jingle/stanzas/IceUdpTransportInfo.java | 70 +++++++++++----- 3 files changed, 137 insertions(+), 22 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java index f9c245b0e..7af1469cf 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -1,7 +1,9 @@ package eu.siacs.conversations.xmpp.jingle; import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; import com.google.common.base.Preconditions; +import com.google.common.base.Predicates; import com.google.common.base.Strings; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; @@ -236,6 +238,23 @@ public IceUdpTransportInfo.Setup getDtlsSetup() { throw new IllegalStateException("Content map doesn't have distinct DTLS setup"); } + private DTLS getDistinctDtls() { + final Set dtlsSet = + ImmutableSet.copyOf( + Collections2.transform( + contents.values(), + dt -> { + final IceUdpTransportInfo.Fingerprint fp = + dt.transport.getFingerprint(); + return new DTLS(fp.getHash(), fp.getSetup(), fp.getContent()); + })); + final DTLS dtls = Iterables.getFirst(dtlsSet, null); + if (dtlsSet.size() == 1 && dtls != null) { + return dtls; + } + throw new IllegalStateException("Content map doesn't have distinct DTLS setup"); + } + public boolean emptyCandidates() { int count = 0; for (DescriptionTransport descriptionTransport : contents.values()) { @@ -262,12 +281,22 @@ public RtpContentMap modifiedCredentials( return new RtpContentMap(this.group, contentMapBuilder.build()); } + public RtpContentMap toContentModification(final Collection modifications) { + return new RtpContentMap( + this.group, + Maps.transformValues( + Maps.filterKeys(contents, Predicates.in(modifications)), + dt -> + new DescriptionTransport( + dt.senders, dt.description, IceUdpTransportInfo.STUB))); + } + public Diff diff(final RtpContentMap rtpContentMap) { final Set existingContentIds = this.contents.keySet(); final Set newContentIds = rtpContentMap.contents.keySet(); return new Diff( - Sets.difference(newContentIds, existingContentIds), - Sets.difference(existingContentIds, newContentIds)); + ImmutableSet.copyOf(Sets.difference(newContentIds, existingContentIds)), + ImmutableSet.copyOf(Sets.difference(existingContentIds, newContentIds))); } public boolean iceRestart(final RtpContentMap rtpContentMap) { @@ -278,6 +307,26 @@ public boolean iceRestart(final RtpContentMap rtpContentMap) { } } + public RtpContentMap addContent(final RtpContentMap modification) { + final IceUdpTransportInfo.Credentials credentials = getDistinctCredentials(); + final DTLS dtls = getDistinctDtls(); + final IceUdpTransportInfo iceUdpTransportInfo = + IceUdpTransportInfo.of(credentials, dtls.setup, dtls.hash, dtls.fingerprint); + final Map combined = + new ImmutableMap.Builder() + .putAll(contents) + .putAll( + Maps.transformValues( + modification.contents, + dt -> + new DescriptionTransport( + dt.senders, + dt.description, + iceUdpTransportInfo))) + .build(); + return new RtpContentMap(modification.group, combined); + } + public static class DescriptionTransport { public final Content.Senders senders; public final RtpDescription description; @@ -370,4 +419,31 @@ public String toString() { .toString(); } } + + public static final class DTLS { + public final String hash; + public final IceUdpTransportInfo.Setup setup; + public final String fingerprint; + + private DTLS(String hash, IceUdpTransportInfo.Setup setup, String fingerprint) { + this.hash = hash; + this.setup = setup; + this.fingerprint = fingerprint; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DTLS dtls = (DTLS) o; + return Objects.equal(hash, dtls.hash) + && setup == dtls.setup + && Objects.equal(fingerprint, dtls.fingerprint); + } + + @Override + public int hashCode() { + return Objects.hashCode(hash, setup, fingerprint); + } + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index de26f1afc..53b7de1e0 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -186,15 +186,22 @@ public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) { } @Override - public void onTrack(RtpTransceiver transceiver) { + public void onTrack(final RtpTransceiver transceiver) { Log.d( EXTENDED_LOGGING_TAG, "onTrack(mid=" + transceiver.getMid() + ",media=" + transceiver.getMediaType() + + ",direction=" + + transceiver.getDirection() + ")"); } + + @Override + public void onRemoveTrack(final RtpReceiver receiver) { + Log.d(EXTENDED_LOGGING_TAG, "onRemoveTrack(" + receiver.id() + ")"); + } }; @Nullable private PeerConnectionFactory peerConnectionFactory = null; @Nullable private PeerConnection peerConnection = null; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java index ee8d12b70..432333090 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java @@ -12,8 +12,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; -import java.util.ArrayList; -import java.util.Collection; import java.util.HashMap; import java.util.Hashtable; import java.util.LinkedHashMap; @@ -28,23 +26,29 @@ public class IceUdpTransportInfo extends GenericTransportInfo { + public static final IceUdpTransportInfo STUB = new IceUdpTransportInfo(); + public IceUdpTransportInfo() { super("transport", Namespace.JINGLE_TRANSPORT_ICE_UDP); } public static IceUdpTransportInfo upgrade(final Element element) { - Preconditions.checkArgument("transport".equals(element.getName()), "Name of provided element is not transport"); - Preconditions.checkArgument(Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(element.getNamespace()), "Element does not match ice-udp transport namespace"); + Preconditions.checkArgument( + "transport".equals(element.getName()), "Name of provided element is not transport"); + Preconditions.checkArgument( + Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(element.getNamespace()), + "Element does not match ice-udp transport namespace"); final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo(); transportInfo.setAttributes(element.getAttributes()); transportInfo.setChildren(element.getChildren()); return transportInfo; } - public static IceUdpTransportInfo of(SessionDescription sessionDescription, SessionDescription.Media media) { + public static IceUdpTransportInfo of( + SessionDescription sessionDescription, SessionDescription.Media media) { final String ufrag = Iterables.getFirst(media.attributes.get("ice-ufrag"), null); final String pwd = Iterables.getFirst(media.attributes.get("ice-pwd"), null); - IceUdpTransportInfo iceUdpTransportInfo = new IceUdpTransportInfo(); + final IceUdpTransportInfo iceUdpTransportInfo = new IceUdpTransportInfo(); if (ufrag != null) { iceUdpTransportInfo.setAttribute("ufrag", ufrag); } @@ -56,7 +60,15 @@ public static IceUdpTransportInfo of(SessionDescription sessionDescription, Sess iceUdpTransportInfo.addChild(fingerprint); } return iceUdpTransportInfo; + } + public static IceUdpTransportInfo of( + final Credentials credentials, final Setup setup, final String hash, final String fingerprint) { + final IceUdpTransportInfo iceUdpTransportInfo = new IceUdpTransportInfo(); + iceUdpTransportInfo.addChild(Fingerprint.of(setup, hash, fingerprint)); + iceUdpTransportInfo.setAttribute("ufrag", credentials.ufrag); + iceUdpTransportInfo.setAttribute("pwd", credentials.password); + return iceUdpTransportInfo; } public Fingerprint getFingerprint() { @@ -91,7 +103,8 @@ public IceUdpTransportInfo modifyCredentials(final Credentials credentials, fina transportInfo.setAttribute("ufrag", credentials.ufrag); transportInfo.setAttribute("pwd", credentials.password); for (final Element child : getChildren()) { - if (child.getName().equals("fingerprint") && Namespace.JINGLE_APPS_DTLS.equals(child.getNamespace())) { + if (child.getName().equals("fingerprint") + && Namespace.JINGLE_APPS_DTLS.equals(child.getNamespace())) { final Fingerprint fingerprint = new Fingerprint(); fingerprint.setAttributes(new Hashtable<>(child.getAttributes())); fingerprint.setContent(child.getContent()); @@ -231,7 +244,7 @@ public int getRelPort() { return getAttributeAsInt("rel-port"); } - public String getType() { //TODO might be converted to enum + public String getType() { // TODO might be converted to enum return getAttribute("type"); } @@ -256,7 +269,8 @@ public String toSdpAttribute(final String ufrag) { checkNotNullNoWhitespace(protocol, "protocol"); final String transport = protocol.toLowerCase(Locale.ROOT); if (!"udp".equals(transport)) { - throw new IllegalArgumentException(String.format("'%s' is not a supported protocol", transport)); + throw new IllegalArgumentException( + String.format("'%s' is not a supported protocol", transport)); } final String priority = this.getAttribute("priority"); checkNotNullNoWhitespace(priority, "priority"); @@ -284,7 +298,15 @@ public String toSdpAttribute(final String ufrag) { if (ufrag != null) { additionalParameter.put("ufrag", ufrag); } - final String parametersString = Joiner.on(' ').join(Collections2.transform(additionalParameter.entrySet(), input -> String.format("%s %s", input.getKey(), input.getValue()))); + final String parametersString = + Joiner.on(' ') + .join( + Collections2.transform( + additionalParameter.entrySet(), + input -> + String.format( + "%s %s", + input.getKey(), input.getValue()))); return String.format( "candidate:%s %s %s %s %s %s %s", foundation, @@ -293,20 +315,19 @@ public String toSdpAttribute(final String ufrag) { priority, connectionAddress, port, - parametersString - - ); + parametersString); } } private static void checkNotNullNoWhitespace(final String value, final String name) { if (Strings.isNullOrEmpty(value)) { - throw new IllegalArgumentException(String.format("Parameter %s is missing or empty", name)); + throw new IllegalArgumentException( + String.format("Parameter %s is missing or empty", name)); } - SessionDescription.checkNoWhitespace(value, String.format("Parameter %s contains white spaces", name)); + SessionDescription.checkNoWhitespace( + value, String.format("Parameter %s contains white spaces", name)); } - public static class Fingerprint extends Element { private Fingerprint() { @@ -340,11 +361,20 @@ private static Fingerprint of(ArrayListMultimap attributes) { return null; } - public static Fingerprint of(final SessionDescription sessionDescription, final SessionDescription.Media media) { + public static Fingerprint of( + final SessionDescription sessionDescription, final SessionDescription.Media media) { final Fingerprint fingerprint = of(media.attributes); return fingerprint == null ? of(sessionDescription.attributes) : fingerprint; } + private static Fingerprint of(final Setup setup, final String hash, final String content) { + final Fingerprint fingerprint = new Fingerprint(); + fingerprint.setContent(content); + fingerprint.setAttribute("hash", hash); + fingerprint.setAttribute("setup", setup.toString().toLowerCase(Locale.ROOT)); + return fingerprint; + } + public String getHash() { return this.getAttribute("hash"); } @@ -356,7 +386,9 @@ public Setup getSetup() { } public enum Setup { - ACTPASS, PASSIVE, ACTIVE; + ACTPASS, + PASSIVE, + ACTIVE; public static Setup of(String setup) { try { @@ -373,7 +405,7 @@ public Setup flip() { if (this == ACTIVE) { return PASSIVE; } - throw new IllegalStateException(this.name()+" can not be flipped"); + throw new IllegalStateException(this.name() + " can not be flipped"); } } } From 63501adc45804ced79486fe7e7ff32213f38ef49 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 25 Nov 2022 08:50:58 +0100 Subject: [PATCH 248/394] trim xmpp address after user input --- src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java | 2 +- .../eu/siacs/conversations/ui/StartConversationActivity.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java index 15ebdb0b7..9ffd9c673 100644 --- a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java +++ b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java @@ -170,7 +170,7 @@ private void handleEnter(EnterJidDialogBinding binding, String account) { } final Jid contactJid; try { - contactJid = Jid.ofEscaped(binding.jid.getText().toString()); + contactJid = Jid.ofEscaped(binding.jid.getText().toString().trim()); } catch (final IllegalArgumentException e) { binding.jidLayout.setError(getActivity().getString(R.string.invalid_jid)); return; diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index 91807295b..7a2ffc0d8 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -1054,7 +1054,7 @@ public void onJoinDialogPositiveClick(Dialog dialog, Spinner spinner, TextInputL if (account == null) { return; } - final String input = jid.getText().toString(); + final String input = jid.getText().toString().trim(); Jid conferenceJid; try { conferenceJid = Jid.ofEscaped(input); From 4e8ceadfbf9f38cc9a53b7e912c9a7eb926dc2df Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 28 Nov 2022 08:59:23 +0100 Subject: [PATCH 249/394] prepare JingleRtpConnection for content-adds --- .../services/AppRTCAudioManager.java | 23 +- .../xmpp/jingle/ContentAddition.java | 88 +++ .../xmpp/jingle/JingleRtpConnection.java | 536 +++++++++++++++++- .../conversations/xmpp/jingle/Media.java | 15 + .../xmpp/jingle/RtpContentMap.java | 69 ++- .../xmpp/jingle/RtpEndUserState.java | 1 + .../xmpp/jingle/SessionDescription.java | 2 +- .../xmpp/jingle/ToneManager.java | 31 +- .../xmpp/jingle/TrackWrapper.java | 53 +- .../xmpp/jingle/WebRTCWrapper.java | 77 ++- .../xmpp/jingle/stanzas/RtpDescription.java | 136 +++-- 11 files changed, 921 insertions(+), 110 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/ContentAddition.java diff --git a/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java b/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java index e1fe854ff..3bed4eaba 100644 --- a/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java +++ b/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java @@ -33,6 +33,7 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.utils.AppRTCUtils; +import eu.siacs.conversations.xmpp.jingle.Media; /** * AppRTCAudioManager manages all audio related parts of the AppRTC demo. @@ -44,7 +45,7 @@ public class AppRTCAudioManager { private final Context apprtcContext; // Contains speakerphone setting: auto, true or false @Nullable - private final SpeakerPhonePreference speakerPhonePreference; + private SpeakerPhonePreference speakerPhonePreference; // Handles all tasks related to Bluetooth headset devices. private final AppRTCBluetoothManager bluetoothManager; @Nullable @@ -110,6 +111,16 @@ private AppRTCAudioManager(Context context, final SpeakerPhonePreference speaker AppRTCUtils.logDeviceInfo(Config.LOGTAG); } + public void switchSpeakerPhonePreference(final SpeakerPhonePreference speakerPhonePreference) { + this.speakerPhonePreference = speakerPhonePreference; + if (speakerPhonePreference == SpeakerPhonePreference.EARPIECE && hasEarpiece()) { + defaultAudioDevice = AudioDevice.EARPIECE; + } else { + defaultAudioDevice = AudioDevice.SPEAKER_PHONE; + } + updateAudioDeviceState(); + } + /** * Construction. */ @@ -587,7 +598,15 @@ public enum AudioManagerState { } public enum SpeakerPhonePreference { - AUTO, EARPIECE, SPEAKER + AUTO, EARPIECE, SPEAKER; + + public static SpeakerPhonePreference of(final Set media) { + if (media.contains(Media.VIDEO)) { + return SPEAKER; + } else { + return EARPIECE; + } + } } /** diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/ContentAddition.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/ContentAddition.java new file mode 100644 index 000000000..97bf802fd --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/ContentAddition.java @@ -0,0 +1,88 @@ +package eu.siacs.conversations.xmpp.jingle; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableSet; + +import java.util.Set; + +import eu.siacs.conversations.xmpp.jingle.stanzas.Content; + +public final class ContentAddition { + + public final Direction direction; + public final Set

summary; + + private ContentAddition(Direction direction, Set summary) { + this.direction = direction; + this.summary = summary; + } + + public Set media() { + return ImmutableSet.copyOf(Collections2.transform(summary, s -> s.media)); + } + + public static ContentAddition of(final Direction direction, final RtpContentMap rtpContentMap) { + return new ContentAddition(direction, summary(rtpContentMap)); + } + + public static Set summary(final RtpContentMap rtpContentMap) { + return ImmutableSet.copyOf( + Collections2.transform( + rtpContentMap.contents.entrySet(), + e -> { + final RtpContentMap.DescriptionTransport dt = e.getValue(); + return new Summary(e.getKey(), dt.description.getMedia(), dt.senders); + })); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("direction", direction) + .add("summary", summary) + .toString(); + } + + public enum Direction { + OUTGOING, + INCOMING + } + + public static final class Summary { + public final String name; + public final Media media; + public final Content.Senders senders; + + private Summary(final String name, final Media media, final Content.Senders senders) { + this.name = name; + this.media = media; + this.senders = senders; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Summary summary = (Summary) o; + return Objects.equal(name, summary.name) + && media == summary.media + && senders == summary.senders; + } + + @Override + public int hashCode() { + return Objects.hashCode(name, media, senders); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("name", name) + .add("media", media) + .add("senders", senders) + .toString(); + } + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index ad5aeac0e..6e14fc56e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -5,16 +5,16 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.google.common.base.Joiner; import com.google.common.base.Optional; import com.google.common.base.Preconditions; -import com.google.common.base.Predicates; import com.google.common.base.Stopwatch; import com.google.common.base.Strings; import com.google.common.base.Throwables; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Maps; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import com.google.common.primitives.Ints; import com.google.common.util.concurrent.FutureCallback; @@ -39,6 +39,7 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import eu.siacs.conversations.BuildConfig; import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.CryptoFailedException; @@ -53,6 +54,7 @@ import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.jingle.stanzas.Content; import eu.siacs.conversations.xmpp.jingle.stanzas.Group; import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; @@ -163,6 +165,8 @@ public class JingleRtpConnection extends AbstractJingleConnection private Set proposedMedia; private RtpContentMap initiatorRtpContentMap; private RtpContentMap responderRtpContentMap; + private RtpContentMap incomingContentAdd; + private RtpContentMap outgoingContentAdd; private IceUdpTransportInfo.Setup peerDtlsSetup; private final Stopwatch sessionDuration = Stopwatch.createUnstarted(); private final Queue stateHistory = new LinkedList<>(); @@ -218,6 +222,18 @@ synchronized void deliverPacket(final JinglePacket jinglePacket) { case SESSION_TERMINATE: receiveSessionTerminate(jinglePacket); break; + case CONTENT_ADD: + receiveContentAdd(jinglePacket); + break; + case CONTENT_ACCEPT: + receiveContentAccept(jinglePacket); + break; + case CONTENT_REJECT: + receiveContentReject(jinglePacket); + break; + case CONTENT_REMOVE: + receiveContentRemove(jinglePacket); + break; default: respondOk(jinglePacket); Log.d( @@ -346,6 +362,405 @@ private void receiveTransportInfo( } } + private void receiveContentAdd(final JinglePacket jinglePacket) { + final RtpContentMap modification; + try { + modification = RtpContentMap.of(jinglePacket); + modification.requireContentDescriptions(); + } catch (final RuntimeException e) { + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + ": improperly formatted contents", + Throwables.getRootCause(e)); + respondOk(jinglePacket); + webRTCWrapper.close(); + sendSessionTerminate(Reason.of(e), e.getMessage()); + return; + } + if (isInState(State.SESSION_ACCEPTED)) { + receiveContentAdd(jinglePacket, modification); + } else { + terminateWithOutOfOrder(jinglePacket); + } + } + + private void receiveContentAdd( + final JinglePacket jinglePacket, final RtpContentMap modification) { + final RtpContentMap remote = getRemoteContentMap(); + if (!Collections.disjoint(modification.getNames(), remote.getNames())) { + respondOk(jinglePacket); + this.webRTCWrapper.close(); + sendSessionTerminate( + Reason.FAILED_APPLICATION, + String.format( + "contents with names %s already exists", + Joiner.on(", ").join(modification.getNames()))); + return; + } + final ContentAddition contentAddition = + ContentAddition.of(ContentAddition.Direction.INCOMING, modification); + + final RtpContentMap outgoing = this.outgoingContentAdd; + final Set outgoingContentAddSummary = + outgoing == null ? Collections.emptySet() : ContentAddition.summary(outgoing); + + if (outgoingContentAddSummary.equals(contentAddition.summary)) { + if (isInitiator()) { + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + + ": respond with tie break to matching content-add offer"); + respondWithTieBreak(jinglePacket); + } else { + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + + ": automatically accept matching content-add offer"); + acceptContentAdd(contentAddition.summary, modification); + } + return; + } + + // once we can display multiple video tracks we can be more loose with this condition + // theoretically it should also be fine to automatically accept audio only contents + if (Media.audioOnly(remote.getMedia()) && Media.videoOnly(contentAddition.media())) { + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + ": received " + contentAddition); + this.incomingContentAdd = modification; + respondOk(jinglePacket); + updateEndUserState(); + } else { + respondOk(jinglePacket); + // TODO do we want to add a reason? + rejectContentAdd(modification); + } + } + + private void receiveContentAccept(final JinglePacket jinglePacket) { + final RtpContentMap receivedContentAccept; + try { + receivedContentAccept = RtpContentMap.of(jinglePacket); + receivedContentAccept.requireContentDescriptions(); + } catch (final RuntimeException e) { + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + ": improperly formatted contents", + Throwables.getRootCause(e)); + respondOk(jinglePacket); + webRTCWrapper.close(); + sendSessionTerminate(Reason.of(e), e.getMessage()); + return; + } + + final RtpContentMap outgoingContentAdd = this.outgoingContentAdd; + if (outgoingContentAdd == null) { + Log.d(Config.LOGTAG, "received content-accept when we had no outgoing content add"); + terminateWithOutOfOrder(jinglePacket); + return; + } + final Set ourSummary = ContentAddition.summary(outgoingContentAdd); + if (ourSummary.equals(ContentAddition.summary(receivedContentAccept))) { + this.outgoingContentAdd = null; + respondOk(jinglePacket); + receiveContentAccept(receivedContentAccept); + } else { + Log.d(Config.LOGTAG, "received content-accept did not match our outgoing content-add"); + terminateWithOutOfOrder(jinglePacket); + } + } + + private void receiveContentAccept(final RtpContentMap receivedContentAccept) { + final IceUdpTransportInfo.Setup peerDtlsSetup = getPeerDtlsSetup(); + final RtpContentMap modifiedContentMap = + getRemoteContentMap().addContent(receivedContentAccept, peerDtlsSetup); + + setRemoteContentMap(modifiedContentMap); + + final SessionDescription answer = SessionDescription.of(modifiedContentMap, !isInitiator()); + + final org.webrtc.SessionDescription sdp = + new org.webrtc.SessionDescription( + org.webrtc.SessionDescription.Type.ANSWER, answer.toString()); + + try { + this.webRTCWrapper.setRemoteDescription(sdp).get(); + } catch (final Exception e) { + final Throwable cause = Throwables.getRootCause(e); + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + + ": unable to set remote description after receiving content-accept", + cause); + webRTCWrapper.close(); + sendSessionTerminate(Reason.FAILED_APPLICATION, cause.getMessage()); + return; + } + updateEndUserState(); + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + + ": remote has accepted content-add " + + ContentAddition.summary(receivedContentAccept)); + } + + private void receiveContentReject(final JinglePacket jinglePacket) { + final RtpContentMap receivedContentReject; + try { + receivedContentReject = RtpContentMap.of(jinglePacket); + } catch (final RuntimeException e) { + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + ": improperly formatted contents", + Throwables.getRootCause(e)); + respondOk(jinglePacket); + this.webRTCWrapper.close(); + sendSessionTerminate(Reason.of(e), e.getMessage()); + return; + } + + final RtpContentMap outgoingContentAdd = this.outgoingContentAdd; + if (outgoingContentAdd == null) { + Log.d(Config.LOGTAG, "received content-reject when we had no outgoing content add"); + terminateWithOutOfOrder(jinglePacket); + return; + } + final Set ourSummary = ContentAddition.summary(outgoingContentAdd); + if (ourSummary.equals(ContentAddition.summary(receivedContentReject))) { + this.outgoingContentAdd = null; + respondOk(jinglePacket); + Log.d(Config.LOGTAG,jinglePacket.toString()); + receiveContentReject(ourSummary); + } else { + Log.d(Config.LOGTAG, "received content-reject did not match our outgoing content-add"); + terminateWithOutOfOrder(jinglePacket); + } + } + + private void receiveContentReject(final Set summary) { + try { + this.webRTCWrapper.removeTrack(Media.VIDEO); + final RtpContentMap localContentMap = customRollback(); + modifyLocalContentMap(localContentMap); + } catch (final Exception e) { + final Throwable cause = Throwables.getRootCause(e); + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + + ": unable to rollback local description after receiving content-reject", + cause); + webRTCWrapper.close(); + sendSessionTerminate(Reason.FAILED_APPLICATION, cause.getMessage()); + return; + } + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + + ": remote has rejected our content-add " + + summary); + } + + private void receiveContentRemove(final JinglePacket jinglePacket) { + final RtpContentMap receivedContentRemove; + try { + receivedContentRemove = RtpContentMap.of(jinglePacket); + receivedContentRemove.requireContentDescriptions(); + } catch (final RuntimeException e) { + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + ": improperly formatted contents", + Throwables.getRootCause(e)); + respondOk(jinglePacket); + this.webRTCWrapper.close(); + sendSessionTerminate(Reason.of(e), e.getMessage()); + return; + } + respondOk(jinglePacket); + receiveContentRemove(receivedContentRemove); + } + + private void receiveContentRemove(final RtpContentMap receivedContentRemove) { + final RtpContentMap incomingContentAdd = this.incomingContentAdd; + final Set contentAddSummary = + incomingContentAdd == null + ? Collections.emptySet() + : ContentAddition.summary(incomingContentAdd); + final Set removeSummary = + ContentAddition.summary(receivedContentRemove); + if (contentAddSummary.equals(removeSummary)) { + this.incomingContentAdd = null; + updateEndUserState(); + } else { + webRTCWrapper.close(); + sendSessionTerminate( + Reason.FAILED_APPLICATION, + String.format( + "%s only supports %s as a means to retract a not yet accepted %s", + BuildConfig.APP_NAME, + JinglePacket.Action.CONTENT_REMOVE, + JinglePacket.Action.CONTENT_ACCEPT)); + } + } + + public synchronized void retractContentAdd() { + final RtpContentMap outgoingContentAdd = this.outgoingContentAdd; + if (outgoingContentAdd == null) { + throw new IllegalStateException("Not outgoing content add"); + } + try { + webRTCWrapper.removeTrack(Media.VIDEO); + final RtpContentMap localContentMap = customRollback(); + modifyLocalContentMap(localContentMap); + } catch (final Exception e) { + final Throwable cause = Throwables.getRootCause(e); + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + + ": unable to rollback local description after trying to retract content-add", + cause); + webRTCWrapper.close(); + sendSessionTerminate(Reason.FAILED_APPLICATION, cause.getMessage()); + return; + } + this.outgoingContentAdd = null; + final JinglePacket retract = + outgoingContentAdd + .toStub() + .toJinglePacket(JinglePacket.Action.CONTENT_REMOVE, id.sessionId); + this.send(retract); + Log.d( + Config.LOGTAG, + id.getAccount().getJid() + + ": retract content-add " + + ContentAddition.summary(outgoingContentAdd)); + } + + private RtpContentMap customRollback() throws ExecutionException, InterruptedException { + final SessionDescription sdp = setLocalSessionDescription(); + final RtpContentMap localRtpContentMap = RtpContentMap.of(sdp, isInitiator()); + final SessionDescription answer = generateFakeResponse(localRtpContentMap); + this.webRTCWrapper + .setRemoteDescription( + new org.webrtc.SessionDescription( + org.webrtc.SessionDescription.Type.ANSWER, answer.toString())) + .get(); + return localRtpContentMap; + } + + private SessionDescription generateFakeResponse(final RtpContentMap localContentMap) { + final RtpContentMap currentRemote = getRemoteContentMap(); + final RtpContentMap.Diff diff = currentRemote.diff(localContentMap); + if (diff.isEmpty()) { + throw new IllegalStateException( + "Unexpected rollback condition. No difference between local and remote"); + } + final RtpContentMap patch = localContentMap.toContentModification(diff.added); + if (ImmutableSet.of(Content.Senders.NONE).equals(patch.getSenders())) { + final RtpContentMap nextRemote = + currentRemote.addContent( + patch.modifiedSenders(Content.Senders.NONE), getPeerDtlsSetup()); + return SessionDescription.of(nextRemote, !isInitiator()); + } + throw new IllegalStateException( + "Unexpected rollback condition. Senders were not uniformly none"); + } + + public synchronized void acceptContentAdd(@NonNull final Set contentAddition) { + final RtpContentMap incomingContentAdd = this.incomingContentAdd; + if (incomingContentAdd == null) { + throw new IllegalStateException("No incoming content add"); + } + + if (contentAddition.equals(ContentAddition.summary(incomingContentAdd))) { + this.incomingContentAdd = null; + acceptContentAdd(contentAddition, incomingContentAdd); + } else { + throw new IllegalStateException("Accepted content add does not match pending content-add"); + } + } + + private void acceptContentAdd(@NonNull final Set contentAddition, final RtpContentMap incomingContentAdd) { + final IceUdpTransportInfo.Setup setup = getPeerDtlsSetup(); + final RtpContentMap modifiedContentMap = getRemoteContentMap().addContent(incomingContentAdd, setup); + this.setRemoteContentMap(modifiedContentMap); + + final SessionDescription offer; + try { + offer = SessionDescription.of(modifiedContentMap, !isInitiator()); + } catch (final IllegalArgumentException | NullPointerException e) { + Log.d(Config.LOGTAG, id.getAccount().getJid().asBareJid() + ": unable convert offer from content-add to SDP", e); + webRTCWrapper.close(); + sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage()); + return; + } + this.incomingContentAdd = null; + acceptContentAdd(contentAddition, offer); + } + + private void acceptContentAdd( + final Set contentAddition, final SessionDescription offer) { + final org.webrtc.SessionDescription sdp = + new org.webrtc.SessionDescription( + org.webrtc.SessionDescription.Type.OFFER, offer.toString()); + try { + this.webRTCWrapper.setRemoteDescription(sdp).get(); + + // TODO add tracks for 'media' where contentAddition.senders matches + + // TODO if senders.sending(isInitiator()) + + this.webRTCWrapper.addTrack(Media.VIDEO); + + // TODO add additional transceivers for recv only cases + + final SessionDescription answer = setLocalSessionDescription(); + final RtpContentMap rtpContentMap = RtpContentMap.of(answer, isInitiator()); + + final RtpContentMap contentAcceptMap = + rtpContentMap.toContentModification( + Collections2.transform(contentAddition, ca -> ca.name)); + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + + ": sending content-accept " + + ContentAddition.summary(contentAcceptMap)); + modifyLocalContentMap(rtpContentMap); + sendContentAccept(contentAcceptMap); + } catch (final Exception e) { + Log.d(Config.LOGTAG, "unable to accept content add", Throwables.getRootCause(e)); + webRTCWrapper.close(); + sendSessionTerminate(Reason.FAILED_APPLICATION); + } + } + + private void sendContentAccept(final RtpContentMap contentAcceptMap) { + final JinglePacket jinglePacket = contentAcceptMap.toJinglePacket(JinglePacket.Action.CONTENT_ACCEPT, id.sessionId); + send(jinglePacket); + } + + public synchronized void rejectContentAdd() { + final RtpContentMap incomingContentAdd = this.incomingContentAdd; + if (incomingContentAdd == null) { + throw new IllegalStateException("No incoming content add"); + } + this.incomingContentAdd = null; + updateEndUserState(); + rejectContentAdd(incomingContentAdd); + } + + private void rejectContentAdd(final RtpContentMap contentMap) { + final JinglePacket jinglePacket = + contentMap + .toStub() + .toJinglePacket(JinglePacket.Action.CONTENT_REJECT, id.sessionId); + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + + ": rejecting content " + + ContentAddition.summary(contentMap)); + send(jinglePacket); + } + private boolean checkForIceRestart( final JinglePacket jinglePacket, final RtpContentMap rtpContentMap) { final RtpContentMap existing = getRemoteContentMap(); @@ -1534,6 +1949,10 @@ public RtpEndUserState getEndUserState() { return RtpEndUserState.CONNECTING; } case SESSION_ACCEPTED: + final ContentAddition ca = getPendingContentAddition(); + if (ca != null && ca.direction == ContentAddition.Direction.INCOMING) { + return RtpEndUserState.INCOMING_CONTENT_ADD; + } return getPeerConnectionStateAsEndUserState(); case REJECTED: case REJECTED_RACED: @@ -1591,6 +2010,18 @@ private RtpEndUserState getPeerConnectionStateAsEndUserState() { } } + public ContentAddition getPendingContentAddition() { + final RtpContentMap in = this.incomingContentAdd; + final RtpContentMap out = this.outgoingContentAdd; + if (out != null) { + return ContentAddition.of(ContentAddition.Direction.OUTGOING, out); + } else if (in != null) { + return ContentAddition.of(ContentAddition.Direction.INCOMING, in); + } else { + return null; + } + } + public Set getMedia() { final State current = getState(); if (current == State.NULL) { @@ -1604,14 +2035,16 @@ public Set getMedia() { return Preconditions.checkNotNull( this.proposedMedia, "RTP connection has not been initialized properly"); } + final RtpContentMap localContentMap = getLocalContentMap(); final RtpContentMap initiatorContentMap = initiatorRtpContentMap; - if (initiatorContentMap != null) { + if (localContentMap != null) { + return localContentMap.getMedia(); + } else if (initiatorContentMap != null) { return initiatorContentMap.getMedia(); } else if (isTerminated()) { - return Collections.emptySet(); // we might fail before we ever got a chance to set media + return Collections.emptySet(); //we might fail before we ever got a chance to set media } else { - return Preconditions.checkNotNull( - this.proposedMedia, "RTP connection has not been initialized properly"); + return Preconditions.checkNotNull(this.proposedMedia, "RTP connection has not been initialized properly"); } } @@ -1625,6 +2058,16 @@ public boolean isVerified() { return status != null && status.isVerified(); } + public boolean addMedia(final Media media) { + final Set currentMedia = getMedia(); + if (currentMedia.contains(media)) { + throw new IllegalStateException(String.format("%s has already been proposed", media)); + } + // TODO add state protection - can only add while ACCEPTED or so + Log.d(Config.LOGTAG,"adding media: "+media); + return webRTCWrapper.addTrack(media); + } + public synchronized void acceptCall() { switch (this.state) { case PROPOSED: @@ -1743,17 +2186,9 @@ private void closeTransitionLogFinish(final State state) { finish(); } - private void setupWebRTC( - final Set media, final List iceServers) - throws WebRTCWrapper.InitializationException { + private void setupWebRTC(final Set media, final List iceServers) throws WebRTCWrapper.InitializationException { this.jingleConnectionManager.ensureConnectionIsRegistered(this); - final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference; - if (media.contains(Media.VIDEO)) { - speakerPhonePreference = AppRTCAudioManager.SpeakerPhonePreference.SPEAKER; - } else { - speakerPhonePreference = AppRTCAudioManager.SpeakerPhonePreference.EARPIECE; - } - this.webRTCWrapper.setup(this.xmppConnectionService, speakerPhonePreference); + this.webRTCWrapper.setup(this.xmppConnectionService, AppRTCAudioManager.SpeakerPhonePreference.of(media)); this.webRTCWrapper.initializePeerConnection(media, iceServers); } @@ -1905,21 +2340,23 @@ public void onConnectionChange(final PeerConnection.PeerConnectionState newState webRTCWrapper.execute(this::closeWebRTCSessionAfterFailedConnection); return; } else { - webRTCWrapper.restartIce(); + this.restartIce(); } } updateEndUserState(); } + private void restartIce() { + this.stateHistory.clear(); + this.webRTCWrapper.restartIce(); + } + @Override public void onRenegotiationNeeded() { this.webRTCWrapper.execute(this::renegotiate); } private void renegotiate() { - //TODO needs to be called only for ice restarts; maybe in the call to restartICe() - this.stateHistory.clear(); - this.webRTCWrapper.setIsReadyToReceiveIceCandidates(false); final SessionDescription sessionDescription; try { sessionDescription = setLocalSessionDescription(); @@ -1945,19 +2382,26 @@ private void renegotiate() { if (diff.hasModifications() && iceRestart) { webRTCWrapper.close(); - sendSessionTerminate(Reason.FAILED_APPLICATION, "WebRTC unexpectedly tried to modify content and transport at once"); + sendSessionTerminate( + Reason.FAILED_APPLICATION, + "WebRTC unexpectedly tried to modify content and transport at once"); return; } if (iceRestart) { initiateIceRestart(rtpContentMap); return; + } else if (diff.isEmpty()) { + Log.d( + Config.LOGTAG, + "renegotiation. nothing to do. SignalingState=" + + this.webRTCWrapper.getSignalingState()); } if (diff.added.size() > 0) { - sendContentAdd(rtpContentMap); + modifyLocalContentMap(rtpContentMap); + sendContentAdd(rtpContentMap, diff.added); } - } private void initiateIceRestart(final RtpContentMap rtpContentMap) { @@ -1977,8 +2421,7 @@ private void initiateIceRestart(final RtpContentMap rtpContentMap) { return; } if (response.getType() == IqPacket.TYPE.ERROR) { - final Element error = response.findChild("error"); - if (error != null && error.hasChild("tie-break", Namespace.JINGLE_ERRORS)) { + if (isTieBreak(response)) { Log.d(Config.LOGTAG, "received tie-break as result of ice restart"); return; } @@ -1990,8 +2433,40 @@ private void initiateIceRestart(final RtpContentMap rtpContentMap) { }); } - private void sendContentAdd(final RtpContentMap rtpContentMap) { + private boolean isTieBreak(final IqPacket response) { + final Element error = response.findChild("error"); + return error != null && error.hasChild("tie-break", Namespace.JINGLE_ERRORS); + } + private void sendContentAdd(final RtpContentMap rtpContentMap, final Collection added) { + final RtpContentMap contentAdd = rtpContentMap.toContentModification(added); + this.outgoingContentAdd = contentAdd; + final JinglePacket jinglePacket = + contentAdd.toJinglePacket(JinglePacket.Action.CONTENT_ADD, id.sessionId); + jinglePacket.setTo(id.with); + xmppConnectionService.sendIqPacket( + id.account, + jinglePacket, + (connection, response) -> { + if (response.getType() == IqPacket.TYPE.RESULT) { + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + + ": received ACK to our content-add"); + return; + } + if (response.getType() == IqPacket.TYPE.ERROR) { + if (isTieBreak(response)) { + this.outgoingContentAdd = null; + Log.d(Config.LOGTAG, "received tie-break as result of our content-add"); + return; + } + handleIqErrorResponse(response); + } + if (response.getType() == IqPacket.TYPE.TIMEOUT) { + handleIqTimeoutResponse(response); + } + }); } private void setLocalContentMap(final RtpContentMap rtpContentMap) { @@ -2010,6 +2485,15 @@ private void setRemoteContentMap(final RtpContentMap rtpContentMap) { } } + // this method is to be used for content map modifications that modify media + private void modifyLocalContentMap(final RtpContentMap rtpContentMap) { + final RtpContentMap activeContents = rtpContentMap.activeContents(); + setLocalContentMap(activeContents); + this.webRTCWrapper.switchSpeakerPhonePreference( + AppRTCAudioManager.SpeakerPhonePreference.of(activeContents.getMedia())); + updateEndUserState(); + } + private SessionDescription setLocalSessionDescription() throws ExecutionException, InterruptedException { final org.webrtc.SessionDescription sessionDescription = diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/Media.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/Media.java index da25516ca..6a41c8906 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/Media.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/Media.java @@ -1,11 +1,18 @@ package eu.siacs.conversations.xmpp.jingle; +import com.google.common.collect.ImmutableSet; + import java.util.Locale; +import java.util.Set; + +import javax.annotation.Nonnull; public enum Media { + VIDEO, AUDIO, UNKNOWN; @Override + @Nonnull public String toString() { return super.toString().toLowerCase(Locale.ROOT); } @@ -17,4 +24,12 @@ public static Media of(String value) { return UNKNOWN; } } + + public static boolean audioOnly(Set media) { + return ImmutableSet.of(AUDIO).equals(media); + } + + public static boolean videoOnly(Set media) { + return ImmutableSet.of(VIDEO).equals(media); + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java index 7af1469cf..994c3a233 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -14,6 +14,7 @@ import com.google.common.collect.Sets; import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -92,6 +93,10 @@ public Set getMedia() { })); } + public Set getSenders() { + return ImmutableSet.copyOf(Collections2.transform(contents.values(),dt -> dt.senders)); + } + public List getNames() { return ImmutableList.copyOf(contents.keySet()); } @@ -281,6 +286,14 @@ public RtpContentMap modifiedCredentials( return new RtpContentMap(this.group, contentMapBuilder.build()); } + public RtpContentMap modifiedSenders(final Content.Senders senders) { + return new RtpContentMap( + this.group, + Maps.transformValues( + contents, + dt -> new DescriptionTransport(senders, dt.description, dt.transport))); + } + public RtpContentMap toContentModification(final Collection modifications) { return new RtpContentMap( this.group, @@ -291,6 +304,22 @@ public RtpContentMap toContentModification(final Collection modification dt.senders, dt.description, IceUdpTransportInfo.STUB))); } + public RtpContentMap toStub() { + return new RtpContentMap( + null, + Maps.transformValues( + this.contents, + dt -> + new DescriptionTransport( + dt.senders, + RtpDescription.stub(dt.description.getMedia()), + IceUdpTransportInfo.STUB))); + } + + public RtpContentMap activeContents() { + return new RtpContentMap(group, Maps.filterValues(this.contents, dt -> dt.senders != Content.Senders.NONE)); + } + public Diff diff(final RtpContentMap rtpContentMap) { final Set existingContentIds = this.contents.keySet(); final Set newContentIds = rtpContentMap.contents.keySet(); @@ -307,24 +336,32 @@ public boolean iceRestart(final RtpContentMap rtpContentMap) { } } - public RtpContentMap addContent(final RtpContentMap modification) { + public RtpContentMap addContent( + final RtpContentMap modification, final IceUdpTransportInfo.Setup setup) { final IceUdpTransportInfo.Credentials credentials = getDistinctCredentials(); final DTLS dtls = getDistinctDtls(); final IceUdpTransportInfo iceUdpTransportInfo = - IceUdpTransportInfo.of(credentials, dtls.setup, dtls.hash, dtls.fingerprint); - final Map combined = - new ImmutableMap.Builder() + IceUdpTransportInfo.of(credentials, setup, dtls.hash, dtls.fingerprint); + final Map combined = merge(contents, modification.contents); + /*new ImmutableMap.Builder() .putAll(contents) - .putAll( - Maps.transformValues( - modification.contents, - dt -> - new DescriptionTransport( - dt.senders, - dt.description, - iceUdpTransportInfo))) - .build(); - return new RtpContentMap(modification.group, combined); + .putAll(modification.contents) + .build();*/ + final Map combinedFixedTransport = + Maps.transformValues( + combined, + dt -> + new DescriptionTransport( + dt.senders, dt.description, iceUdpTransportInfo)); + return new RtpContentMap(modification.group, combinedFixedTransport); + } + + private static Map merge( + final Map a, final Map b) { + final Map combined = new HashMap<>(); + combined.putAll(a); + combined.putAll(b); + return ImmutableMap.copyOf(combined); } public static class DescriptionTransport { @@ -410,6 +447,10 @@ public boolean hasModifications() { return !this.added.isEmpty() || !this.removed.isEmpty(); } + public boolean isEmpty() { + return this.added.isEmpty() && this.removed.isEmpty(); + } + @Override @Nonnull public String toString() { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java index 9a431bc01..24ed790dd 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java @@ -5,6 +5,7 @@ public enum RtpEndUserState { CONNECTING, //session-initiate or session-accepted but no webrtc peer connection yet CONNECTED, //session-accepted and webrtc peer connection is connected RECONNECTING, //session-accepted and webrtc peer connection was connected once but is currently disconnected or failed + INCOMING_CONTENT_ADD, //session-accepted with a pending, incoming content-add FINDING_DEVICE, //'propose' has been sent out; no 184 ack yet RINGING, //'propose' has been sent out and it has been 184 acked ACCEPTING_CALL, //'proceed' message has been sent; but no session-initiate has been received diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java index eef7ae0da..f0f98260b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java @@ -298,7 +298,7 @@ public static SessionDescription of(final RtpContentMap contentMap, final boolea mediaAttributes.put("mid", name); mediaAttributes.put(descriptionTransport.senders.asMediaAttribute(isInitiatorContentMap), ""); - if (description.hasChild("rtcp-mux", Namespace.JINGLE_APPS_RTP)) { + if (description.hasChild("rtcp-mux", Namespace.JINGLE_APPS_RTP) || group != null) { mediaAttributes.put("rtcp-mux", ""); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java index e368d3b09..02c1f6fe1 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java @@ -5,6 +5,7 @@ import android.media.ToneGenerator; import android.util.Log; +import java.util.Arrays; import java.util.Set; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -19,6 +20,7 @@ class ToneManager { private final Context context; private ToneState state = null; + private RtpEndUserState endUserState = null; private ScheduledFuture currentTone; private ScheduledFuture currentResetFuture; private boolean appRtcAudioManagerHasControl = false; @@ -51,7 +53,11 @@ private static ToneState of(final boolean isInitiator, final RtpEndUserState sta return ToneState.ENDING_CALL; } } - if (state == RtpEndUserState.CONNECTED || state == RtpEndUserState.RECONNECTING) { + if (Arrays.asList( + RtpEndUserState.CONNECTED, + RtpEndUserState.RECONNECTING, + RtpEndUserState.INCOMING_CONTENT_ADD) + .contains(state)) { if (media.contains(Media.VIDEO)) { return ToneState.NULL; } else { @@ -62,14 +68,19 @@ private static ToneState of(final boolean isInitiator, final RtpEndUserState sta } void transition(final RtpEndUserState state, final Set media) { - transition(of(true, state, media), media); + transition(state, of(true, state, media), media); } void transition(final boolean isInitiator, final RtpEndUserState state, final Set media) { - transition(of(isInitiator, state, media), media); + transition(state, of(isInitiator, state, media), media); } - private synchronized void transition(ToneState state, final Set media) { + private synchronized void transition(final RtpEndUserState endUserState, final ToneState state, final Set media) { + final RtpEndUserState normalizeEndUserState = normalize(endUserState); + if (this.endUserState == normalizeEndUserState) { + return; + } + this.endUserState = normalizeEndUserState; if (this.state == state) { return; } @@ -105,6 +116,18 @@ private synchronized void transition(ToneState state, final Set media) { this.state = state; } + private static RtpEndUserState normalize(final RtpEndUserState endUserState) { + if (Arrays.asList( + RtpEndUserState.CONNECTED, + RtpEndUserState.RECONNECTING, + RtpEndUserState.INCOMING_CONTENT_ADD) + .contains(endUserState)) { + return RtpEndUserState.CONNECTED; + } else { + return endUserState; + } + } + void setAppRtcAudioManagerHasControl(final boolean appRtcAudioManagerHasControl) { this.appRtcAudioManagerHasControl = appRtcAudioManagerHasControl; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/TrackWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/TrackWrapper.java index 4e2952127..31c3577ee 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/TrackWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/TrackWrapper.java @@ -1,15 +1,26 @@ package eu.siacs.conversations.xmpp.jingle; +import android.util.Log; + +import com.google.common.base.CaseFormat; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import org.webrtc.MediaStreamTrack; import org.webrtc.PeerConnection; import org.webrtc.RtpSender; +import org.webrtc.RtpTransceiver; + +import java.util.UUID; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import eu.siacs.conversations.Config; class TrackWrapper { - private final T track; - private final RtpSender rtpSender; + public final T track; + public final RtpSender rtpSender; private TrackWrapper(final T track, final RtpSender rtpSender) { Preconditions.checkNotNull(track); @@ -25,7 +36,41 @@ public static TrackWrapper addTrack( } public static Optional get( - final TrackWrapper trackWrapper) { - return trackWrapper == null ? Optional.absent() : Optional.of(trackWrapper.track); + @Nullable final PeerConnection peerConnection, final TrackWrapper trackWrapper) { + if (trackWrapper == null) { + return Optional.absent(); + } + final RtpTransceiver transceiver = + peerConnection == null ? null : getTransceiver(peerConnection, trackWrapper); + if (transceiver == null) { + Log.w(Config.LOGTAG, "unable to detect transceiver for " + trackWrapper.rtpSender.id()); + return Optional.of(trackWrapper.track); + } + final RtpTransceiver.RtpTransceiverDirection direction = transceiver.getDirection(); + if (direction == RtpTransceiver.RtpTransceiverDirection.SEND_ONLY + || direction == RtpTransceiver.RtpTransceiverDirection.SEND_RECV) { + return Optional.of(trackWrapper.track); + } else { + Log.d(Config.LOGTAG, "withholding track because transceiver is " + direction); + return Optional.absent(); + } + } + + public static RtpTransceiver getTransceiver( + @Nonnull final PeerConnection peerConnection, final TrackWrapper trackWrapper) { + final RtpSender rtpSender = trackWrapper.rtpSender; + for (final RtpTransceiver transceiver : peerConnection.getTransceivers()) { + if (transceiver.getSender().id().equals(rtpSender.id())) { + return transceiver; + } + } + return null; + } + + public static String id(final Class clazz) { + return String.format( + "%s-%s", + CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_HYPHEN, clazz.getSimpleName()), + UUID.randomUUID().toString()); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 53b7de1e0..b5ccf5c41 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -225,7 +225,7 @@ private static void dispose(final PeerConnection peerConnection) { public void setup( final XmppConnectionService service, - final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference) + @Nonnull final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference) throws InitializationException { try { PeerConnectionFactory.initialize( @@ -330,18 +330,35 @@ public synchronized boolean addTrack(final Media media) { throw new IllegalStateException(String.format("Could not add track for %s", media)); } + public synchronized void removeTrack(final Media media) { + if (media == Media.VIDEO) { + removeVideoTrack(requirePeerConnection()); + } + } + private boolean addAudioTrack(final PeerConnection peerConnection) { final AudioSource audioSource = requirePeerConnectionFactory().createAudioSource(new MediaConstraints()); final AudioTrack audioTrack = - requirePeerConnectionFactory().createAudioTrack("my-audio-track", audioSource); + requirePeerConnectionFactory() + .createAudioTrack(TrackWrapper.id(AudioTrack.class), audioSource); this.localAudioTrack = TrackWrapper.addTrack(peerConnection, audioTrack); return true; } private boolean addVideoTrack(final PeerConnection peerConnection) { - Preconditions.checkState( - this.localVideoTrack == null, "A local video track already exists"); + final TrackWrapper existing = this.localVideoTrack; + if (existing != null) { + final RtpTransceiver transceiver = + TrackWrapper.getTransceiver(peerConnection, existing); + if (transceiver == null) { + Log.w(EXTENDED_LOGGING_TAG, "unable to restart video transceiver"); + return false; + } + transceiver.setDirection(RtpTransceiver.RtpTransceiverDirection.SEND_RECV); + this.videoSourceWrapper.startCapture(); + return true; + } final VideoSourceWrapper videoSourceWrapper; try { videoSourceWrapper = initializeVideoSourceWrapper(); @@ -351,11 +368,34 @@ private boolean addVideoTrack(final PeerConnection peerConnection) { } final VideoTrack videoTrack = requirePeerConnectionFactory() - .createVideoTrack("my-video-track", videoSourceWrapper.getVideoSource()); + .createVideoTrack( + TrackWrapper.id(VideoTrack.class), + videoSourceWrapper.getVideoSource()); this.localVideoTrack = TrackWrapper.addTrack(peerConnection, videoTrack); return true; } + private void removeVideoTrack(final PeerConnection peerConnection) { + final TrackWrapper localVideoTrack = this.localVideoTrack; + if (localVideoTrack != null) { + + final RtpTransceiver exactTransceiver = + TrackWrapper.getTransceiver(peerConnection, localVideoTrack); + if (exactTransceiver == null) { + throw new IllegalStateException(); + } + exactTransceiver.setDirection(RtpTransceiver.RtpTransceiverDirection.INACTIVE); + } + final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper; + if (videoSourceWrapper != null) { + try { + videoSourceWrapper.stopCapture(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + private static PeerConnection.RTCConfiguration buildConfiguration( final List iceServers) { final PeerConnection.RTCConfiguration rtcConfig = @@ -375,7 +415,12 @@ void reconfigurePeerConnection(final List iceServers) } void restartIce() { - executorService.execute(() -> requirePeerConnection().restartIce()); + executorService.execute(() -> { + final PeerConnection peerConnection = requirePeerConnection(); + setIsReadyToReceiveIceCandidates(false); + peerConnection.restartIce(); + requirePeerConnection().restartIce();} + ); } public void setIsReadyToReceiveIceCandidates(final boolean ready) { @@ -450,7 +495,8 @@ ListenableFuture switchCamera() { } boolean isMicrophoneEnabled() { - final Optional audioTrack = TrackWrapper.get(this.localAudioTrack); + final Optional audioTrack = + TrackWrapper.get(peerConnection, this.localAudioTrack); if (audioTrack.isPresent()) { try { return audioTrack.get().enabled(); @@ -465,7 +511,8 @@ boolean isMicrophoneEnabled() { } boolean setMicrophoneEnabled(final boolean enabled) { - final Optional audioTrack = TrackWrapper.get(this.localAudioTrack); + final Optional audioTrack = + TrackWrapper.get(peerConnection, this.localAudioTrack); if (audioTrack.isPresent()) { try { audioTrack.get().setEnabled(enabled); @@ -481,7 +528,8 @@ boolean setMicrophoneEnabled(final boolean enabled) { } boolean isVideoEnabled() { - final Optional videoTrack = TrackWrapper.get(this.localVideoTrack); + final Optional videoTrack = + TrackWrapper.get(peerConnection, this.localVideoTrack); if (videoTrack.isPresent()) { return videoTrack.get().enabled(); } @@ -489,7 +537,8 @@ boolean isVideoEnabled() { } void setVideoEnabled(final boolean enabled) { - final Optional videoTrack = TrackWrapper.get(this.localVideoTrack); + final Optional videoTrack = + TrackWrapper.get(peerConnection, this.localVideoTrack); if (videoTrack.isPresent()) { videoTrack.get().setEnabled(enabled); return; @@ -528,7 +577,7 @@ public void onSetFailure(final String message) { MoreExecutors.directExecutor()); } - private static void logDescription(final SessionDescription sessionDescription) { + public static void logDescription(final SessionDescription sessionDescription) { for (final String line : sessionDescription.description.split( eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) { @@ -612,7 +661,7 @@ EglBase.Context getEglBaseContext() { } Optional getLocalVideoTrack() { - return TrackWrapper.get(this.localVideoTrack); + return TrackWrapper.get(peerConnection, this.localVideoTrack); } Optional getRemoteVideoTrack() { @@ -635,6 +684,10 @@ void execute(final Runnable command) { executorService.execute(command); } + public void switchSpeakerPhonePreference(AppRTCAudioManager.SpeakerPhonePreference preference) { + mainHandler.post(() -> appRTCAudioManager.switchSpeakerPhonePreference(preference)); + } + public interface EventCallback { void onIceCandidate(IceCandidate iceCandidate); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java index 650c26bef..a7b62363c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java @@ -22,7 +22,6 @@ public class RtpDescription extends GenericDescription { - private RtpDescription(final String media) { super("description", Namespace.JINGLE_APPS_RTP); this.setAttribute("media", media); @@ -32,6 +31,10 @@ private RtpDescription() { super("description", Namespace.JINGLE_APPS_RTP); } + public static RtpDescription stub(final Media media) { + return new RtpDescription(media.toString()); + } + public Media getMedia() { return Media.of(this.getAttribute("media")); } @@ -57,7 +60,8 @@ public List feedbackNegotiationTrrInts() { public List getHeaderExtensions() { final ImmutableList.Builder builder = new ImmutableList.Builder<>(); for (final Element child : getChildren()) { - if ("rtp-hdrext".equals(child.getName()) && Namespace.JINGLE_RTP_HEADER_EXTENSIONS.equals(child.getNamespace())) { + if ("rtp-hdrext".equals(child.getName()) + && Namespace.JINGLE_RTP_HEADER_EXTENSIONS.equals(child.getNamespace())) { builder.add(RtpHeaderExtension.upgrade(child)); } } @@ -67,7 +71,9 @@ public List getHeaderExtensions() { public List getSources() { final ImmutableList.Builder builder = new ImmutableList.Builder<>(); for (final Element child : this.children) { - if ("source".equals(child.getName()) && Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(child.getNamespace())) { + if ("source".equals(child.getName()) + && Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals( + child.getNamespace())) { builder.add(Source.upgrade(child)); } } @@ -77,7 +83,9 @@ public List getSources() { public List getSourceGroups() { final ImmutableList.Builder builder = new ImmutableList.Builder<>(); for (final Element child : this.children) { - if ("ssrc-group".equals(child.getName()) && Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(child.getNamespace())) { + if ("ssrc-group".equals(child.getName()) + && Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals( + child.getNamespace())) { builder.add(SourceGroup.upgrade(child)); } } @@ -85,8 +93,12 @@ public List getSourceGroups() { } public static RtpDescription upgrade(final Element element) { - Preconditions.checkArgument("description".equals(element.getName()), "Name of provided element is not description"); - Preconditions.checkArgument(Namespace.JINGLE_APPS_RTP.equals(element.getNamespace()), "Element does not match the jingle rtp namespace"); + Preconditions.checkArgument( + "description".equals(element.getName()), + "Name of provided element is not description"); + Preconditions.checkArgument( + Namespace.JINGLE_APPS_RTP.equals(element.getNamespace()), + "Element does not match the jingle rtp namespace"); final RtpDescription description = new RtpDescription(); description.setAttributes(element.getAttributes()); description.setChildren(element.getChildren()); @@ -116,7 +128,8 @@ public String getSubType() { private static FeedbackNegotiation upgrade(final Element element) { Preconditions.checkArgument("rtcp-fb".equals(element.getName())); - Preconditions.checkArgument(Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(element.getNamespace())); + Preconditions.checkArgument( + Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(element.getNamespace())); final FeedbackNegotiation feedback = new FeedbackNegotiation(); feedback.setAttributes(element.getAttributes()); feedback.setChildren(element.getChildren()); @@ -126,13 +139,13 @@ private static FeedbackNegotiation upgrade(final Element element) { public static List fromChildren(final List children) { ImmutableList.Builder builder = new ImmutableList.Builder<>(); for (final Element child : children) { - if ("rtcp-fb".equals(child.getName()) && Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(child.getNamespace())) { + if ("rtcp-fb".equals(child.getName()) + && Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(child.getNamespace())) { builder.add(upgrade(child)); } } return builder.build(); } - } public static class FeedbackNegotiationTrrInt extends Element { @@ -142,7 +155,6 @@ private FeedbackNegotiationTrrInt(int value) { this.setAttribute("value", value); } - private FeedbackNegotiationTrrInt() { super("rtcp-fb-trr-int", Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION); } @@ -150,12 +162,12 @@ private FeedbackNegotiationTrrInt() { public int getValue() { final String value = getAttribute("value"); return Integer.parseInt(value); - } private static FeedbackNegotiationTrrInt upgrade(final Element element) { Preconditions.checkArgument("rtcp-fb-trr-int".equals(element.getName())); - Preconditions.checkArgument(Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(element.getNamespace())); + Preconditions.checkArgument( + Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(element.getNamespace())); final FeedbackNegotiationTrrInt trr = new FeedbackNegotiationTrrInt(); trr.setAttributes(element.getAttributes()); trr.setChildren(element.getChildren()); @@ -163,9 +175,11 @@ private static FeedbackNegotiationTrrInt upgrade(final Element element) { } public static List fromChildren(final List children) { - ImmutableList.Builder builder = new ImmutableList.Builder<>(); + ImmutableList.Builder builder = + new ImmutableList.Builder<>(); for (final Element child : children) { - if ("rtcp-fb-trr-int".equals(child.getName()) && Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(child.getNamespace())) { + if ("rtcp-fb-trr-int".equals(child.getName()) + && Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(child.getNamespace())) { builder.add(upgrade(child)); } } @@ -173,9 +187,8 @@ public static List fromChildren(final List c } } - - //XEP-0294: Jingle RTP Header Extensions Negotiation - //maps to `extmap:$id $uri` + // XEP-0294: Jingle RTP Header Extensions Negotiation + // maps to `extmap:$id $uri` public static class RtpHeaderExtension extends Element { private RtpHeaderExtension() { @@ -198,7 +211,8 @@ public String getUri() { public static RtpHeaderExtension upgrade(final Element element) { Preconditions.checkArgument("rtp-hdrext".equals(element.getName())); - Preconditions.checkArgument(Namespace.JINGLE_RTP_HEADER_EXTENSIONS.equals(element.getNamespace())); + Preconditions.checkArgument( + Namespace.JINGLE_RTP_HEADER_EXTENSIONS.equals(element.getNamespace())); final RtpHeaderExtension extension = new RtpHeaderExtension(); extension.setAttributes(element.getAttributes()); extension.setChildren(element.getChildren()); @@ -217,7 +231,7 @@ public static RtpHeaderExtension ofSdpString(final String sdp) { } } - //maps to `rtpmap:$id $name/$clockrate/$channels` + // maps to `rtpmap:$id $name/$clockrate/$channels` public static class PayloadType extends Element { private PayloadType() { @@ -238,8 +252,14 @@ public String toSdpAttribute() { final int channels = getChannels(); final String name = getPayloadTypeName(); Preconditions.checkArgument(name != null, "Payload-type name must not be empty"); - SessionDescription.checkNoWhitespace(name, "payload-type name must not contain whitespaces"); - return getId() + " " + name + "/" + getClockRate() + (channels == 1 ? "" : "/" + channels); + SessionDescription.checkNoWhitespace( + name, "payload-type name must not contain whitespaces"); + return getId() + + " " + + name + + "/" + + getClockRate() + + (channels == 1 ? "" : "/" + channels); } public int getIntId() { @@ -251,7 +271,6 @@ public String getId() { return this.getAttribute("id"); } - public String getPayloadTypeName() { return this.getAttribute("name"); } @@ -271,7 +290,8 @@ public int getClockRate() { public int getChannels() { final String channels = this.getAttribute("channels"); if (channels == null) { - return 1; // The number of channels; if omitted, it MUST be assumed to contain one channel + return 1; // The number of channels; if omitted, it MUST be assumed to contain one + // channel } try { return Integer.parseInt(channels); @@ -299,7 +319,9 @@ public List feedbackNegotiationTrrInts() { } public static PayloadType of(final Element element) { - Preconditions.checkArgument("payload-type".equals(element.getName()), "element name must be called payload-type"); + Preconditions.checkArgument( + "payload-type".equals(element.getName()), + "element name must be called payload-type"); PayloadType payloadType = new PayloadType(); payloadType.setAttributes(element.getAttributes()); payloadType.setChildren(element.getChildren()); @@ -339,8 +361,8 @@ public void addParameters(List parameters) { } } - //map to `fmtp $id key=value;key=value - //where id is the id of the parent payload-type + // map to `fmtp $id key=value;key=value + // where id is the id of the parent payload-type public static class Parameter extends Element { private Parameter() { @@ -362,7 +384,8 @@ public String getParameterValue() { } public static Parameter of(final Element element) { - Preconditions.checkArgument("parameter".equals(element.getName()), "element name must be called parameter"); + Preconditions.checkArgument( + "parameter".equals(element.getName()), "element name must be called parameter"); Parameter parameter = new Parameter(); parameter.setAttributes(element.getAttributes()); parameter.setChildren(element.getChildren()); @@ -375,12 +398,18 @@ public static String toSdpString(final String id, List parameters) { for (int i = 0; i < parameters.size(); ++i) { final Parameter p = parameters.get(i); final String name = p.getParameterName(); - Preconditions.checkArgument(name != null, String.format("parameter for %s must have a name", id)); - SessionDescription.checkNoWhitespace(name, String.format("parameter names for %s must not contain whitespaces", id)); + Preconditions.checkArgument( + name != null, String.format("parameter for %s must have a name", id)); + SessionDescription.checkNoWhitespace( + name, + String.format("parameter names for %s must not contain whitespaces", id)); final String value = p.getParameterValue(); - Preconditions.checkArgument(value != null, String.format("parameter for %s must have a value", id)); - SessionDescription.checkNoWhitespace(value, String.format("parameter values for %s must not contain whitespaces", id)); + Preconditions.checkArgument( + value != null, String.format("parameter for %s must have a value", id)); + SessionDescription.checkNoWhitespace( + value, + String.format("parameter values for %s must not contain whitespaces", id)); stringBuilder.append(name).append('=').append(value); if (i != parameters.size() - 1) { @@ -393,8 +422,11 @@ public static String toSdpString(final String id, List parameters) { public static String toSdpString(final String id, final Parameter parameter) { final String name = parameter.getParameterName(); final String value = parameter.getParameterValue(); - Preconditions.checkArgument(value != null, String.format("parameter for %s must have a value", id)); - SessionDescription.checkNoWhitespace(value, String.format("parameter values for %s must not contain whitespaces", id)); + Preconditions.checkArgument( + value != null, String.format("parameter for %s must have a value", id)); + SessionDescription.checkNoWhitespace( + value, + String.format("parameter values for %s must not contain whitespaces", id)); if (Strings.isNullOrEmpty(name)) { return String.format("%s %s", id, value); } else { @@ -420,8 +452,8 @@ static Pair> ofSdpString(final String sdp) { } } - //XEP-0339: Source-Specific Media Attributes in Jingle - //maps to `a=ssrc: :` + // XEP-0339: Source-Specific Media Attributes in Jingle + // maps to `a=ssrc: :` public static class Source extends Element { private Source() { @@ -452,7 +484,9 @@ public List getParameters() { public static Source upgrade(final Element element) { Preconditions.checkArgument("source".equals(element.getName())); - Preconditions.checkArgument(Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(element.getNamespace())); + Preconditions.checkArgument( + Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals( + element.getNamespace())); final Source source = new Source(); source.setChildren(element.getChildren()); source.setAttributes(element.getAttributes()); @@ -489,7 +523,6 @@ public static Parameter upgrade(final Element element) { return parameter; } } - } public static class SourceGroup extends Element { @@ -525,7 +558,9 @@ public List getSsrcs() { public static SourceGroup upgrade(final Element element) { Preconditions.checkArgument("ssrc-group".equals(element.getName())); - Preconditions.checkArgument(Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(element.getNamespace())); + Preconditions.checkArgument( + Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals( + element.getNamespace())); final SourceGroup group = new SourceGroup(); group.setChildren(element.getChildren()); group.setAttributes(element.getAttributes()); @@ -533,15 +568,18 @@ public static SourceGroup upgrade(final Element element) { } } - public static RtpDescription of(final SessionDescription sessionDescription, final SessionDescription.Media media) { + public static RtpDescription of( + final SessionDescription sessionDescription, final SessionDescription.Media media) { final RtpDescription rtpDescription = new RtpDescription(media.media); final Map> parameterMap = new HashMap<>(); - final ArrayListMultimap feedbackNegotiationMap = ArrayListMultimap.create(); - final ArrayListMultimap sourceParameterMap = ArrayListMultimap.create(); - final Set attributes = Sets.newHashSet(Iterables.concat( - sessionDescription.attributes.keySet(), - media.attributes.keySet() - )); + final ArrayListMultimap feedbackNegotiationMap = + ArrayListMultimap.create(); + final ArrayListMultimap sourceParameterMap = + ArrayListMultimap.create(); + final Set attributes = + Sets.newHashSet( + Iterables.concat( + sessionDescription.attributes.keySet(), media.attributes.keySet())); for (final String rtcpFb : media.attributes.get("rtcp-fb")) { final String[] parts = rtcpFb.split(" "); if (parts.length >= 2) { @@ -550,7 +588,10 @@ public static RtpDescription of(final SessionDescription sessionDescription, fin final String subType = parts.length >= 3 ? parts[2] : null; if ("trr-int".equals(type)) { if (subType != null) { - feedbackNegotiationMap.put(id, new FeedbackNegotiationTrrInt(SessionDescription.ignorantIntParser(subType))); + feedbackNegotiationMap.put( + id, + new FeedbackNegotiationTrrInt( + SessionDescription.ignorantIntParser(subType))); } } else { feedbackNegotiationMap.put(id, new FeedbackNegotiation(type, subType)); @@ -602,7 +643,8 @@ public static RtpDescription of(final SessionDescription sessionDescription, fin rtpDescription.addChild(new SourceGroup(semantics, builder.build())); } } - for (Map.Entry> source : sourceParameterMap.asMap().entrySet()) { + for (Map.Entry> source : + sourceParameterMap.asMap().entrySet()) { rtpDescription.addChild(new Source(source.getKey(), source.getValue())); } if (media.attributes.containsKey("rtcp-mux")) { From c178e9ad33ae3dd09621d5b11c704e0661951285 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 28 Nov 2022 11:39:26 +0100 Subject: [PATCH 250/394] add switch to video menu item to call --- .../conversations/ui/RtpSessionActivity.java | 139 +++++++++++++++--- .../res/drawable/ic_baseline_check_24.xml | 5 + src/main/res/menu/activity_rtp_session.xml | 3 + src/main/res/values/strings.xml | 4 + 4 files changed, 132 insertions(+), 19 deletions(-) create mode 100644 src/main/res/drawable/ic_baseline_check_24.xml diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index f9c7177a2..b91269c25 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -65,6 +65,7 @@ import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; +import eu.siacs.conversations.xmpp.jingle.ContentAddition; import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.jingle.Media; @@ -101,9 +102,12 @@ public class RtpSessionActivity extends XmppActivity Arrays.asList( RtpEndUserState.CONNECTING, RtpEndUserState.CONNECTED, - RtpEndUserState.RECONNECTING); + RtpEndUserState.RECONNECTING, + RtpEndUserState.INCOMING_CONTENT_ADD); private static final List STATES_CONSIDERED_CONNECTED = - Arrays.asList(RtpEndUserState.CONNECTED, RtpEndUserState.RECONNECTING); + Arrays.asList( + RtpEndUserState.CONNECTED, + RtpEndUserState.RECONNECTING); private static final List STATES_SHOWING_PIP_PLACEHOLDER = Arrays.asList( RtpEndUserState.ACCEPTING_CALL, @@ -111,6 +115,8 @@ public class RtpSessionActivity extends XmppActivity RtpEndUserState.RECONNECTING); private static final String PROXIMITY_WAKE_LOCK_TAG = "conversations:in-rtp-session"; private static final int REQUEST_ACCEPT_CALL = 0x1111; + private static final int REQUEST_ACCEPT_CONTENT = 0x1112; + private static final int REQUEST_ADD_CONTENT = 0x1113; private WeakReference rtpConnectionReference; private ActivityRtpSessionBinding binding; @@ -164,8 +170,10 @@ public boolean onCreateOptionsMenu(final Menu menu) { getMenuInflater().inflate(R.menu.activity_rtp_session, menu); final MenuItem help = menu.findItem(R.id.action_help); final MenuItem gotoChat = menu.findItem(R.id.action_goto_chat); + final MenuItem switchToVideo = menu.findItem(R.id.action_switch_to_video); help.setVisible(Config.HELP != null && isHelpButtonVisible()); gotoChat.setVisible(isSwitchToConversationVisible()); + switchToVideo.setVisible(isSwitchToVideoVisible()); return super.onCreateOptionsMenu(menu); } @@ -203,6 +211,15 @@ private boolean isSwitchToConversationVisible() { && STATES_SHOWING_SWITCH_TO_CHAT.contains(connection.getEndUserState()); } + private boolean isSwitchToVideoVisible() { + final JingleRtpConnection connection = + this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; + if (connection == null) { + return false; + } + return Media.audioOnly(connection.getMedia()) && STATES_CONSIDERED_CONNECTED.contains(connection.getEndUserState()); + } + private void switchToConversation() { final Contact contact = getWith(); final Conversation conversation = @@ -215,10 +232,13 @@ public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { case R.id.action_help: launchHelpInBrowser(); - break; + return true; case R.id.action_goto_chat: switchToConversation(); - break; + return true; + case R.id.action_switch_to_video: + requestPermissionAndSwitchToVideo(); + return true; } return super.onOptionsItemSelected(item); } @@ -272,9 +292,60 @@ private void acceptCall(View view) { requestPermissionsAndAcceptCall(); } + private void acceptContentAdd() { + try { + requireRtpConnection() + .acceptContentAdd(requireRtpConnection().getPendingContentAddition().summary); + } catch (final IllegalStateException e) { + Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show(); + } + } + + private void requestPermissionAndSwitchToVideo() { + final List permissions = permissions(ImmutableSet.of(Media.VIDEO, Media.AUDIO)); + if (PermissionUtils.hasPermission(this, permissions, REQUEST_ADD_CONTENT)) { + switchToVideo(); + } + } + + private void switchToVideo() { + try { + requireRtpConnection().addMedia(Media.VIDEO); + } catch (final IllegalStateException e) { + Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show(); + } + } + + private void acceptContentAdd(final ContentAddition contentAddition) { + if (contentAddition == null || contentAddition.direction != ContentAddition.Direction.INCOMING) { + Log.d(Config.LOGTAG,"ignore press on content-accept button"); + return; + } + requestPermissionAndAcceptContentAdd(contentAddition); + } + + private void requestPermissionAndAcceptContentAdd(final ContentAddition contentAddition) { + final List permissions = permissions(contentAddition.media()); + if (PermissionUtils.hasPermission(this, permissions, REQUEST_ACCEPT_CONTENT)) { + requireRtpConnection().acceptContentAdd(contentAddition.summary); + } + } + + private void rejectContentAdd(final View view) { + requireRtpConnection().rejectContentAdd(); + } + private void requestPermissionsAndAcceptCall() { + final List permissions = permissions(getMedia()); + if (PermissionUtils.hasPermission(this, permissions, REQUEST_ACCEPT_CALL)) { + putScreenInCallMode(); + checkRecorderAndAcceptCall(); + } + } + + private List permissions(final Set media) { final ImmutableList.Builder permissions = ImmutableList.builder(); - if (getMedia().contains(Media.VIDEO)) { + if (media.contains(Media.VIDEO)) { permissions.add(Manifest.permission.CAMERA).add(Manifest.permission.RECORD_AUDIO); } else { permissions.add(Manifest.permission.RECORD_AUDIO); @@ -282,10 +353,7 @@ private void requestPermissionsAndAcceptCall() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { permissions.add(Manifest.permission.BLUETOOTH_CONNECT); } - if (PermissionUtils.hasPermission(this, permissions.build(), REQUEST_ACCEPT_CALL)) { - putScreenInCallMode(); - checkRecorderAndAcceptCall(); - } + return permissions.build(); } private void checkRecorderAndAcceptCall() { @@ -516,6 +584,10 @@ public void onRequestPermissionsResult( if (PermissionUtils.allGranted(permissionResult.grantResults)) { if (requestCode == REQUEST_ACCEPT_CALL) { checkRecorderAndAcceptCall(); + } else if (requestCode == REQUEST_ACCEPT_CONTENT) { + acceptContentAdd(); + } else if (requestCode == REQUEST_ADD_CONTENT) { + switchToVideo(); } } else { @StringRes int res; @@ -598,8 +670,8 @@ public void onUserLeaveHint() { private boolean isConnected() { final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; - return connection != null - && STATES_CONSIDERED_CONNECTED.contains(connection.getEndUserState()); + final RtpEndUserState endUserState = connection == null ? null : connection.getEndUserState(); + return STATES_CONSIDERED_CONNECTED.contains(endUserState) || endUserState == RtpEndUserState.INCOMING_CONTENT_ADD; } private boolean switchToPictureInPicture() { @@ -691,6 +763,7 @@ private boolean initializeActivityWithRunningRtpSession( return true; } final Set media = getMedia(); + final ContentAddition contentAddition = getPendingContentAddition(); if (currentState == RtpEndUserState.INCOMING_CALL) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } @@ -700,9 +773,9 @@ private boolean initializeActivityWithRunningRtpSession( } setWidth(currentState); updateVideoViews(currentState); - updateStateDisplay(currentState, media); + updateStateDisplay(currentState, media, contentAddition); updateVerifiedShield(verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(currentState)); - updateButtonConfiguration(currentState, media); + updateButtonConfiguration(currentState, media, contentAddition); updateIncomingCallScreen(currentState); invalidateOptionsMenu(); return false; @@ -753,10 +826,10 @@ private void ensureSurfaceViewRendererIsSetup(final SurfaceViewRenderer surfaceV } private void updateStateDisplay(final RtpEndUserState state) { - updateStateDisplay(state, Collections.emptySet()); + updateStateDisplay(state, Collections.emptySet(), null); } - private void updateStateDisplay(final RtpEndUserState state, final Set media) { + private void updateStateDisplay(final RtpEndUserState state, final Set media, final ContentAddition contentAddition) { switch (state) { case INCOMING_CALL: Preconditions.checkArgument(media.size() > 0, "Media must not be empty"); @@ -766,6 +839,13 @@ private void updateStateDisplay(final RtpEndUserState state, final Set me setTitle(R.string.rtp_state_incoming_call); } break; + case INCOMING_CONTENT_ADD: + if (contentAddition != null && contentAddition.media().contains(Media.VIDEO)) { + setTitle(R.string.rtp_state_content_add_video); + } else { + setTitle(R.string.rtp_state_content_add); + } + break; case CONNECTING: setTitle(R.string.rtp_state_connecting); break; @@ -857,12 +937,16 @@ private Set getMedia() { return requireRtpConnection().getMedia(); } + public ContentAddition getPendingContentAddition() { + return requireRtpConnection().getPendingContentAddition(); + } + private void updateButtonConfiguration(final RtpEndUserState state) { - updateButtonConfiguration(state, Collections.emptySet()); + updateButtonConfiguration(state, Collections.emptySet(), null); } @SuppressLint("RestrictedApi") - private void updateButtonConfiguration(final RtpEndUserState state, final Set media) { + private void updateButtonConfiguration(final RtpEndUserState state, final Set media, final ContentAddition contentAddition) { if (state == RtpEndUserState.ENDING_CALL || isPictureInPicture()) { this.binding.rejectCall.setVisibility(View.INVISIBLE); this.binding.endCall.setVisibility(View.INVISIBLE); @@ -877,6 +961,16 @@ private void updateButtonConfiguration(final RtpEndUserState state, final Set acceptContentAdd(contentAddition))); + this.binding.acceptCall.setImageResource(R.drawable.ic_baseline_check_24); + this.binding.acceptCall.setVisibility(View.VISIBLE); } else if (state == RtpEndUserState.DECLINED_OR_BUSY) { this.binding.rejectCall.setContentDescription(getString(R.string.exit)); this.binding.rejectCall.setOnClickListener(this::exit); @@ -1051,6 +1145,12 @@ private void enableVideo(View view) { } private void disableVideo(View view) { + final JingleRtpConnection rtpConnection = requireRtpConnection(); + final ContentAddition pending = rtpConnection.getPendingContentAddition(); + if (pending != null && pending.direction == ContentAddition.Direction.OUTGOING) { + rtpConnection.retractContentAdd(); + return; + } requireRtpConnection().setVideoEnabled(false); updateInCallButtonConfigurationVideo(false, requireRtpConnection().isCameraSwitchable()); } @@ -1279,6 +1379,7 @@ public void onJingleRtpConnectionUpdate( final AbstractJingleConnection.Id id = requireRtpConnection().getId(); final boolean verified = requireRtpConnection().isVerified(); final Set media = getMedia(); + final ContentAddition contentAddition = getPendingContentAddition(); final Contact contact = getWith(); if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) { if (state == RtpEndUserState.ENDED) { @@ -1287,10 +1388,10 @@ public void onJingleRtpConnectionUpdate( } runOnUiThread( () -> { - updateStateDisplay(state, media); + updateStateDisplay(state, media, contentAddition); updateVerifiedShield( verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(state)); - updateButtonConfiguration(state, media); + updateButtonConfiguration(state, media, contentAddition); updateVideoViews(state); updateIncomingCallScreen(state, contact); invalidateOptionsMenu(); diff --git a/src/main/res/drawable/ic_baseline_check_24.xml b/src/main/res/drawable/ic_baseline_check_24.xml new file mode 100644 index 000000000..2501e9fd9 --- /dev/null +++ b/src/main/res/drawable/ic_baseline_check_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/main/res/menu/activity_rtp_session.xml b/src/main/res/menu/activity_rtp_session.xml index 04756490a..8768c2906 100644 --- a/src/main/res/menu/activity_rtp_session.xml +++ b/src/main/res/menu/activity_rtp_session.xml @@ -13,4 +13,7 @@ android:icon="?attr/icon_goto_chat" android:title="@string/switch_to_conversation" app:showAsAction="always" /> + \ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index d14a0c971..3fc93601f 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -909,6 +909,8 @@ Make call Incoming call Incoming video call + Switch to video call? + Add additional tracks? Connecting Connected Reconnecting @@ -995,5 +997,7 @@ Temporary authentication failure Delete avatar Calls are disabled when using Tor + Switch to video + Reject switch to video request From b374feccbd3e13bab51610026d5204e12f8a76af Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 30 Nov 2022 11:55:11 +0100 Subject: [PATCH 251/394] pulled translations from transifex --- .../res/values-zh-rTW/strings.xml | 10 +- src/main/res/values-de/strings.xml | 17 +- src/main/res/values-es/strings.xml | 18 + src/main/res/values-gl/strings.xml | 15 + src/main/res/values-pl/strings.xml | 21 ++ src/main/res/values-pt-rBR/strings.xml | 18 + src/main/res/values-ro-rRO/strings.xml | 18 + src/main/res/values-tr-rTR/strings.xml | 20 ++ src/main/res/values-zh-rCN/strings.xml | 12 + src/main/res/values-zh-rTW/strings.xml | 328 +++++++++++++++++- src/quicksy/res/values-zh-rTW/strings.xml | 4 + 11 files changed, 474 insertions(+), 7 deletions(-) diff --git a/src/conversations/res/values-zh-rTW/strings.xml b/src/conversations/res/values-zh-rTW/strings.xml index a5ba73d42..8f1828bf6 100644 --- a/src/conversations/res/values-zh-rTW/strings.xml +++ b/src/conversations/res/values-zh-rTW/strings.xml @@ -3,6 +3,14 @@ 挑選您的 XMPP 提供者 使用 conversations.im 建立新帳戶 + 您已經擁有一個 XMPP 賬戶嗎?如果您之前使用過其他 XMPP 客戶端或 Conversations 的話,那麼您已經擁有 XMPP 賬戶了。如果沒有賬戶的話,您可以現在建立一個。\n提示:有些電子郵件服務供應商也會提供 XMPP 賬戶。 + XMPP 是提供者無關的即時訊息網絡。 任何你選擇的 XMPP 伺服器都可在此客戶端上使用。\n不過,我們令它在 Coversations.im 中建立帳戶變得更方便; Conversations.im 是特別適合 Conversations 的提供者 + 你已受邀參加 %1$s 。 我們將指導你完成建立帳戶的過程。選擇 %1$s 作爲提供者後,你可以將你完整的 XMPP 地址交給使用其他提供者的用戶,你便能與他們交流。 + 您已被邀請參加 %1$s 。 我們已經爲你選擇了一個用戶名。 我們將指導你完成建立帳戶的過程。\n將你完整的 XMPP 地址交給使用其他提供者的用戶後,你便能與他們交流。 您的伺服器邀請 - 分享邀請至… + 配置代碼格式不正確 + 輕觸分享按鍵向您的聯絡人發送加入 %1$s 的邀請。 + 如果你的聯絡人就在附近,他們也可以掃描下面的代碼來接受你的邀請。 + 加入 %1$s 和我聊天:%2$s + 分享邀請到... \ No newline at end of file diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 7fbc7ec10..c97286d86 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -170,6 +170,7 @@ Domain nicht überprüfbar Verstoß gegen die Richtlinien Inkompatibler Server + Inkompatibler Client Stream-Fehler Fehler beim Öffnen des Streams Unverschlüsselt @@ -363,7 +364,7 @@ Geräte entfernen Bist du sicher, dass du alle anderen Geräte aus der OMEMO-Bekanntmachung entfernen willst? Die Bekanntmachung wird bei der nächsten Verbindung erneuert aber möglicherweise werden keine zwischenzeitlich gesendeten Nachrichten empfangen. Für diesen Kontakt sind keine nutzbaren Schlüssel verfügbar.\nEs konnten keine neuen Schlüssel vom Server abgerufen werden. Gibt es vielleicht ein Problem mit dem Server deines Kontaktes? - Für diesen Kontakt sind keine benutzbaren Schlüssel verfügbar.\nStelle sicher, dass ihre beide gegenseitig den Online-Status aktiviert habt. + Für diesen Kontakt sind keine benutzbaren Schlüssel verfügbar.\nStelle sicher, dass ihr beide gegenseitig den Online-Status aktiviert habt. Etwas ist schief gelaufen Lade Chatverlauf vom Server Keine weiteren Nachrichten vorhanden @@ -767,6 +768,7 @@ Nachrichten Eingehende Anrufe Laufende Anrufe + Entgangene Anrufe Lautlose Nachrichten Diese Benachrichtigungsart wird verwendet, um Benachrichtigungen anzuzeigen, die keinen Ton auslösen sollen. Zum Beispiel, wenn du auf einem anderen Gerät aktiv bist (Schonfrist). Fehlgeschlagene Zustellungen @@ -934,6 +936,18 @@ Ausgehender Anruf Ausgehender Anruf · %s Entgangener Anruf + + %1$d entgangener Anruf von %2$s + %1$d entgangene Anrufe von %2$s + + + %d entgangener Anruf + %d entgangene Anrufe + + + %1$d entgangener Anruf von %2$d Kontakt + %1$d entgangene Anrufe von %2$d Kontakten + Audioanruf Videoanruf Hilfe @@ -980,4 +994,5 @@ Keine XMPP-Adresse gefunden Temporärer Authentifizierungsfehler Profilbild löschen + Anrufe sind bei der Verwendung von Tor deaktiviert diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index 04a5cc170..0b59a88d1 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -173,6 +173,7 @@ Dominio no verificable Policy violation Servidor incompatible + Cliente incompatible Error de flujo Error al abrir la secuencia Sin cifrado @@ -778,6 +779,7 @@ Mensajes Llamadas entrantes Llamadas salientes + Llamadas perdidas Mensajes sin sonido Este grupo de notificaciones se usa para mostrar notificaciones que no deberían emitir ningún sonido. Por ejemplo, cuando estás activo en otro dispositivo (periodo de gracia). Envíos fallidos @@ -945,6 +947,21 @@ Llamada saliente Video llamada saliente · %s Llamada perdida + + %1$d llamada perdida de %2$s + %1$d llamadas perdidas de %2$s + %1$d llamadas perdidas de %2$s + + + %d llamada perdida + %d llamadas perdidas + %d llamadas perdidas + + + %1$d llamadas perdidas de %2$d contacto + %1$d llamadas perdidas de %2$d contacto + %1$d llamadas perdidas de %2$d contactos + Audio llamada Video llamada Ayuda @@ -993,4 +1010,5 @@ Dirección XMPP no encontrada Fallo temporal de autenticación Eliminar imagen de perfil + Las llamadas están deshabilitadas cuando se usa Tor diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 8c8c00f60..24b3d3ffb 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -170,6 +170,7 @@ Dominio non verificable Violación da política Servidor incompatible + Cliente non compatible Erro de fluxo Fallo ao abrir o fluxo Non cifrado @@ -767,6 +768,7 @@ Mensaxes Chamadas recibidas Chamadas realizadas + Chamadas perdidas Mensaxes acalados Este grupo de notificacións é utilizado para mostrar notificacións que non debería activar ningún son. Por exemplo, cando está activo en outro dispositivo (Período de Graza). Entregas fallidas @@ -934,6 +936,18 @@ Chamada realizada Conversa de · %s Chamada perdida + + %1$d chamada perdida de %2$s + %1$d chamadas perdidas de %2$s + + + %d chamada perdida + %d chamadas perdidas + + + %1$d chamadas perdidas de %2$d contacto + %1$d chamadas perdidas de %2$d contactos + Chamada de audio Chamada de vídeo Axuda @@ -980,4 +994,5 @@ Non se atopa un enderezo XMPP Fallo temporal da autenticación Eliminar avatar + As chamadas están desactivadas cando usas Tor diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 75c46057a..2952e156f 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -176,6 +176,7 @@ Nie można zweryfikować tej domeny Naruszenie zasad Serwer niekompatybilny + Niekompatybilny klient Błąd strumienia Błąd otwierania strumienia Bez szyfrowania @@ -790,6 +791,7 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Wiadomości Połączenia przychodzące Połączenia wychodzące + Nieodebrane rozmowy Ciche wiadomości Ta kategoria powiadomień jest używana aby wyświetlać powiadomienia które nie powodują żadnych dźwięków. Na przykład w ciągu aktywności na innym urządzeniu (okres karencji). Nie dostarczone wiadomości @@ -957,6 +959,24 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Połączenie wychodzące Połączenie wychodzące · %s Nieodebrane połączenie + + %1$d nieodebrana rozmowa od %2$s + %1$d nieodebrane rozmowy od %2$s + %1$d nieodebranych rozmów od %2$s + %1$d nieodebranych rozmów od %2$s + + + %d nieodebrana rozmowa + %d nieodebrane rozmowy + %d nieodebranych rozmów + %d nieodebranych rozmów + + + %1$d nieodebrana rozmowa od %2$d kontaktu + %1$d nieodebrane rozmowy od %2$d kontaktu + %1$d nieodebranych rozmów od %2$d kontaktów + %1$d nieodebranych rozmów od %2$d kontaktów + Połączenie audio Połączenie wideo Pomoc @@ -1007,4 +1027,5 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Nie znaleziono adresu XMPP Tymczasowy błąd uwierzytelniania Usuń awatar + Dzwonienie jest wyłączone podczas używania Tora diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index 58e17a785..a5c8c8bed 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -173,6 +173,7 @@ Domínio não verificável Violação de política Servidor incompatível + Cliente incompatível Erro de fluxo Erro na abertura do fluxo Descriptografada @@ -778,6 +779,7 @@ Mensagens Chamadas recebidas Chamadas em andamento + Chamadas perdidas Silenciar mensagens Essa categoria de notificação é utilizada para exibir notificações que não deveriam gerar nenhum som. Por exemplo, quando estiver ativo em outro dispositivo (Período de Espera). Entregas não efetuadas @@ -945,6 +947,21 @@ Chamada realizada Chamada realizada · %s Chamada perdida + + %1$d chamada perdida para %2$s + %1$d chamadas perdidas para %2$s + %1$d chamadas perdidas para %2$s + + + %d chamada perdida + %d chamadas perdidas + %d chamadas perdidas + + + %1$d chamadas perdidas de %2$d contato + %1$d chamadas perdidas de %2$d contatos + %1$d chamadas perdidas de %2$d contatos + Chamada de áudio Chamada de vídeo Ajuda @@ -993,4 +1010,5 @@ Não foi encontrado nenhum endereço XMPP Falha temporária na autenticação Excluir avatar + As chamadas estão desabilitadas ao usar Tor diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index 2f35075a5..908d29fdf 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -173,6 +173,7 @@ Domeniul nu se poate verifica Încălcare condiții furnizare serviciu Server incompatibil + Client incompatibil Eroare de date Eroare deschidere flux de date Necriptat @@ -778,6 +779,7 @@ Mesaje Apeluri primite Apeluri în curs + Apeluri pierdute Mesaje silențioase Acest grup de notificări este folosit pentru a arăta notificări care nu emit sunete. De exemplu atunci când sunteți activi pe un alt dispozitiv (Perioada de grație). Trimiteri eșuate @@ -945,6 +947,21 @@ Apel efectuat Apel efectuat · %s Apel pierdut + + %1$d apel pierdut de la %2$s + %1$d apeluri pierdute de la %2$s + %1$d de apeluri pierdute de la %2$s + + + %d apel pierdut + %d apeluri pierdute + %d de apeluri pierdute + + + %1$d apel pierdut de la %2$d contact + %1$d apeluri pierdute de la %2$d contact + %1$d de apeluri pierdute de la %2$d contacte + Apel audio Apel video Ajutor @@ -993,4 +1010,5 @@ Nu a fost găsită o adresă XMPP Eroare temporară de autentificare Șterge avatar + Apelurile sunt dezactivate atunci când utilizați Tor diff --git a/src/main/res/values-tr-rTR/strings.xml b/src/main/res/values-tr-rTR/strings.xml index 683901d9f..62d8966b5 100644 --- a/src/main/res/values-tr-rTR/strings.xml +++ b/src/main/res/values-tr-rTR/strings.xml @@ -170,6 +170,7 @@ Alan adı doğrulanamıyor Politika ihlali Sunucu uyuşmazlığı + Uyumsuz istemci Akış hatası Akış açılım hatası Şifrelenmemiş @@ -294,6 +295,7 @@ Sessiz saatleri etkinleştir Bildirimler sessiz saatler boyunca sessize alınacaktır Diğer + Yer imleriyle senkronize et OMEMO parmak izi panoya kopyalandı Bu grup konuşmasından menedildiniz Bu grup konuşması yalnızca üyeleri içindir @@ -301,6 +303,7 @@ Bu grup konuşmasından atıldınız Grup konuşması kapatıldı Artık bu grup konuşmasında değilsiniz + Teknik sebeplerden dolayı bu grup sohbetinden ayrıldınız %s hesabını kullanarak %sev sahipliğinde HTTP sunucusundaki %s denetleniyor @@ -415,6 +418,7 @@ video görüntü Vektör grafik + Multimedya dosyası PDF belgesi Android uygulaması Kişi @@ -763,6 +767,7 @@ İletiler Gelen aramalar Yapılan aramalar + Cevapsız aramalar Sessiz iletiler Bu bildirim grubu, bildirimlerin herhangi bir ses çıkarmaması gerektiğini belirtmekte kullanılır. Mesela başka bir cihazda aktif olunduğunda (Mühlet) Başarısız gönderiler @@ -930,6 +935,18 @@ Yapılan arama Yapılan arama. %s Cevapsız arama + + %2$s tarafından %1$d cevapsız çağrı + %2$s tarafından %1$d cevapsız çağrı + + + %d cevapsız çağrı + %d cevapsız çağrı + + + %2$d tarafından %1$d cevapsız çağrı + %2$d kişi tarafından %1$d cevapsız çağrı + Sesli arama Görüntülü arama Yardım @@ -974,4 +991,7 @@ Düz metin dosyası Hesap kayıtları desteklenmemektedir. Herhangi bir XMPP adresi bulunamadı + Geçici doğrulama hatası + Avatar\'ı sil + Tor kullanırken çağrılar devre dışı diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index cca20889f..edd1df560 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -167,6 +167,7 @@ 域名不可验证 违反政策 服务器不兼容 + 不兼容的客户端 流错误 流打开错误 未加密 @@ -756,6 +757,7 @@ 消息 来电 正在进行的通话 + 未接来电 无声消息 此通知组用于显示不应触发任何声音的通知。 例如,当在另一个设备上激活时(宽限期)。 发送失败 @@ -923,6 +925,15 @@ 去电 去电 · %s 未接电话 + + %1$d 错过了来自 %2$s 的电话 + + + %d 个未接电话 + + + %1$d 个未接电话,来自 %2$d 位联系人 + 语音通话 视频通话 帮助 @@ -967,4 +978,5 @@ 未找到 XMPP 地址 临时认证失败 删除群聊 + 使用 Tor 时通话被禁用 diff --git a/src/main/res/values-zh-rTW/strings.xml b/src/main/res/values-zh-rTW/strings.xml index 5c21adb4f..45af4864b 100644 --- a/src/main/res/values-zh-rTW/strings.xml +++ b/src/main/res/values-zh-rTW/strings.xml @@ -124,8 +124,11 @@ 防止截圖 在多工畫面隱藏應用程式聯絡人並且封鎖螢幕截圖 UI + OpenKeychain 產生一個錯誤。 + 錯誤加密金鑰 接受 產生了一個錯誤 + 錯誤 你的帳戶 發送線上連絡人列表更新 接收線上連絡人列表更新 @@ -134,6 +137,7 @@ 照相 預先同意訂閱請求 選擇的檔案不是一張圖片 + 無法轉換圖片檔案 找不到檔案 常規的 I/O 錯誤。可能是存儲空間不足? 未知 @@ -149,9 +153,13 @@ 註冊完成 伺服器不支援註冊 無效的註冊權杖 + TLS 協商失敗 + 網域不可驗證 違反政策 伺服器不相容 + 不兼容的客戶端 串流錯誤 + 串流開啟錯誤 未加密 OTR OpenPGP @@ -162,6 +170,7 @@ 發佈 OpenPGP 公開金鑰 移除 OpenPGP 公開金鑰 確定要移除上線狀態中的 OpenPGP 公開金鑰嗎?\n這樣一來,你的聯絡人就無法傳送以 OpenPGP 加密的訊息給你了。 + OpenPGP 公開金鑰已發佈 啟用帳戶 確定? 刪除帳戶將清除您全部的會話記錄 @@ -188,11 +197,14 @@ 無效 缺少公開金鑰通知 剛剛查看過 + 一分鐘前查看過 %d 分鐘前查看過 一小時前查看過 %d 小時前查看過 一天前查看過 %d 天前查看過 + 訊息已加密。請安裝 OpenKeychain 以解密該訊息。 + 發現新的 OpenPGP 加密訊息 OpenPGP 金鑰 ID OMEMO 指紋 v\\OMEMO 指紋 @@ -218,17 +230,28 @@ channel@conference.example.com 儲存為書籤 刪除書籤 + 解散群組聊天 + 解散頻道 + 不能解散群組聊天 + 無法解散頻道 + 編輯群組聊天主題 主旨 正在加入群組聊天… 離開 聯絡人已新增至你的聯絡人清單 新增回 %s 已讀此句 + %s 已讀到這裏 + %1$s 和其他 %2$d 位已經讀到這裏 + 所有人已讀到這裏 發佈 + 輕觸頭像以從相片庫中選擇相片 正在發佈… 伺服器拒絕了您的發佈請求 + 無法轉換你的相片 不能將頭像保存至磁片 (或長按按鈕將返回預設頭像) + 你的伺服器不支援發佈頭像 私聊 至 %s 送私密訊息給 %s @@ -255,13 +278,24 @@ 啟用靜默時間段 在靜默時間段內通知將保持靜音 其他 + 同步處理書籤 + OMEMO 指紋已複製到剪貼簿 + 你已被這群組聊天封鎖 + 這群組聊天只有會員可以加入 + 資源限制 + 你已被踢出群組聊天 + 群組聊天已被關閉 + 你已不在該群組聊天 + 出於技術性原因,你離開了群組聊天 用帳戶 %s + 託管於 %s 正在 HTTP 伺服器中檢查 %s 你沒有連接。請稍後重試 檢查 %s 大小 在 %2$s 上檢查 %1$s 的大小 訊息選項 引用 + 作為引用貼上 拷貝原始URL 再次發送 檔案 URL @@ -275,6 +309,7 @@ 帳戶詳情 確認 再試一遍 + 前臺服務 防止作業系統中斷你的連接 建立備份 備份檔案將被儲存至 %s @@ -295,13 +330,21 @@ 可以下載 %s 取消傳送 無法分享檔案 + 檔案傳輸已取消 檔案已刪除 + 沒有可以打開檔案的應用程式 + 沒有可以打開連結的應用程式 + 沒有可以查看聯絡人的應用程式 + 動態標簽 在連絡人下方顯示唯讀標籤 啟用通知 + 未找到群組聊天伺服器 + 未能建立群組聊天 帳戶頭像 拷貝 OMEMO 指紋到剪貼板 重新生成 OMEMO 金鑰 清除設備 + 出錯了 從伺服器獲取歷史記錄 伺服器上沒有更多歷史記錄 更新中… @@ -310,21 +353,37 @@ 修改密碼 當前密碼 新密碼 + 密碼不能留空 啟用所有帳戶 禁用所有帳戶 選擇一個操作 沒有從屬關係 離線 拋棄 - 成員 + 會員 進階模式 + 授予會員許可權 + 撤銷會員許可權 授予管理員許可權 吊銷管理員許可權 + 授予擁有者許可權 + 撤銷擁有者許可權 + 從群組聊天移除 + 從頻道中移除 不能修改 %s 的從屬關係 - 現在遮罩 + 從群組聊天封鎖 + 從頻道中封鎖 + 你正在嘗試從公用頻道中移除 %s。只有永遠封鎖此用戶方能做到。 + 立即封鎖 不能修改 %s 的角色 - 私密,只有成員可以加入 + 設置私人群組聊天 + 設置公用頻道 + 私密,只有會員可以加入 + 令所有人可以看見 XMPP 地址 + 使頻道受到管理 您尚未參與 + 成功修改群組聊天選項! + 無法修改群組聊天選項 從不 直到新的通知 延遲 @@ -357,6 +416,8 @@ 找不到可以顯示位置的應用程式 位置 Conversation 已關閉 + 離開私人群組聊天 + 離開了公用頻道 不信任系統的憑證機構 所有證書必須人工通過 移除證書 @@ -368,11 +429,15 @@ %d 個證書已被刪除 + 以快速動作代替「發送」按鈕 快速動作 最近使用過的 選擇快速動作 + 搜尋聯絡人 + 搜尋書籤 送私密訊息 + %1$s 離開了群組聊天 用戶名 用戶名 該用戶名無效 @@ -380,17 +445,31 @@ 下載失敗:找不到檔案 下載失敗:無法連接到伺服器 下載失敗:無法寫入檔案 + 下載失敗:無效的檔案 Tor network 不可用 綁定失敗 + 伺服器不負責此網域名稱 損壞 + 在線狀態 + 裝置上鎖時離開 + 裝置上鎖時顯示為離開 + 靜音模式時忙碌 + 靜音模式時顯示為忙碌 靜音模式開啟振動 + 裝置振動時顯示為忙碌 高級連接設置 註冊帳戶時顯示主機名稱和埠 xmpp.example.com + 以證書登入 + 無法解析證書 壓縮設置 服務端壓縮設置 正在獲取壓縮設置。請稍後... + 無法獲取封存設置 + 需要 CAPTCHA 輸入上圖中的文字 + 未受信任的證書鏈 + XMPP 地址與證書不相符 更新證書 獲取 OMEMO 金鑰錯誤! 請用證書驗證 OMEMO 金鑰! @@ -400,6 +479,7 @@ 所有連接使用 Tor 網路傳輸,需要 Orbot 主機名稱 + 伺服器- 或 .orion- 地址 該埠號無效 該主機名稱無效 %2$d 個中的 %1$d 個帳戶已連接 @@ -409,11 +489,18 @@ 載入更多訊息 與 %s 分享的檔案 與 %s 分享的圖片 + 與 %s 分享的圖片 + 與 %s 分享的文字 + 授予 %1$s 存取外部儲存 + 授予 %1$s 存取相機 與連絡人同步 為所有訊息顯示通知 + 只在被提到時通知 關閉通知 暫停通知 + 圖像壓縮 總是 + 只限大圖片 啟用節電模式 禁用 選擇區域過大 @@ -422,10 +509,17 @@ 更正訊息 發送更正後的訊息 你已經禁用了此帳戶 + 安全性錯誤:無效的檔案存取! + 找不到可以分享 URI 的應用程式 分享網址(URI)… - 創建帳戶 + 同意並繼續 + 此指引將爲你在conversations.im¹上建立一個賬戶。\n使用 conversations.im 為你的提供者,再將你完整的 XMPP 地址交給使用其他提供者的用戶後,你便能與他們進行交流。 + 您的 XMPP 完整地址將會是: %s + 建立帳戶 使用我自己的服務端 輸入您的用戶名 + 手動更改在線狀態 + 在編輯你的狀態訊息時設立你的在線狀態 狀態訊息 免費聊天室 線上 @@ -437,6 +531,7 @@ 註冊失敗:請重試 註冊失敗:密碼太弱 選擇成員 + 正在建立群組聊天... 重新邀請 禁用 @@ -445,8 +540,12 @@ 隱私 主題 選擇調色板 + 自動 + 明亮 + 深色 綠色背景 接收到的訊息使用綠色背景 + 無法連接到 OpenKeychain 此設備不再使用 電腦 行動電話 @@ -454,19 +553,27 @@ 流覽器 控制台 需要付款 + 允計互聯網存取權 連絡人請求線上訂閱 允許 沒有訪問 %s 的許可 找不到遠端伺服器 + 遠端伺服器超時 + 無法更新帳戶 + 舉報此 XMPP 地址發送垃圾信息 刪除 OMEMO 身份 刪除選擇的金鑰 你需要連接才能發佈頭像 顯示錯誤訊息 錯誤訊息 省流量模式已啟動 + 該設備不支援對 %1$s 禁用節省流量模式 + 無法建立暫存檔案 已經驗證這個設備了 複製指紋 + 你已驗證了你擁有的所有 OMEMO 密鑰 + 條碼中沒有這個會話的指紋。 驗證過的指紋 使用相機來掃描聯絡人的條碼 取得金鑰中,請稍後 @@ -475,18 +582,38 @@ 分享網頁連結 在驗證前總是信任 不可信任 - 二維條碼不合格 + 二維條碼無效 清理快取資料 清理私人空間 清理儲存檔案的私人空間(檔案還可以從伺服器重新下載) 我使用來源可信任的連結 點了連結以後將會驗證 %1$s 的 OMEMO 金鑰。這個行為只有在該連結的來源可信任,並且只有 %2$s 可以提供該連結的情況下,才是安全無虞的。 + 繼續 驗證 OMEMO 金鑰 停止信任設備 + + %d 秒 + + + %d 分鐘 + + + %d 小時 + + + %d 天 + + + %d 星期 + + + %d 月 + 自動刪除訊息 自動從這個設備刪除比設定的時間區間還舊的訊息。 訊息加密中 訊息的時間因為超過本機保留區間而沒有下載。 + 壓縮影片中 關閉相關的對話了。 已經封鎖聯絡人了。 陌生人訊息通知 @@ -496,9 +623,17 @@ 剛剛上線了 再試解密ㄧ次 通訊對話錯誤 + 已降級的 SASL 機制 + 伺服器要求在網站上註冊 + 開啟網站 + 沒有可以打開網站的應用程式 頭條通知 + 顯示頭條通知 今天 昨天 + 以 DNSSEC 驗證主機名稱 + 證書不包含 XMPP 地址 + 部份 錄製影片 複製到剪貼簿 訊息已複製到剪貼簿 @@ -506,8 +641,12 @@ 私密訊息已停用 受保護的應用程式 接受未知憑證? + 伺服器證書未由已知證書機構簽發 + 接受不相符的伺服器名稱? + 你仍然想連線嗎? 憑證詳細資料: 僅一次 + 二維條碼掃描器需要相機權限 捲動至底部 傳送訊息後向下捲動 編輯狀態訊息 @@ -515,7 +654,9 @@ 停用加密 無法擷取裝置清單 無法擷取加密金鑰 + 提示:某些情況下,將對方加入聯絡人列表,便可以解決此問題。 立即停用 + 草稿: OMEMO 加密 一對一以及私人群組的聊天一定會用 OMEMO 新的對話預設會用 OMEMO 加密 @@ -528,6 +669,8 @@ + 訊息未在此裝置加密 + OMEMO 訊息解密失敗 復原 位置分享已停用 固定位置 @@ -548,71 +691,246 @@ 使用分享位置外掛程式而非內建地圖 複製網站位址 複製 XMPP 位址 + 用於 S3 的 HTTP 檔案分享 直接搜尋 + 在「開始對話」版面上打開鍵盤並將遊標放在搜尋列 + 群組聊天頭像 + 主機不支援群組聊天頭像 + 只有擁有者才能變更群組聊天頭像 + 聯絡人名稱 暱稱 名稱 + 可選擇提供名稱 聊天群組名稱 + 此群組聊天已被解散 無法儲存錄製 + 前臺服務 + 此通知類別用於顯示 %1$s 正在運行永久通知。 狀態資訊 + 連接問題 + 此通知類別用於顯示帳戶連接問題的通知。 訊息 通話 訊息 來電 正在進行的通話 + 未接來電 無聲訊息 + 傳送失敗 訊息通知設定 來電通知設定 + 重要程度,聲音,振動 影片壓縮 檢視媒體 成員 媒體瀏覽器 + 由於違反安全規定,你的檔案已被刪除。 影片質量 低質量意味這更小的檔案 中 (360P) 高 (720P) 已取消 + 你已經在起草一條訊息。 + 沒有此功能 無效的國家碼 選擇國家 電話號碼 驗證電話號碼 + Quicksy 將發送短訊(營運商可能收費)以驗證你的電話號碼。輸入國家地區代碼和手機號碼: + %s 不是有效的電話號碼 請輸入您的電話號碼。 搜尋國家 驗證 %s + %s。]]> + 我們已向你發出另一個包含六位數字代碼的簡訊。 + 請在下面輸入六位數字的 PIN 碼。 重新傳送簡訊 重新傳送簡訊 (%s) 請等候 (%s) 返回 + 已自動從剪貼簿貼上可能的 PIN 碼 + 請輸入六位數字的 PIN 碼。 + 你確定要終止註冊? 正在驗證… 正在要求簡訊… + 你輸入的 PIN 碼不正確。 + 我們向你發出的 PIN 碼已經過期。 未知網路錯誤。 + 伺服器的未知回應。 + 無法與伺服器連接。 + 無法建立安全連線。 + 找不到伺服器 + 處理你的請求時出錯 + 無效的用戶輸入 + 暫時無法連接,請稍候再試。 沒有網路連線。 + 請在 %s 後再次嘗試 + 你的頻率已被限制 + 太多的嘗試 + 你正在使用此應用程式的過時版本。 更新 + 此電話號碼已在其他裝置上登錄 + 請輸入您的名稱,使那些沒有把你加入通訊錄的人也知道你是誰。 你的名稱 輸入你的名稱 + 用編輯按鍵設立你的名稱 拒絕要求 + 安裝 Orbot + 啟動 Orbot + 沒有安裝軟件商店 + 這頻道將會公開你的 XMPP 地址 電子書 + 原始(未壓縮) 開啟為… + Conversations 設定檔圖片 選擇帳戶 還原備份 還原 + 輸入帳戶 %s 的密碼以恢復備份。 + 請勿使用恢復備份功能來嘗試複製安裝(即同時運行)。恢復備份功能應只在遷移裝置或丟失裝置的情況下才使用。 + 無法恢復備份。 + 無法為備份解密。密碼是不正確? 備份與還原 + 輸入 XMPP 地址 建立群組聊天 加入公用頻道 建立私人群組聊天 建立公用頻道 頻道名稱 XMPP 位址 + 請為頻道提供一個名稱 + 請提供 XMPP 地址 + 這是一個 XMPP 地址。請提供名稱。 + 正在建立公用頻道... + 此頻道已經存在 + 你已加入一個已經存在的頻道廿 + 無法儲存頻道設置 + 允許所有人編輯主題 + 允許所有人邀請其他人 + 所有人都可以編輯主題 + 擁有人可以編輯主題 + 管理員可以編輯主題 + 擁有人可以邀請其他人 + 所有人都可以邀請其他人 + 管理員可以看見此 XMPP 地址 + 所有人可以看見 XMPP 地址 + 此公開頻道沒有成員。邀請聯絡人或使用分享按鍵傳播 XMPP 地址。 + 此私人群組聊天沒有成員 + 管理許可權 + 搜尋成員 + 檔案太大 + 附加 + 探索頻道 + 搜尋頻道 + 可能侵犯私隱! + search.jabber.network

的第三方服務。使用此功能會將你的IP地址和搜尋字詞傳輸到該服務。 有關更多資訊,請參閱其私隱政策。]]> + 我已經有一個帳戶 + 添加已有帳戶 + 註冊新帳戶 + 這看似是一個網域地址 + 仍然添加 + 這看似是一個頻道地址 + 分享備份檔案 + Conversations 備份 活動 開啟備份 + 你選擇的並不是 Conversations 的備份檔案 + 此帳戶已設置 + 請輸入此帳戶的密碼 + 無法執行此操作 + 加入公用頻道... + 分享程式沒有存取檔案的權限 + 本機伺服器 + 大多數用戶應該選擇 “jabber.network” 以從整個公開的 XMPP 生態系統中獲得更好的建議。 + 頻道探索方法 + 備份 關於 + 請啟用一個帳戶 + 進行通話 + 來電 + 視像通話來電 + 正在連接 + 已接通 + 正在重新連接 + 正在接通來電 + 終止通話 + 接聽 + 拒接 + 正在探索裝置 + 正在響鈴 忙碌 + 無法連接通話 + 連接失敗 + 通話已撤銷 + 程式錯誤 + 驗証問題 + 掛斷 + 正在進行的通話 + 打出視像通話 + 重新連接通話 + 視像通話重新連接中 + 關閉 Tor 以進行通話 + 來電 + 來電 %s + 未接來電 %s + 撥出通話 + 撥出通話 %s + 未接來電 + + 來自 %2$s 的 %1$d 個未接來電 + + + %d 未接來電 + + + 來自 %2$d 個聯絡人的 %1$d 個未接來電 + + 語音通話 + 視像通話 說明 + 切換到會話 + 你的麥克風未能使用 + 你同時只能有一個通話 + 返回正在進行的通話 + 無法切換鏡頭 釘選 取消釘選 + GPX 追綜 + 無法更正訊息 + 所有會話 + 這會話 + 你的頭像 + %s 的頭像 + 以 OMEMO 加密 + 以 OpenPGP 加密 + 沒有加密 離開 + 錄製語音訊息 播放音訊 + 暫停音訊 + 添加聯絡人, 建立或加入群組聊天, 或探索頻道 + + 查看 %1$d 成員 + + + 有些訊息無法傳送 + + 傳送失敗 更多選項 + 沒有找到應用程式 + 邀請到 Conversations + 無法解析邀請 + 伺服器不支援產生邀請 + 沒有活躍帳戶支持此功能 + 已開始進行備份。完成後你會收到一則通知。 + 無法啓用視訊 + 純文字檔案 + 不支援帳戶註冊 + 未找到 XMPP 地址 + 臨時驗證失敗 + 刪除頭像 + 使用 Tor 時不能進行通話 diff --git a/src/quicksy/res/values-zh-rTW/strings.xml b/src/quicksy/res/values-zh-rTW/strings.xml index d4b37b792..9846922b1 100644 --- a/src/quicksy/res/values-zh-rTW/strings.xml +++ b/src/quicksy/res/values-zh-rTW/strings.xml @@ -1,5 +1,9 @@ + 發現在其它設備上的活動後,Quicksy 保持安靜的時間 + 發送堆疊跟蹤説明以幫助 Quicksy 持續開發 + 讓你的所有聯絡人知道你何時使用 Quicksy + 爲了在螢幕關閉時也能收到通知,你需要將 Quicksy 加入受保護的應用程式列表。 Quicksy 設定檔圖片 Quicksy 在您的國家無法使用。 無法驗證伺服器身分。 From 16f140572f65a48016164cbfd13c66e9fe2d99c1 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 30 Nov 2022 11:55:34 +0100 Subject: [PATCH 252/394] version bump to 2.11.0-beta.2 --- CHANGELOG.md | 1 + build.gradle | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bed8eedb..ca90749ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Implement Extensible SASL Profile, Bind 2.0 and Fast for faster reconnects * Implement Channel Binding +* Add ability to switch from audio call to video call * Add ability to delete own avatar * Add notification for missed calls diff --git a/build.gradle b/build.gradle index f5c5fda51..c5f1ba1d8 100644 --- a/build.gradle +++ b/build.gradle @@ -93,8 +93,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 32 - versionCode 42039 - versionName "2.11.0-beta" + versionCode 42040 + versionName "2.11.0-beta.2" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId From 80d195d35eba1d1b12f9efbb86734c33d234ad61 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 30 Nov 2022 17:32:46 +0100 Subject: [PATCH 253/394] avoid race condition when restarting ICE --- .../xmpp/jingle/WebRTCWrapper.java | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index b5ccf5c41..4bb26be72 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -415,12 +415,20 @@ void reconfigurePeerConnection(final List iceServers) } void restartIce() { - executorService.execute(() -> { - final PeerConnection peerConnection = requirePeerConnection(); - setIsReadyToReceiveIceCandidates(false); - peerConnection.restartIce(); - requirePeerConnection().restartIce();} - ); + executorService.execute( + () -> { + final PeerConnection peerConnection; + try { + peerConnection = requirePeerConnection(); + } catch (final PeerConnectionNotInitialized e) { + Log.w( + EXTENDED_LOGGING_TAG, + "PeerConnection vanished before we could execute restart"); + return; + } + setIsReadyToReceiveIceCandidates(false); + peerConnection.restartIce(); + }); } public void setIsReadyToReceiveIceCandidates(final boolean ready) { From 2c7c44e957edb7df8ab63f628c4233073bfe374e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 1 Dec 2022 20:46:18 +0100 Subject: [PATCH 254/394] null PeerConnection reference before disposing; otherwise getState() might be issued against disposed object --- .../java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 4bb26be72..b929e9509 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -445,8 +445,8 @@ synchronized void close() { final AppRTCAudioManager audioManager = this.appRTCAudioManager; final EglBase eglBase = this.eglBase; if (peerConnection != null) { - dispose(peerConnection); this.peerConnection = null; + dispose(peerConnection); } if (audioManager != null) { toneManager.setAppRtcAudioManagerHasControl(false); @@ -467,6 +467,7 @@ synchronized void close() { this.eglBase = null; } if (peerConnectionFactory != null) { + this.peerConnectionFactory = null; peerConnectionFactory.dispose(); } } From 542afe2cb0258bef64d55ea478fa794bb9adb701 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 2 Dec 2022 19:40:11 +0100 Subject: [PATCH 255/394] pulled translations from transifex --- src/main/res/values-de/strings.xml | 7 ++++++- src/main/res/values-gl/strings.xml | 7 ++++++- src/main/res/values-it/strings.xml | 25 ++++++++++++++++++++++++- src/main/res/values-pl/strings.xml | 7 ++++++- src/main/res/values-ro-rRO/strings.xml | 7 ++++++- src/main/res/values-zh-rCN/strings.xml | 7 ++++++- 6 files changed, 54 insertions(+), 6 deletions(-) diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index c97286d86..8a38abbc4 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -909,6 +909,8 @@ Anrufen Eingehender Anruf Eingehender Videoanruf + Umschalten auf Videoanruf? + Zusätzliche Audiospuren hinzufügen? Verbinden Verbunden Erneut verbinden @@ -995,4 +997,7 @@ Temporärer Authentifizierungsfehler Profilbild löschen Anrufe sind bei der Verwendung von Tor deaktiviert - + Umschalten auf Video + Umschalten auf Video ablehnen + + diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 24b3d3ffb..32dce7a18 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -909,6 +909,8 @@ Facer unha chamada Chamada entrante Videochamada entrante + Cambiar a unha chamada de vídeo? + Engadir pistas adicionais? Conectando Conectado Reconectando @@ -995,4 +997,7 @@ Fallo temporal da autenticación Eliminar avatar As chamadas están desactivadas cando usas Tor - + Cambiar a vídeo + Rexeitar a solicitude para cambiar a vídeo + + diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index df1331f46..ab03fb074 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -173,6 +173,7 @@ Dominio non verificabile Violazione della policy Server non compatibile + Client non compatibile Errore di stream Errore apertura flusso Non cifrato @@ -778,6 +779,7 @@ Messaggi Chiamate in arrivo Chiamate in uscita + Chiamate perse Messaggi silenziosi Questo gruppo di notifiche è usato per mostrare notifiche che non devono riprodurre alcun suono. Ad esempio mentre si è attivi su un altro dispositivo (Periodo di grazia). Recapiti falliti @@ -918,6 +920,8 @@ Chiama Chiamata in arrivo Chiamata video in arrivo + Passare a una videochiamata? + Aggiungere altre tracce? Connessione Connesso Riconnessione @@ -945,6 +949,21 @@ Chiamata in uscita Chiamata in uscita · %s Chiamata persa + + %1$d chiamata persa da %2$s + %1$d chiamate perse da %2$s + %1$d chiamate perse da %2$s + + + %d chiamata persa + %d chiamate perse + %d chiamate perse + + + %1$d chiamate perse da %2$d contatto + %1$d chiamate perse da %2$d contatti + %1$d chiamate perse da %2$d contatti + Chiamata vocale Chiamata video Aiuto @@ -993,4 +1012,8 @@ Nessun indirizzo XMPP trovato Errore di autenticazione temporaneo Elimina avatar - + Le chiamate sono disattivate quando si usa Tor + Passa al video + Rifiuta richiesta di passare al video + + diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 2952e156f..b4e03f700 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -932,6 +932,8 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Zadzwoń Połączenie przychodzące Wideorozmowa przychodząca + Przełączyć na rozmowę wideo? + Włączyć dodatkowe ścieżki? Łączenie Połączony Ponowne łączenie @@ -1028,4 +1030,7 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Tymczasowy błąd uwierzytelniania Usuń awatar Dzwonienie jest wyłączone podczas używania Tora - + Przełącz na wideo + Odrzuć prośbę przełączenia na wideo + + diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index 908d29fdf..ad37ec02e 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -920,6 +920,8 @@ Apelează Apel primit Apel video primit + Comută la apel video? + Adăugați canale suplimentare? Conectare Conectat Reconectare @@ -1011,4 +1013,7 @@ Eroare temporară de autentificare Șterge avatar Apelurile sunt dezactivate atunci când utilizați Tor - + Comută la video + Respinge solicitarea de comutare la video + + diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index edd1df560..960e1ec41 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -898,6 +898,8 @@ 进行通话 来电 视频来电 + 切换到视频通话? + 添加额外轨道? 正在连接 已连接 重新连接 @@ -979,4 +981,7 @@ 临时认证失败 删除群聊 使用 Tor 时通话被禁用 - + 切换到视频 + 拒绝切换到视频的请求 + + From a27f6210dfe9dd0ec2d26451fa9fcdb38f36cbac Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 5 Dec 2022 09:45:56 +0100 Subject: [PATCH 256/394] pulled translations from transifex --- src/conversations/res/values-zh-rCN/strings.xml | 12 ++++++------ src/main/res/values-pt-rBR/strings.xml | 7 ++++++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/conversations/res/values-zh-rCN/strings.xml b/src/conversations/res/values-zh-rCN/strings.xml index 39254955a..dfe82d428 100644 --- a/src/conversations/res/values-zh-rCN/strings.xml +++ b/src/conversations/res/values-zh-rCN/strings.xml @@ -1,12 +1,12 @@ - 选择您的XMPP提供者 - 使用conversations.im + 选择您的 XMPP 提供者 + 使用 conversations.im 创建新账户 - 您已经拥有一个XMPP账户了吗?如果您之前使用过其他的XMPP客户端的话,那么您已经拥有这种账户了。如果没有账户的话,您可以现在创建一个。\n提示:有些电子邮件服务也提供XMPP账户。 - XMPP是独立于提供程序的即时消息网络。 您可以将此客户端与所选的任何XMPP服务器一起使用。\ n不过,为了您的方便,我们很容易在对话中创建帐户。im; 特别适合与“对话”配合使用的提供商。 - 您已受邀参加%1$s。 我们将指导您完成创建帐户的过程。\n选择%1$s作为提供者后,您可以通过提供其他人的完整XMPP地址与其他提供者的用户进行交流。 - 您已受邀参加%1$s。 已经为您选择了一个用户名。 我们将指导您完成创建帐户的过程。\n您可以通过向其他提供商的用户提供完整的XMPP地址来与他们进行交流。 + 您有 XMPP 账户吗?如果您之前使用过其他的 XMPP 客户端,那么您已经拥有这种账户了。如果没有的话,您现在可以创建一个。\n提示:有些电子邮件服务也提供XMPP账户。 + XMPP 是独立于提供者的即时消息网络。您可以将此客户端与任意 XMPP 服务器一同使用。\n不过,您可以很容易地在 conversations.im 上创建账户;它是特别适合与“Conversations”一起使用的提供者。 + 您已受邀加入 %1$s。我们将指导您完成创建帐户的过程。\n使用 %1$s 作为提供者时,您可以通过您的完整 XMPP 地址与其他提供者的用户进行交流。 + 您已受邀加入 %1$s。已为您选择了一个用户名。我们将指导您完成创建帐户的过程。\n您可以使用完整的 XMPP 地址来与其他提供者的用户进行交流。 你的服务器邀请 格式不正确的配置代码 点击分享按钮向您的联系人发送加入 %1$s 的邀请。 diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index a5c8c8bed..67ffdbedf 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -920,6 +920,8 @@ Fazer chamada Recebendo chamada Recebendo chamada de vídeo + Mudar para videochamada? + Adicionar outras trilhas? Conectando Conectado Reconectando @@ -1011,4 +1013,7 @@ Falha temporária na autenticação Excluir avatar As chamadas estão desabilitadas ao usar Tor - + Mudar para vídeo + Recusar requisição de mudança para vídeo + + From bb52962f0d6f7c8dcf36db69231e7a0f640a4a8e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 5 Dec 2022 15:40:07 +0100 Subject: [PATCH 257/394] delay candidates until after session-init/accept --- .../siacs/conversations/xmpp/jingle/JingleRtpConnection.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 6e14fc56e..2581088ca 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -1262,7 +1262,6 @@ private void prepareSessionAccept( final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription, false); this.responderRtpContentMap = respondingRtpContentMap; storePeerDtlsSetup(respondingRtpContentMap.getDtlsSetup().flip()); - webRTCWrapper.setIsReadyToReceiveIceCandidates(true); final ListenableFuture outgoingContentMapFuture = prepareOutgoingContentMap(respondingRtpContentMap); Futures.addCallback( @@ -1271,6 +1270,7 @@ private void prepareSessionAccept( @Override public void onSuccess(final RtpContentMap outgoingContentMap) { sendSessionAccept(outgoingContentMap); + webRTCWrapper.setIsReadyToReceiveIceCandidates(true); } @Override @@ -1713,8 +1713,6 @@ private void prepareSessionInitiate( SessionDescription.parse(webRTCSessionDescription.description); final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription, true); this.initiatorRtpContentMap = rtpContentMap; - //TODO delay ready to receive ice until after session-init - this.webRTCWrapper.setIsReadyToReceiveIceCandidates(true); final ListenableFuture outgoingContentMapFuture = encryptSessionInitiate(rtpContentMap); Futures.addCallback( @@ -1723,6 +1721,7 @@ private void prepareSessionInitiate( @Override public void onSuccess(final RtpContentMap outgoingContentMap) { sendSessionInitiate(outgoingContentMap, targetState); + webRTCWrapper.setIsReadyToReceiveIceCandidates(true); } @Override From 0a133b6c4c07e0402500c5c00ea0a437a9fcb50e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 10 Dec 2022 08:50:52 +0100 Subject: [PATCH 258/394] =?UTF-8?q?temporarily=20use=20Snikket=E2=80=99s?= =?UTF-8?q?=20build=20of=20WebRTC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c5f1ba1d8..936c4b706 100644 --- a/build.gradle +++ b/build.gradle @@ -76,7 +76,8 @@ dependencies { implementation 'com.google.guava:guava:31.1-android' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.49' - freeImplementation 'ch.threema:webrtc-android:100.0.0' + // temporarily use Snikket’s build of WebRTC. The next release will use our own build + freeImplementation 'org.snikket:webrtc-android:107.0.0' playstoreImplementation fileTree(include: ['libwebrtc-m107.aar'], dir: 'libs') } From f8517612524426821357ea71d34a9e04ec654a70 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 10 Dec 2022 08:51:26 +0100 Subject: [PATCH 259/394] version bump to 2.11.0 --- build.gradle | 4 ++-- fastlane/metadata/android/en-US/changelogs/42041.txt | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/42041.txt diff --git a/build.gradle b/build.gradle index 936c4b706..fa4a0aca0 100644 --- a/build.gradle +++ b/build.gradle @@ -94,8 +94,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 32 - versionCode 42040 - versionName "2.11.0-beta.2" + versionCode 42041 + versionName "2.11.0" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId diff --git a/fastlane/metadata/android/en-US/changelogs/42041.txt b/fastlane/metadata/android/en-US/changelogs/42041.txt new file mode 100644 index 000000000..302e9719b --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/42041.txt @@ -0,0 +1,5 @@ +* Implement Extensible SASL Profile, Bind 2.0 and Fast for faster reconnects +* Implement Channel Binding +* Add ability to switch from audio call to video call +* Add ability to delete own avatar +* Add notification for missed calls From 2093aa76ada6f7dc3724973c63f91710b22ee7d3 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 11 Dec 2022 20:13:09 +0100 Subject: [PATCH 260/394] code clean up in ContactChooserTargetService --- .../services/ContactChooserTargetService.java | 50 ++++++++++++------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/ContactChooserTargetService.java b/src/main/java/eu/siacs/conversations/services/ContactChooserTargetService.java index 96097d0b3..c68b502af 100644 --- a/src/main/java/eu/siacs/conversations/services/ContactChooserTargetService.java +++ b/src/main/java/eu/siacs/conversations/services/ContactChooserTargetService.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.services; +import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.ComponentName; import android.content.Context; @@ -12,19 +13,23 @@ import android.os.IBinder; import android.service.chooser.ChooserTarget; import android.service.chooser.ChooserTargetService; +import android.util.Log; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.ui.ConversationsActivity; import eu.siacs.conversations.utils.Compatibility; +@SuppressLint("Deprecated") @TargetApi(Build.VERSION_CODES.M) public class ContactChooserTargetService extends ChooserTargetService implements ServiceConnection { private final Object lock = new Object(); - private final int MAX_TARGETS = 5; + private static final int MAX_TARGETS = 5; private XmppConnectionService mXmppConnectionService; private static boolean textOnly(IntentFilter filter) { @@ -37,10 +42,10 @@ private static boolean textOnly(IntentFilter filter) { } @Override - public List onGetChooserTargets(ComponentName targetActivityName, IntentFilter matchedFilter) { - final ArrayList chooserTargets = new ArrayList<>(); + public List onGetChooserTargets( + final ComponentName targetActivityName, final IntentFilter matchedFilter) { if (!EventReceiver.hasEnabledAccounts(this)) { - return chooserTargets; + return Collections.emptyList(); } final Intent intent = new Intent(this, XmppConnectionService.class); intent.setAction("contact_chooser"); @@ -48,37 +53,48 @@ public List onGetChooserTargets(ComponentName targetActivityName, bindService(intent, this, Context.BIND_AUTO_CREATE); try { waitForService(); - final ArrayList conversations = new ArrayList<>(); if (!mXmppConnectionService.areMessagesInitialized()) { - return chooserTargets; + return Collections.emptyList(); } - - mXmppConnectionService.populateWithOrderedConversations(conversations, textOnly(matchedFilter)); - final ComponentName componentName = new ComponentName(this, ConversationsActivity.class); + final ArrayList conversations = new ArrayList<>(); + mXmppConnectionService.populateWithOrderedConversations( + conversations, textOnly(matchedFilter)); + final ComponentName componentName = + new ComponentName(this, ConversationsActivity.class); final int pixel = AvatarService.getSystemUiAvatarSize(this); - for (Conversation conversation : conversations) { + final ArrayList chooserTargets = new ArrayList<>(); + for (final Conversation conversation : conversations) { if (conversation.sentMessagesCount() == 0) { continue; } final String name = conversation.getName().toString(); - final Icon icon = Icon.createWithBitmap(mXmppConnectionService.getAvatarService().get(conversation, pixel)); + final Icon icon = + Icon.createWithBitmap( + mXmppConnectionService.getAvatarService().get(conversation, pixel)); final float score = 1 - (1.0f / MAX_TARGETS) * chooserTargets.size(); final Bundle extras = new Bundle(); extras.putString(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid()); chooserTargets.add(new ChooserTarget(name, icon, score, componentName, extras)); if (chooserTargets.size() >= MAX_TARGETS) { - break; + return chooserTargets; } } - } catch (InterruptedException e) { + return chooserTargets; + } catch (final InterruptedException e) { + Log.d( + Config.LOGTAG, + "Thread got interrupted before binding to XmppConnectionService", + e); + } finally { + unbindService(this); } - unbindService(this); - return chooserTargets; + return Collections.emptyList(); } @Override - public void onServiceConnected(ComponentName name, IBinder service) { - XmppConnectionService.XmppConnectionBinder binder = (XmppConnectionService.XmppConnectionBinder) service; + public void onServiceConnected(final ComponentName name, final IBinder service) { + XmppConnectionService.XmppConnectionBinder binder = + (XmppConnectionService.XmppConnectionBinder) service; mXmppConnectionService = binder.getService(); synchronized (this.lock) { lock.notifyAll(); From 36da1c3a89218e7dcf0e8f2ad330353906aaecfb Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 12 Dec 2022 09:27:03 +0100 Subject: [PATCH 261/394] Quicksy: remove REQUEST_INSTALL_PACKAGES permission --- src/quicksy/AndroidManifest.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/quicksy/AndroidManifest.xml b/src/quicksy/AndroidManifest.xml index f82377f01..7b03ed1b7 100644 --- a/src/quicksy/AndroidManifest.xml +++ b/src/quicksy/AndroidManifest.xml @@ -2,10 +2,15 @@ + + + Date: Mon, 12 Dec 2022 10:15:10 +0100 Subject: [PATCH 262/394] show switch to video only if other party has caps fixes #4421 --- .../conversations/ui/RtpSessionActivity.java | 2 +- .../xmpp/jingle/JingleRtpConnection.java | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index b91269c25..a052f8b39 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -217,7 +217,7 @@ private boolean isSwitchToVideoVisible() { if (connection == null) { return false; } - return Media.audioOnly(connection.getMedia()) && STATES_CONSIDERED_CONNECTED.contains(connection.getEndUserState()); + return connection.isSwitchToVideoAvailable(); } private void switchToConversation() { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 2581088ca..4c327a31f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -45,10 +45,13 @@ import eu.siacs.conversations.crypto.axolotl.CryptoFailedException; import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.Presence; import eu.siacs.conversations.entities.RtpSessionStatus; +import eu.siacs.conversations.entities.ServiceDiscoveryResult; import eu.siacs.conversations.services.AppRTCAudioManager; import eu.siacs.conversations.utils.IP; import eu.siacs.conversations.xml.Element; @@ -2763,6 +2766,25 @@ public void fireStateUpdate() { id.account, id.with, id.sessionId, endUserState); } + public boolean isSwitchToVideoAvailable() { + final boolean prerequisite = + Media.audioOnly(getMedia()) + && Arrays.asList(RtpEndUserState.CONNECTED, RtpEndUserState.RECONNECTING) + .contains(getEndUserState()); + return prerequisite && remoteHasVideoFeature(); + } + + private boolean remoteHasVideoFeature() { + final Contact contact = id.getContact(); + final Presence presence = + contact.getPresences().get(Strings.nullToEmpty(id.with.getResource())); + final ServiceDiscoveryResult serviceDiscoveryResult = + presence == null ? null : presence.getServiceDiscoveryResult(); + final List features = + serviceDiscoveryResult == null ? null : serviceDiscoveryResult.getFeatures(); + return features != null && features.contains(Namespace.JINGLE_FEATURE_VIDEO); + } + private interface OnIceServersDiscovered { void onIceServersDiscovered(List iceServers); } From c0b4ae84163f9804d3eca95e324433e5e78a10bf Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 12 Dec 2022 11:07:09 +0100 Subject: [PATCH 263/394] pulled translations from transifex --- src/main/res/values-es/strings.xml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index 0b59a88d1..232cca375 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -920,6 +920,8 @@ Hacer una llamada Llamada entrante Videollamada entrante + ¿Cambiar a videollamada? + ¿Añadir pistas adicionales? Conectando Conectado Reconectando @@ -1011,4 +1013,7 @@ Fallo temporal de autenticación Eliminar imagen de perfil Las llamadas están deshabilitadas cuando se usa Tor - + Cambiar a vídeo + Rechazar petición de cambiar a vídeo + + From 499c4ddd0a869ee6b0b2416983281620dd8b536f Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 16 Dec 2022 08:07:41 +0100 Subject: [PATCH 264/394] do not detect sm:2 as sm available the 2.11.0 release removed support for enabling sm:2 unfortunatly sm:2 was still detected as "server supports stream managment" down the line leading to resend loops. fixes #4426 --- .../java/eu/siacs/conversations/xmpp/XmppConnection.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 54305cdb7..407aa7eab 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -758,10 +758,9 @@ private boolean processSuccess(final Element success) account.getJid().asBareJid() + ": jid changed during SASL 2.0. updating database"); } - final boolean nopStreamFeatures; final Element bound = success.findChild("bound", Namespace.BIND2); - final Element resumed = success.findChild("resumed", "urn:xmpp:sm:3"); - final Element failed = success.findChild("failed", "urn:xmpp:sm:3"); + final Element resumed = success.findChild("resumed", Namespace.STREAM_MANAGEMENT); + final Element failed = success.findChild("failed", Namespace.STREAM_MANAGEMENT); final Element tokenWrapper = success.findChild("token", Namespace.FAST); final String token = tokenWrapper == null ? null : tokenWrapper.getAttribute("token"); if (bound != null && resumed != null) { @@ -1403,7 +1402,7 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio quickStartAvailable = false; } else if (version == SaslMechanism.Version.SASL_2) { final Element inline = authElement.findChild("inline", Namespace.SASL_2); - final boolean sm = inline != null && inline.hasChild("sm", "urn:xmpp:sm:3"); + final boolean sm = inline != null && inline.hasChild("sm", Namespace.STREAM_MANAGEMENT); final HashedToken.Mechanism hashTokenRequest; if (usingFast) { hashTokenRequest = null; @@ -2666,7 +2665,7 @@ public boolean invite() { public boolean sm() { return streamId != null || (connection.streamFeatures != null - && connection.streamFeatures.hasChild("sm")); + && connection.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT)); } public boolean csi() { From 12edf12ce95f205a25c11295ac852851fe266664 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 18 Dec 2022 09:42:45 +0100 Subject: [PATCH 265/394] version bump to 2.11.1 + changelog --- CHANGELOG.md | 5 +++++ build.gradle | 10 +++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca90749ba..21b56f3d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +### Version 2.11.1 + +* Fix resend loop on servers that support only sm:2 +* Show 'Switch to video' only if other party supports video + ### Version 2.11.0 * Implement Extensible SASL Profile, Bind 2.0 and Fast for faster reconnects diff --git a/build.gradle b/build.gradle index fa4a0aca0..bb0befe00 100644 --- a/build.gradle +++ b/build.gradle @@ -33,7 +33,7 @@ configurations { dependencies { implementation 'androidx.viewpager:viewpager:1.0.0' - playstoreImplementation('com.google.firebase:firebase-messaging:23.1.0') { + playstoreImplementation('com.google.firebase:firebase-messaging:23.1.1') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' @@ -94,14 +94,14 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 32 - versionCode 42041 - versionName "2.11.0" + versionCode 42042 + versionName "2.11.1" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId def appName = "Conversations" resValue "string", "app_name", appName - buildConfigField "String", "APP_NAME", "\"$appName\""; + buildConfigField "String", "APP_NAME", "\"$appName\"" } splits { @@ -135,7 +135,7 @@ android { def appName = "Quicksy" resValue "string", "app_name", appName - buildConfigField "String", "APP_NAME", "\"$appName\""; + buildConfigField "String", "APP_NAME", "\"$appName\"" } conversations { From cd8548b0c8d8ea87c78f9aa5698a1be898eb35bc Mon Sep 17 00:00:00 2001 From: Licaon_Kter Date: Mon, 19 Dec 2022 13:35:12 +0000 Subject: [PATCH 266/394] Add fastlane changelog (#4428) --- fastlane/metadata/android/en-US/changelogs/42042.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/en-US/changelogs/42042.txt diff --git a/fastlane/metadata/android/en-US/changelogs/42042.txt b/fastlane/metadata/android/en-US/changelogs/42042.txt new file mode 100644 index 000000000..4437be30a --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/42042.txt @@ -0,0 +1,2 @@ +* Fix resend loop on servers that support only sm:2 +* Show 'Switch to video' only if other party supports video From 995cda9ddf22912f9fa739262ea177a4cfb3161c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 19 Dec 2022 14:14:46 +0100 Subject: [PATCH 267/394] remove travis-ci badge. add f-droid button --- README.md | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 5bed4cd31..7864d4273 100644 --- a/README.md +++ b/README.md @@ -3,16 +3,12 @@

Conversations: the very last word in instant messaging

- - build status - -

- -

- - Google Play - + + Get it on Google Play + + + Get it on F-Droid +

![screenshots](https://raw.githubusercontent.com/inputmice/Conversations/master/screenshots.png) From 36efd51a7fd980b43431bb9ff93fc0578e60c83b Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 20 Dec 2022 19:28:41 +0100 Subject: [PATCH 268/394] fix transports/descriptions not upgraded to jingle ft fixes #4429 --- .../conversations/xmpp/jingle/stanzas/Content.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java index 962515293..061cea752 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java @@ -64,7 +64,9 @@ public GenericDescription getDescription() { return null; } final String namespace = description.getNamespace(); - if (Namespace.JINGLE_APPS_RTP.equals(namespace)) { + if (FileTransferDescription.NAMESPACES.contains(namespace)) { + return FileTransferDescription.upgrade(description); + } else if (Namespace.JINGLE_APPS_RTP.equals(namespace)) { return RtpDescription.upgrade(description); } else { return GenericDescription.upgrade(description); @@ -84,7 +86,11 @@ public String getDescriptionNamespace() { public GenericTransportInfo getTransport() { final Element transport = this.findChild("transport"); final String namespace = transport == null ? null : transport.getNamespace(); - if (Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(namespace)) { + if (Namespace.JINGLE_TRANSPORTS_IBB.equals(namespace)) { + return IbbTransportInfo.upgrade(transport); + } else if (Namespace.JINGLE_TRANSPORTS_S5B.equals(namespace)) { + return S5BTransportInfo.upgrade(transport); + } else if (Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(namespace)) { return IceUdpTransportInfo.upgrade(transport); } else if (transport != null) { return GenericTransportInfo.upgrade(transport); @@ -93,6 +99,7 @@ public GenericTransportInfo getTransport() { } } + public void setTransport(GenericTransportInfo transportInfo) { this.addChild(transportInfo); } From d21362288ee0820839667bcd79acd9cbd7941f1f Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 21 Dec 2022 08:34:21 +0100 Subject: [PATCH 269/394] version bump to 2.11.2 + changelog --- CHANGELOG.md | 4 ++++ build.gradle | 4 ++-- fastlane/metadata/android/en-US/changelogs/42043.txt | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/42043.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 21b56f3d8..fd8df902e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### Version 2.11.2 + +* Fixed regression in P2P file transfer + ### Version 2.11.1 * Fix resend loop on servers that support only sm:2 diff --git a/build.gradle b/build.gradle index bb0befe00..4fd88d8ba 100644 --- a/build.gradle +++ b/build.gradle @@ -94,8 +94,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 32 - versionCode 42042 - versionName "2.11.1" + versionCode 42043 + versionName "2.11.2" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId diff --git a/fastlane/metadata/android/en-US/changelogs/42043.txt b/fastlane/metadata/android/en-US/changelogs/42043.txt new file mode 100644 index 000000000..2c0bd4a34 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/42043.txt @@ -0,0 +1 @@ +* Fixed regression in P2P file transfer From 63d61408e66a3bb00b12746932e616a08b1b2b9f Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 24 Dec 2022 10:54:57 +0100 Subject: [PATCH 270/394] removed unused travis detection --- build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/build.gradle b/build.gradle index 4fd88d8ba..ce559b326 100644 --- a/build.gradle +++ b/build.gradle @@ -82,7 +82,6 @@ dependencies { } ext { - travisBuild = System.getenv("TRAVIS") == "true" preDexEnabled = System.getProperty("pre-dex", "true") abiCodes = ['armeabi-v7a': 1, 'x86': 2, 'x86_64': 3, 'arm64-v8a': 4] } From 909aa72b258db92f27f3910d978770726ef6f7d6 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 24 Dec 2022 10:55:16 +0100 Subject: [PATCH 271/394] catch exception in getSignalingState() --- .../eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index b929e9509..6c270fbad 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -662,7 +662,11 @@ PeerConnection.PeerConnectionState getState() { } public PeerConnection.SignalingState getSignalingState() { - return requirePeerConnection().signalingState(); + try { + return requirePeerConnection().signalingState(); + } catch (final IllegalStateException e) { + return PeerConnection.SignalingState.CLOSED; + } } EglBase.Context getEglBaseContext() { From 07598eab7935609c286bf844c6f182468431825d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 26 Dec 2022 10:02:56 +0100 Subject: [PATCH 272/394] remove empty translations --- src/main/res/values-ca-rES/strings.xml | 2 -- src/main/res/values-fa/strings.xml | 2 -- 2 files changed, 4 deletions(-) delete mode 100644 src/main/res/values-ca-rES/strings.xml delete mode 100644 src/main/res/values-fa/strings.xml diff --git a/src/main/res/values-ca-rES/strings.xml b/src/main/res/values-ca-rES/strings.xml deleted file mode 100644 index c757504ac..000000000 --- a/src/main/res/values-ca-rES/strings.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/src/main/res/values-fa/strings.xml b/src/main/res/values-fa/strings.xml deleted file mode 100644 index c757504ac..000000000 --- a/src/main/res/values-fa/strings.xml +++ /dev/null @@ -1,2 +0,0 @@ - - From 0a2d8c48a3cfd37f42175c12042d592bb690d9b8 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 26 Dec 2022 10:21:51 +0100 Subject: [PATCH 273/394] pulled translations from transifex --- src/main/res/values-da-rDK/strings.xml | 22 +++++++++++++++- src/main/res/values-hr/strings.xml | 14 ++++++++++ src/main/res/values-ja/strings.xml | 36 +++++++++++++++++++------- src/quicksy/res/values-hr/strings.xml | 12 +++++++++ 4 files changed, 74 insertions(+), 10 deletions(-) create mode 100644 src/quicksy/res/values-hr/strings.xml diff --git a/src/main/res/values-da-rDK/strings.xml b/src/main/res/values-da-rDK/strings.xml index fa1c87bd1..032876c1a 100644 --- a/src/main/res/values-da-rDK/strings.xml +++ b/src/main/res/values-da-rDK/strings.xml @@ -170,6 +170,7 @@ Domæne kan ikke verificeres Brud på retningslinjer Inkompatibel server + Inkompatibel klient Strømfejl Fejl ved streamåbning Ukrypteret @@ -767,6 +768,7 @@ Beskeder Indkommende opkald Udgående opkald + Mistet opkald Lydløse beskeder Denne notifikationsgruppe bruges til at vise notifikationer, der ikke bør udløse nogen lyd. For eksempel når du er aktiv på en anden enhed (Fredningsperiode). Mislykkede leverancer @@ -907,6 +909,8 @@ Lav opkald Indkommende opkald Indkommende videoopkald + Skift til videoopkald + Tilføje yderligere spor? Forbinder Forbundet Forbinder igen @@ -934,6 +938,18 @@ Udgående opkald Udgående opkald · %s Mistet opkald + + %1$d mistet opkald fra %2$s + %1$d mistet opkald fra %2$s + + + %d mistet opkald + %d mistet opkald + + + %1$d mistet opkald fra %2$d kontakt + %1$d mistet opkald fra %2$d kontakter + Lydopkald Videoopkald Hjælp @@ -980,4 +996,8 @@ Ingen XMPP-adresse fundet Midlertidig godkendelsesfejl Slet avatar - + Opkald er deaktiveret ved brug af Tor + Skift til video + Afvis skift til video anmodning + + diff --git a/src/main/res/values-hr/strings.xml b/src/main/res/values-hr/strings.xml index 440ee1fed..2ee852279 100644 --- a/src/main/res/values-hr/strings.xml +++ b/src/main/res/values-hr/strings.xml @@ -73,4 +73,18 @@ Odblokiraj Sačuvaj Ok + Pošalji sada + Nikad više ne pitaj + Nije moguće povezati se s računom + Nije moguće povezati se s više računa + Dodirnite za upravljanje svojim računima + Priložite datoteku + Dodati ovaj kontakt koji nedostaje na popis kontakata? + Dodaj kontakt + dostava nije uspjela + Priprema za slanje slike + Priprema za slanje slika + Dijeljenje datoteka. Molimo pričekajte… + Obriši povijest + Obriši povijest razgovora diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml index 5f1edfc60..d788c54a5 100644 --- a/src/main/res/values-ja/strings.xml +++ b/src/main/res/values-ja/strings.xml @@ -37,7 +37,7 @@ メッセージを復号しています。しばらくお待ちください… OpenPGP 暗号化メッセージ ニックネームは既に使用されています - 不正なニックネーム + このニックネームは使えません 管理者 所有者 調停者 @@ -167,6 +167,7 @@ 検証不可能なドメイン ポリシー違反 互換性のないサーバー + 互換性のない端末 ストリーム エラー ストリームを開く際にエラー 暗号化されていない @@ -221,7 +222,7 @@ v\\OMEMO フィンガープリント (メッセージ起源) 他のデバイス OMEMO フィンガープリントを信頼 - 鍵の取得中… + 暗号鍵の取得中… 完了 復号 ブックマーク @@ -323,7 +324,7 @@ 確認 再試行 フォアグラウンドサービス - オペレーティングシステムが接続を切断するのを防止します + OSが接続を切断するのを防止します バックアップを作成 バックアップファイルは %s に保存されます バックアップファイルを作成しています @@ -348,7 +349,7 @@ ファイルを開くアプリケーションが見つかりません リンクを開くアプリケーションが見つかりません 連絡先を表示するアプリケーションが見つかりません - ダイナミック タグ + タグ付け 連絡先の下に、読み取り専用タグを表示します 通知を有効化 グループチャットのサーバーが見つかりませんでした @@ -511,7 +512,7 @@ %1$s に外部ストレージへのアクセス権を付与してください %1$s にカメラへのアクセス権を付与 連絡先と同期 - %1$s はあなたのアドレス帳にアクセスして、あなたのXMPP 連絡先名簿と照合する権限を求めています。\nこれにより、連絡先のフルネームとアバターが表示されます。\n\n%1$s は、あなたのサーバーに何かをアップロードすることなく、あなたのアドレス帳を読み込んでローカルに照合するだけです。 + %1$s はあなたのアドレス帳にアクセスして、あなたのXMPP 連絡先名簿と照合する権限を求めています。\nこれにより、連絡先のフルネームとアバターが表示されます。\n\n%1$s は、あなたのサーバーに何かをアップロードすることなく、あなたのアドレス帳を読み込んで照合するだけです。
Quicksyは、それらの電話番号のコピーを保存することはありません。\n\n詳細はプライバシーポリシーをご覧ください。

今、連絡先へのアクセス権限を付与するよう求められます。]]>
すべてのメッセージで通知 メンションされたときにのみ通知 @@ -535,7 +536,7 @@ セキュリティエラー: 不正なファイルアクセス! URI を共有するアプリが見つかりません …で URI を共有 -
電話番号を入力して登録すると、アドレス帳に登録されている電話番号をもとに、Quicksyが自動的に連絡先を提案します。

登録すると、我々のプライバシーポリシーに同意することになります。]]>
+
電話番号を入力して登録すると、アドレス帳に登録されている電話番号をもとに、Quicksyが自動的に連絡先を提案します。

登録すると、我々のプライバシーポリシーに同意することになります。]]>
同意して続行 conversations.im 上にアカウントを作成する設定の指南です。¹\nconversations.im をプロバイダーとして選択した場合、あなたの完全な XMPP アドレスを他のプロバイダーのユーザーに示すことで、その人と連絡をとることができます。 あなたの完全なXMPPアドレスは: %s @@ -608,10 +609,10 @@ バーコードで共有 XMPP URI で共有 HTTP リンクで共有 - 検証前の盲目的な信頼 + 認証前で鍵を使用 認証されていない連絡先からの新規デバイスを信頼するが、認証されている連絡先からの新規デバイスについては手動での確認を求める。 - OMEMO 鍵を盲目的に信用していた。つまり、他の人かもしれないし、誰かが盗聴しているかもしれない。 - 信頼できない + 認証せずOMEMO 鍵を信用しています。このままでは盗聴される危険性があります。 + 信頼されていない 不正な二次元バーコード キャッシュフォルダを消去します (カメラアプリで使用) キャッシュを消去 @@ -754,6 +755,7 @@ メッセージ 着信通話 継続中の通話 + 不在着信 サイレントメッセージ この通知グループは、音を鳴らしてはいけない通知を表示するために使用します。例えば、他のデバイスでアクティブになっているときなどです (猶予期間)。 配信に失敗 @@ -893,8 +895,10 @@ 通話をする 着信通話 着信映像通話 + ビデオ通話に切り替えますか? 接続中 接続しました + 再接続中 通話受入 通話終了 応答 @@ -910,6 +914,8 @@ 電話を切る 継続中の通話 継続中の映像通話 + 通話再接続中 + ビデオ通話再接続中 通話するのに Tor を無効化 着信通話 着信通話・%s @@ -917,8 +923,18 @@ 発信通話 発信通話・%s 不在着信通話 + + %2$sから%1$d件の不在着信 + + + 不在着信%d件 + + + %2$d人から%1$d件の不在着信 + 音声通話 映像通話 + ヘルプ 会話に切り替え マイクが利用できません 1度に1回線の通話のみ。 @@ -960,4 +976,6 @@ XMPPアドレスがみつかりません 一時的な認証失敗 アバターを削除 + Tor使用中のため通話できません + ビデオ通話切替 diff --git a/src/quicksy/res/values-hr/strings.xml b/src/quicksy/res/values-hr/strings.xml new file mode 100644 index 000000000..80b63a28f --- /dev/null +++ b/src/quicksy/res/values-hr/strings.xml @@ -0,0 +1,12 @@ + + + Duljina vremena u kojem Quicksy šuti nakon što vidi aktivnost na drugom uređaju + Slanjem tragova hrpe pomažete tekući razvoj Quicksyja + Obavijestite sve svoje kontakte kada koristite Quicksy + Kako biste nastavili primati obavijesti, čak i kada je ekran isključen, trebate dodati Quicksy na popis zaštićenih aplikacija. + Quicksy profilna slika + Quicksy nije dostupan u vašoj zemlji. + Nije moguće potvrditi identitet poslužitelja. + Nepoznata sigurnosna pogreška. + Istek vremena tijekom povezivanja s poslužiteljem. + From 90ad0a29abfe669568fcbf6aeb9455f6b7a0faf6 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 26 Dec 2022 10:34:44 +0100 Subject: [PATCH 274/394] link to weblate instead of Transifex --- README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7864d4273..5d38d8d70 100644 --- a/README.md +++ b/README.md @@ -258,11 +258,9 @@ Conversations is trying to get rid of old behaviours and set an example for other clients. #### Translations -Translations are managed on [Transifex](https://www.transifex.com/projects/p/conversations/). -If you want to become a translator Please register on transifex, apply to join -the translation team and then step by our group chat on -[conversations@conference.siacs.eu](https://conversations.im/j/conversations@conference.siacs.eu) -and introduce yourself to `iNPUTmice` so he can approve your join request. +Translations are managed on [Weblate](https://translate.codeberg.org/projects/converastions/). + +You can log in with your Codeberg account and start translating. #### How do I backup / move Conversations to a new device? From 50c015f6f59cc10b0b55455db2e22a93a60a7c31 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 26 Dec 2022 10:43:11 +0100 Subject: [PATCH 275/394] delete transifex configuration --- .tx/config | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 .tx/config diff --git a/.tx/config b/.tx/config deleted file mode 100644 index 16a45f234..000000000 --- a/.tx/config +++ /dev/null @@ -1,21 +0,0 @@ -[main] -host = https://www.transifex.com -lang_map = af_ZA: af-rZA, am_ET: am-rET, ar_AE: ar-rAE, ar_BH: ar-rBH, ar_DZ: ar-rDZ, ar_EG: ar-rEG, ar_IQ: ar-rIQ, ar_JO: ar-rJO, ar_KW: ar-rKW, ar_LB: ar-rLB, ar_LY: ar-rLY, ar_MA: ar-rMA, ar_OM: ar-rOM, ar_QA: ar-rQA, ar_SA: ar-rSA, ar_SY: ar-rSY, ar_TN: ar-rTN, ar_YE: ar-rYE, arn_CL: arn-rCL, as_IN: as-rIN, az_AZ: az-rAZ, ba_RU: ba-rRU, be_BY: be-rBY, bg_BG: bg-rBG, bn_BD: bn-rBD, bn_IN: bn-rIN, bo_CN: bo-rCN, br_FR: br-rFR, bs_BA: bs-rBA, ca_ES: ca-rES, co_FR: co-rFR, cs_CZ: cs-rCZ, cy_GB: cy-rGB, da_DK: da-rDK, de_AT: de-rAT, de_CH: de-rCH, de_DE: de-rDE, de_LI: de-rLI, de_LU: de-rLU, dsb_DE: dsb-rDE, dv_MV: dv-rMV, el_GR: el-rGR, en_AU: en-rAU, en_BZ: en-rBZ, en_CA: en-rCA, en_GB: en-rGB, en_IE: en-rIE, en_IN: en-rIN, en_JM: en-rJM, en_MY: en-rMY, en_NZ: en-rNZ, en_PH: en-rPH, en_SG: en-rSG, en_TT: en-rTT, en_US: en-rUS, en_ZA: en-rZA, en_ZW: en-rZW, es_AR: es-rAR, es_BO: es-rBO, es_CL: es-rCL, es_CO: es-rCO, es_CR: es-rCR, es_DO: es-rDO, es_EC: es-rEC, es_ES: es-rES, es_GT: es-rGT, es_HN: es-rHN, es_MX: es-rMX, es_NI: es-rNI, es_PA: es-rPA, es_PE: es-rPE, es_PR: es-rPR, es_PY: es-rPY, es_SV: es-rSV, es_US: es-rUS, es_UY: es-rUY, es_VE: es-rVE, et_EE: et-rEE, eu_ES: eu-rES, fa_IR: fa-rIR, fi_FI: fi-rFI, fil_PH: fil-rPH, fo_FO: fo-rFO, fr_BE: fr-rBE, fr_CA: fr-rCA, fr_CH: fr-rCH, fr_FR: fr-rFR, fr_LU: fr-rLU, fr_MC: fr-rMC, fy_NL: fy-rNL, ga_IE: ga-rIE, gd_GB: gd-rGB, gl_ES: gl-rES, gsw_FR: gsw-rFR, gu_IN: gu-rIN, ha_NG: ha-rNG, hi_IN: hi-rIN, hr_BA: hr-rBA, hr_HR: hr-rHR, hsb_DE: hsb-rDE, hu_HU: hu-rHU, hy_AM: hy-rAM, id_ID: id-rID, ig_NG: ig-rNG, ii_CN: ii-rCN, is_IS: is-rIS, it_CH: it-rCH, it_IT: it-rIT, iu_CA: iu-rCA, ja_JP: ja-rJP, ka_GE: ka-rGE, kk_KZ: kk-rKZ, kl_GL: kl-rGL, km_KH: km-rKH, kn_IN: kn-rIN, ko_KR: ko-rKR, kok_IN: kok-rIN, ky_KG: ky-rKG, lb_LU: lb-rLU, lo_LA: lo-rLA, lt_LT: lt-rLT, lv_LV: lv-rLV, mi_NZ: mi-rNZ, mk_MK: mk-rMK, ml_IN: ml-rIN, mn_CN: mn-rCN, mn_MN: mn-rMN, moh_CA: moh-rCA, mr_IN: mr-rIN, ms_BN: ms-rBN, ms_MY: ms-rMY, mt_MT: mt-rMT, nb_NO: nb-rNO, ne_NP: ne-rNP, nl_BE: nl-rBE, nl_NL: nl-rNL, nn_NO: nn-rNO, nso_ZA: nso-rZA, oc_FR: oc-rFR, or_IN: or-rIN, pa_IN: pa-rIN, pl_PL: pl-rPL, prs_AF: prs-rAF, ps_AF: ps-rAF, pt_BR: pt-rBR, pt_PT: pt-rPT, qut_GT: qut-rGT, quz_BO: quz-rBO, quz_EC: quz-rEC, quz_PE: quz-rPE, rm_CH: rm-rCH, ro_RO: ro-rRO, ru_RU: ru-rRU, rw_RW: rw-rRW, sa_IN: sa-rIN, sah_RU: sah-rRU, se_FI: se-rFI, se_NO: se-rNO, se_SE: se-rSE, si_LK: si-rLK, sk_SK: sk-rSK, sl_SI: sl-rSI, sma_NO: sma-rNO, sma_SE: sma-rSE, smj_NO: smj-rNO, smj_SE: smj-rSE, smn_FI: smn-rFI, sms_FI: sms-rFI, sq_AL: sq-rAL, sr_BA: sr-rBA, sr_CS: sr-rCS, sr_ME: sr-rME, sr_RS: sr-rRS, sv_FI: sv-rFI, sv_SE: sv-rSE, sw_KE: sw-rKE, syr_SY: syr-rSY, ta_IN: ta-rIN, te_IN: te-rIN, tg_TJ: tg-rTJ, th_TH: th-rTH, tk_TM: tk-rTM, tn_ZA: tn-rZA, tr_TR: tr-rTR, tt_RU: tt-rRU, tzm_DZ: tzm-rDZ, ug_CN: ug-rCN, uk_UA: uk-rUA, ur_PK: ur-rPK, uz_UZ: uz-rUZ, vi_VN: vi-rVN, wo_SN: wo-rSN, xh_ZA: xh-rZA, yo_NG: yo-rNG, zh_CN: zh-rCN, zh_HK: zh-rHK, zh_MO: zh-rMO, zh_SG: zh-rSG, zh_TW: zh-rTW, zu_ZA: zu-rZA, no_NO: no-rNO, he_IL: iw-rIL, he: iw - -[conversations.main-strings] -file_filter = src/main/res/values-/strings.xml -source_file = src/main/res/values/strings.xml -source_lang = en - -[conversations.quicksy-strings] -file_filter = src/quicksy/res/values-/strings.xml -source_file = src/quicksy/res/values/strings.xml -source_lang = en -type = ANDROID - -[conversations.conversations-strings] -file_filter = src/conversations/res/values-/strings.xml -source_file = src/conversations/res/values/strings.xml -source_lang = en -type = ANDROID - From 1a03fb95e0855209deedeb567518e524c0d0be6a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 26 Dec 2022 11:05:31 +0100 Subject: [PATCH 276/394] delete .github configuration folder --- .github/FUNDING.yml | 3 --- .github/workflows/android.yml | 34 ---------------------------------- 2 files changed, 37 deletions(-) delete mode 100644 .github/FUNDING.yml delete mode 100644 .github/workflows/android.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 04b80e8a2..000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,3 +0,0 @@ -github: inputmice -liberapay: inputmice -custom: https://gultsch.de/donate.html diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml deleted file mode 100644 index 0b737c568..000000000 --- a/.github/workflows/android.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Android CI - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Download WebRTC - run: mkdir libs && wget -O libs/libwebrtc-m99.aar https://gultsch.de/files/libwebrtc-m99.aar - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - name: Build Quicksy - run: ./gradlew assembleQuicksyFreeDebug - - name: Build Conversations - run: ./gradlew assembleConversationsFreeDebug - - uses: actions/upload-artifact@v2 - with: - name: Conversations all-flavors (debug) - path: ./build/outputs/apk/**/debug/Conversations-*.apk - - From 1b949063950ab6540dc83469946f3e367a4ab208 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 26 Dec 2022 11:40:42 +0100 Subject: [PATCH 277/394] fix weblate link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5d38d8d70..206c398bd 100644 --- a/README.md +++ b/README.md @@ -258,7 +258,7 @@ Conversations is trying to get rid of old behaviours and set an example for other clients. #### Translations -Translations are managed on [Weblate](https://translate.codeberg.org/projects/converastions/). +Translations are managed on [Weblate](https://translate.codeberg.org/projects/conversations/). You can log in with your Codeberg account and start translating. From c848da5b73d0d4056f26100be60ecfa5bdcb3eff Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 26 Dec 2022 12:26:11 +0100 Subject: [PATCH 278/394] always use WebRTC from maven (remove build instructions) --- README.md | 35 ----------------------------------- build.gradle | 3 +-- 2 files changed, 1 insertion(+), 37 deletions(-) diff --git a/README.md b/README.md index 206c398bd..bbbc50acc 100644 --- a/README.md +++ b/README.md @@ -369,41 +369,6 @@ you can get access to the the latest beta version by signing up using [this link #### How do I build Conversations -##### Compiling WebRTC. - -WebRTC is a standard for Internet audio and video communication. libwebrtc, also used in the Google Chrome web browser, implementing the WebRTC standard. - -**Note:** Starting with version 2.8.0 you will need to compile libwebrtc from source because there are no fresh binary releases available to download. - -[Instructions](https://webrtc.github.io/webrtc-org/native-code/android/) can be found on the WebRTC website, however, there build method used by Conversations developers is slightly different. - -``` -mkdir -p ~/Prerequisites-for-Conversations -cd ~/Prerequisites-for-Conversations -git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git -export PATH=~/Prerequisites-for-Conversations/depot_tools:$PATH -mkdir webrtc -cd webrtc -fetch --nohooks webrtc_android -# ...wait for 20Gb of stuff... -gclient sync -# ...wait for more 5Gb of stuff... -cd src -unset _JAVA_OPTS -./tools_webrtc/android/build_aar.py -``` - -It will take some time and build webrtc for all popular Android architectures. -The result will be the file `./libwebrtc.aar` - - -##### Building Conversations itself - -Place the resulting libwebrtc.aar in the `libs/` directory. The PlayStore release currently -uses the stable M90 release and renamed the file name to `libwebrtc-m90.aar` put potentially you can -reference any file name by modifying `build.gradle`. Search for `libwebrtc-m90.aar`, and replace it with `libwebrtc.aar`. - - Make sure to have ANDROID_HOME point to your Android SDK. Use the Android SDK Manager to install missing dependencies. Alternatively (and to avoid thinking about environment variables), create a file called local.properties, in the root of the Conversations build tree, diff --git a/build.gradle b/build.gradle index ce559b326..e00bd688e 100644 --- a/build.gradle +++ b/build.gradle @@ -77,8 +77,7 @@ dependencies { implementation 'com.google.guava:guava:31.1-android' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.49' // temporarily use Snikket’s build of WebRTC. The next release will use our own build - freeImplementation 'org.snikket:webrtc-android:107.0.0' - playstoreImplementation fileTree(include: ['libwebrtc-m107.aar'], dir: 'libs') + implementation 'org.snikket:webrtc-android:107.0.0' } ext { From eb51b03d1a7648fe5d8ceafb2526ca2d12fe679e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 26 Dec 2022 14:30:43 +0100 Subject: [PATCH 279/394] change source code links --- README.md | 12 +++++------- conversations.doap | 8 ++++---- src/main/res/values/about.xml | 4 ++-- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index bbbc50acc..655dc50db 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

Conversations: the very last word in instant messaging

- + Get it on Google Play @@ -11,7 +11,7 @@

-![screenshots](https://raw.githubusercontent.com/inputmice/Conversations/master/screenshots.png) +![screenshots](https://codeberg.org/iNPUTmice/Conversations/raw/branch/master/screenshots.png) ## Design principles @@ -79,7 +79,7 @@ build your apk file. The more convenient way — which not only gives you automatic updates but also supports the further development of Conversations — is to buy the App in the -Google [Play Store](https://play.google.com/store/apps/details?id=eu.siacs.conversations&referrer=utm_source%3Dgithub). +Google [Play Store](https://play.google.com/store/apps/details?id=eu.siacs.conversations&referrer=utm_source%3Dcodeberg). Buying the App from the Play Store will also give you access to our [beta test](#beta). @@ -388,7 +388,7 @@ sdk.dir=Path-To-Sdk Then issue the following commands in order to build the apk. - git clone https://github.com/inputmice/Conversations.git + git clone https://codeberg.org/iNPUTmice/Conversations.git cd Conversations ./gradlew assembleConversationsFreeDebug @@ -429,11 +429,9 @@ directly on your rooted phone. (Search for logcat). However in regards to furthe #### I found a bug -Please report it to our [issue tracker][issues]. If your app crashes please +Please report it to our [issue tracker](https://codeberg.org/iNPUTmice/Conversations/issues). If your app crashes please provide a stack trace. If you are experiencing misbehavior please provide detailed steps to reproduce. Always mention whether you are running the latest Play Store version or the current HEAD. If you are having problems connecting to your XMPP server your file transfer doesn’t work as expected please always include a logcat debug output with your issue (see above). - -[issues]: https://github.com/inputmice/Conversations/issues diff --git a/conversations.doap b/conversations.doap index 93a7df126..7f5e654b2 100644 --- a/conversations.doap +++ b/conversations.doap @@ -15,12 +15,12 @@ - + - + en @@ -53,8 +53,8 @@ - - + + diff --git a/src/main/res/values/about.xml b/src/main/res/values/about.xml index dd5d32a15..d6e064d57 100644 --- a/src/main/res/values/about.xml +++ b/src/main/res/values/about.xml @@ -31,7 +31,7 @@ Conversations • the very last word in instant messaging. - \n\nCopyright © 2014-2022 Daniel Gultsch + \n\nCopyright © 2014-2023 Daniel Gultsch \n\nThis program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or @@ -42,7 +42,7 @@ GNU General Public License for more details. \n\nYou should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses - \n\nDownload the full source code at https://github.com/iNPUTmice/Conversations + \n\nDownload the full source code at https://codeberg.org/iNPUTmice/Conversations \n\n\nLibraries \n\nhttps://webrtc.org\nCopyright (c) 2011, The WebRTC project authors. All rights reserved. (https://webrtc.org/support/license) \n\nhttps://github.com/ypresto/android-transcoder\n(Apache License, Version 2.0) From a157d6796aba748df08006fcd76234b2ceac7c2e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 26 Dec 2022 15:21:18 +0100 Subject: [PATCH 280/394] reformat ruby render script --- art/render.rb | 299 ++++++++++++++++++++++++-------------------------- 1 file changed, 144 insertions(+), 155 deletions(-) diff --git a/art/render.rb b/art/render.rb index 7ae4ac8ae..59cd7b156 100755 --- a/art/render.rb +++ b/art/render.rb @@ -1,168 +1,157 @@ #!/bin/env ruby +# frozen_string_literal: true require 'xml' resolutions = { - 'mdpi' => 1, - 'hdpi' => 1.5, - 'xhdpi' => 2, - 'xxhdpi' => 3, - 'xxxhdpi' => 4, - } + 'mdpi' => 1, + 'hdpi' => 1.5, + 'xhdpi' => 2, + 'xxhdpi' => 3, + 'xxxhdpi' => 4 +} images = { - 'main_logo.svg' => ['conversations/main_logo', 200], - 'quicksy_main_logo.svg' => ['quicksy/main_logo', 200], - 'splash_logo.svg' => ['conversations/splash_logo', 144], - 'quicksy_splash_logo.svg' => ['quicksy/splash_logo', 144], - 'ic_search_black.svg' => ['ic_search_background_black', 144], - 'ic_search_white.svg' => ['ic_search_background_white', 144], - 'ic_no_results_white.svg' => ['ic_no_results_background_white', 144], - 'ic_no_results_black.svg' => ['ic_no_results_background_black', 144], - 'play_video_white.svg' => ['play_video_white', 128], - 'play_gif_white.svg' => ['play_gif_white', 128], - 'play_video_black.svg' => ['play_video_black', 128], - 'play_gif_black.svg' => ['play_gif_black', 128], - 'open_pdf_black.svg' => ['open_pdf_black', 128], - 'open_pdf_white.svg' => ['open_pdf_white', 128], - 'conversations_mono.svg' => ['conversations/ic_notification', 24], - 'quicksy_mono.svg' => ['quicksy/ic_notification', 24], - 'flip_camera_android-black-24dp.svg' => ['ic_flip_camera_android_black_24dp', 24], - 'ic_missed_call_notification.svg' => ['ic_missed_call_notification', 24], - 'ic_send_text_offline.svg' => ['ic_send_text_offline', 36], - 'ic_send_text_offline_white.svg' => ['ic_send_text_offline_white', 36], - 'ic_send_text_online.svg' => ['ic_send_text_online', 36], - 'ic_send_text_away.svg' => ['ic_send_text_away', 36], - 'ic_send_text_dnd.svg' => ['ic_send_text_dnd', 36], - 'ic_send_photo_online.svg' => ['ic_send_photo_online', 36], - 'ic_send_photo_offline.svg' => ['ic_send_photo_offline', 36], - 'ic_send_photo_offline_white.svg' => ['ic_send_photo_offline_white', 36], - 'ic_send_photo_away.svg' => ['ic_send_photo_away', 36], - 'ic_send_photo_dnd.svg' => ['ic_send_photo_dnd', 36], - 'ic_send_location_online.svg' => ['ic_send_location_online', 36], - 'ic_send_location_offline.svg' => ['ic_send_location_offline', 36], - 'ic_send_location_offline_white.svg' => ['ic_send_location_offline_white', 36], - 'ic_send_location_away.svg' => ['ic_send_location_away', 36], - 'ic_send_location_dnd.svg' => ['ic_send_location_dnd', 36], - 'ic_send_voice_online.svg' => ['ic_send_voice_online', 36], - 'ic_send_voice_offline.svg' => ['ic_send_voice_offline', 36], - 'ic_send_voice_offline_white.svg' => ['ic_send_voice_offline_white', 36], - 'ic_send_voice_away.svg' => ['ic_send_voice_away', 36], - 'ic_send_voice_dnd.svg' => ['ic_send_voice_dnd', 36], - 'ic_send_cancel_online.svg' => ['ic_send_cancel_online', 36], - 'ic_send_cancel_offline.svg' => ['ic_send_cancel_offline', 36], - 'ic_send_cancel_offline_white.svg' => ['ic_send_cancel_offline_white', 36], - 'ic_send_cancel_away.svg' => ['ic_send_cancel_away', 36], - 'ic_send_cancel_dnd.svg' => ['ic_send_cancel_dnd', 36], - 'ic_send_picture_online.svg' => ['ic_send_picture_online', 36], - 'ic_send_picture_offline.svg' => ['ic_send_picture_offline', 36], - 'ic_send_picture_offline_white.svg' => ['ic_send_picture_offline_white', 36], - 'ic_send_picture_away.svg' => ['ic_send_picture_away', 36], - 'ic_send_picture_dnd.svg' => ['ic_send_picture_dnd', 36], - 'ic_send_videocam_online.svg' => ['ic_send_videocam_online', 36], - 'ic_send_videocam_offline.svg' => ['ic_send_videocam_offline', 36], - 'ic_send_videocam_offline_white.svg' => ['ic_send_videocam_offline_white', 36], - 'ic_send_videocam_away.svg' => ['ic_send_videocam_away', 36], - 'ic_send_videocam_dnd.svg' => ['ic_send_videocam_dnd', 36], - 'ic_notifications_none_white80.svg' => ['ic_notifications_none_white80', 24], - 'ic_notifications_off_white80.svg' => ['ic_notifications_off_white80', 24], - 'ic_notifications_paused_white80.svg' => ['ic_notifications_paused_white80', 24], - 'ic_notifications_white80.svg' => ['ic_notifications_white80', 24], - 'ic_verified_fingerprint.svg' => ['ic_verified_fingerprint', 36], - 'qrcode-scan.svg' => ['ic_qr_code_scan_white_24dp', 24], - 'message_bubble_received.svg' => ['message_bubble_received.9', 0], - 'message_bubble_received_grey.svg' => ['message_bubble_received_grey.9', 0], - 'message_bubble_received_dark.svg' => ['message_bubble_received_dark.9', 0], - 'message_bubble_received_warning.svg' => ['message_bubble_received_warning.9', 0], - 'message_bubble_received_white.svg' => ['message_bubble_received_white.9', 0], - 'message_bubble_sent.svg' => ['message_bubble_sent.9', 0], - 'message_bubble_sent_grey.svg' => ['message_bubble_sent_grey.9', 0], - 'date_bubble_white.svg' => ['date_bubble_white.9', 0], - 'date_bubble_grey.svg' => ['date_bubble_grey.9', 0], - 'marker.svg' => ['marker', 0] - } - -# Executable paths for Mac OSX -# "/Applications/Inkscape.app/Contents/Resources/bin/inkscape" - -inkscape = "inkscape" -imagemagick = "magick" + 'main_logo.svg' => ['conversations/main_logo', 200], + 'quicksy_main_logo.svg' => ['quicksy/main_logo', 200], + 'splash_logo.svg' => ['conversations/splash_logo', 144], + 'quicksy_splash_logo.svg' => ['quicksy/splash_logo', 144], + 'ic_search_black.svg' => ['ic_search_background_black', 144], + 'ic_search_white.svg' => ['ic_search_background_white', 144], + 'ic_no_results_white.svg' => ['ic_no_results_background_white', 144], + 'ic_no_results_black.svg' => ['ic_no_results_background_black', 144], + 'play_video_white.svg' => ['play_video_white', 128], + 'play_gif_white.svg' => ['play_gif_white', 128], + 'play_video_black.svg' => ['play_video_black', 128], + 'play_gif_black.svg' => ['play_gif_black', 128], + 'open_pdf_black.svg' => ['open_pdf_black', 128], + 'open_pdf_white.svg' => ['open_pdf_white', 128], + 'conversations_mono.svg' => ['conversations/ic_notification', 24], + 'quicksy_mono.svg' => ['quicksy/ic_notification', 24], + 'flip_camera_android-black-24dp.svg' => ['ic_flip_camera_android_black_24dp', 24], + 'ic_send_text_offline.svg' => ['ic_send_text_offline', 36], + 'ic_send_text_offline_white.svg' => ['ic_send_text_offline_white', 36], + 'ic_send_text_online.svg' => ['ic_send_text_online', 36], + 'ic_send_text_away.svg' => ['ic_send_text_away', 36], + 'ic_send_text_dnd.svg' => ['ic_send_text_dnd', 36], + 'ic_send_photo_online.svg' => ['ic_send_photo_online', 36], + 'ic_send_photo_offline.svg' => ['ic_send_photo_offline', 36], + 'ic_send_photo_offline_white.svg' => ['ic_send_photo_offline_white', 36], + 'ic_send_photo_away.svg' => ['ic_send_photo_away', 36], + 'ic_send_photo_dnd.svg' => ['ic_send_photo_dnd', 36], + 'ic_send_location_online.svg' => ['ic_send_location_online', 36], + 'ic_send_location_offline.svg' => ['ic_send_location_offline', 36], + 'ic_send_location_offline_white.svg' => ['ic_send_location_offline_white', 36], + 'ic_send_location_away.svg' => ['ic_send_location_away', 36], + 'ic_send_location_dnd.svg' => ['ic_send_location_dnd', 36], + 'ic_send_voice_online.svg' => ['ic_send_voice_online', 36], + 'ic_send_voice_offline.svg' => ['ic_send_voice_offline', 36], + 'ic_send_voice_offline_white.svg' => ['ic_send_voice_offline_white', 36], + 'ic_send_voice_away.svg' => ['ic_send_voice_away', 36], + 'ic_send_voice_dnd.svg' => ['ic_send_voice_dnd', 36], + 'ic_send_cancel_online.svg' => ['ic_send_cancel_online', 36], + 'ic_send_cancel_offline.svg' => ['ic_send_cancel_offline', 36], + 'ic_send_cancel_offline_white.svg' => ['ic_send_cancel_offline_white', 36], + 'ic_send_cancel_away.svg' => ['ic_send_cancel_away', 36], + 'ic_send_cancel_dnd.svg' => ['ic_send_cancel_dnd', 36], + 'ic_send_picture_online.svg' => ['ic_send_picture_online', 36], + 'ic_send_picture_offline.svg' => ['ic_send_picture_offline', 36], + 'ic_send_picture_offline_white.svg' => ['ic_send_picture_offline_white', 36], + 'ic_send_picture_away.svg' => ['ic_send_picture_away', 36], + 'ic_send_picture_dnd.svg' => ['ic_send_picture_dnd', 36], + 'ic_send_videocam_online.svg' => ['ic_send_videocam_online', 36], + 'ic_send_videocam_offline.svg' => ['ic_send_videocam_offline', 36], + 'ic_send_videocam_offline_white.svg' => ['ic_send_videocam_offline_white', 36], + 'ic_send_videocam_away.svg' => ['ic_send_videocam_away', 36], + 'ic_send_videocam_dnd.svg' => ['ic_send_videocam_dnd', 36], + 'ic_notifications_none_white80.svg' => ['ic_notifications_none_white80', 24], + 'ic_notifications_off_white80.svg' => ['ic_notifications_off_white80', 24], + 'ic_notifications_paused_white80.svg' => ['ic_notifications_paused_white80', 24], + 'ic_notifications_white80.svg' => ['ic_notifications_white80', 24], + 'ic_verified_fingerprint.svg' => ['ic_verified_fingerprint', 36], + 'qrcode-scan.svg' => ['ic_qr_code_scan_white_24dp', 24], + 'message_bubble_received.svg' => ['message_bubble_received.9', 0], + 'message_bubble_received_grey.svg' => ['message_bubble_received_grey.9', 0], + 'message_bubble_received_dark.svg' => ['message_bubble_received_dark.9', 0], + 'message_bubble_received_warning.svg' => ['message_bubble_received_warning.9', 0], + 'message_bubble_received_white.svg' => ['message_bubble_received_white.9', 0], + 'message_bubble_sent.svg' => ['message_bubble_sent.9', 0], + 'message_bubble_sent_grey.svg' => ['message_bubble_sent_grey.9', 0], + 'date_bubble_white.svg' => ['date_bubble_white.9', 0], + 'date_bubble_grey.svg' => ['date_bubble_grey.9', 0], + 'marker.svg' => ['marker', 0] +} + +inkscape = 'inkscape' +imagemagick = 'convert' def execute_cmd(cmd) - puts cmd - system cmd + puts cmd + system cmd end images.each do |source_filename, settings| - svg_content = File.read(source_filename) - - svg = XML::Document.string(svg_content) - base_width = svg.root["width"].to_i - base_height = svg.root["height"].to_i - - guides = svg.find(".//sodipodi:guide","sodipodi:http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd") - - resolutions.each do |resolution, factor| - output_filename, base_size = settings - - if base_size > 0 - width = factor * base_size - height = factor * base_size - else - width = factor * base_width - height = factor * base_height - end - - output_parts = output_filename.split('/') - - if output_parts.count != 2 - path = "../src/main/res/drawable-#{resolution}/#{output_filename}.png" - else - path = "../src/#{output_parts[0]}/res/drawable-#{resolution}/#{output_parts[1]}.png" - end - execute_cmd "#{inkscape} #{source_filename} -C -w #{width} -h #{height} -e #{path}" - - top = [] - right = [] - bottom = [] - left = [] - - guides.each do |guide| - orientation = guide["orientation"] - x, y = guide["position"].split(",") - x, y = x.to_i, y.to_i - - if orientation == "1,0" and y == base_height - top.push(x * factor) - end - - if orientation == "0,1" and x == base_width - right.push((base_height - y) * factor) - end - - if orientation == "1,0" and y == 0 - bottom.push(x * factor) - end - - if orientation == "0,1" and x == 0 - left.push((base_height - y) * factor) - end - end - - next if top.length != 2 - next if right.length != 2 - next if bottom.length != 2 - next if left.length != 2 - - execute_cmd "#{imagemagick} -background none PNG32:#{path} -gravity center -extent #{width+2}x#{height+2} PNG32:#{path}" - - draw_format = "-draw \"line %d,%d %d,%d\"" - top_line = draw_format % [top.min + 1, 0, top.max, 0] - right_line = draw_format % [width + 1, right.min + 1, width + 1, right.max] - bottom_line = draw_format % [bottom.min + 1, height + 1, bottom.max, height + 1] - left_line = draw_format % [0, left.min + 1, 0, left.max] - draws = "#{top_line} #{right_line} #{bottom_line} #{left_line}" - - execute_cmd "#{imagemagick} -background none PNG32:#{path} -fill black -stroke none #{draws} PNG32:#{path}" - end + svg_content = File.read(source_filename) + output_filename, base_size = settings + + svg = XML::Document.string(svg_content) + base_width = svg.root['width'].to_i + base_height = svg.root['height'].to_i + + guides = svg.find('.//sodipodi:guide', 'sodipodi:http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd') + + resolutions.each do |resolution, factor| + if base_size.positive? + width = factor * base_size + height = factor * base_size + else + width = factor * base_width + height = factor * base_height + end + + output_parts = output_filename.split('/') + + path = if output_parts.count != 2 + "../src/main/res/drawable-#{resolution}/#{output_filename}.png" + else + "../src/#{output_parts[0]}/res/drawable-#{resolution}/#{output_parts[1]}.png" + end + execute_cmd "#{inkscape} #{source_filename} -C -w #{width.to_i} -h #{height.to_i} --export-filename=#{path}" + + top = [] + right = [] + bottom = [] + left = [] + + guides.each do |guide| + orientation = guide['orientation'] + x, y = guide['position'].split(',') + x = x.to_i + y = y.to_i + + top.push(x * factor) if (orientation == '1,0') && (y == base_height) + + right.push((base_height - y) * factor) if (orientation == '0,1') && (x == base_width) + + bottom.push(x * factor) if (orientation == '1,0') && y.zero? + + left.push((base_height - y) * factor) if (orientation == '0,1') && x.zero? + end + + next if top.length != 2 + next if right.length != 2 + next if bottom.length != 2 + next if left.length != 2 + + execute_cmd "#{imagemagick} -background none PNG32:#{path} -gravity center -extent #{width + 2}x#{height + 2} PNG32:#{path}" + + draw_format = '-draw "line %d,%d %d,%d"' + top_line = format(draw_format, top.min + 1, 0, top.max, 0) + right_line = format(draw_format, width + 1, right.min + 1, width + 1, right.max) + bottom_line = format(draw_format, bottom.min + 1, height + 1, bottom.max, height + 1) + left_line = format(draw_format, 0, left.min + 1, 0, left.max) + draws = "#{top_line} #{right_line} #{bottom_line} #{left_line}" + + execute_cmd "#{imagemagick} -background none PNG32:#{path} -fill black -stroke none #{draws} PNG32:#{path}" + end end From b4b2939b9cbaca0d29c39bd4c70bb75124780c31 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 26 Dec 2022 18:32:48 +0100 Subject: [PATCH 281/394] add XEP-0215 to readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 655dc50db..dbfc674eb 100644 --- a/README.md +++ b/README.md @@ -47,13 +47,14 @@ support these extensions; therefore to get the most out of Conversations you should consider either switching to an XMPP server that does or — even better — run your own XMPP server for you and your friends. These XEP's are: -* [XEP-0065: SOCKS5 Bytestreams](http://xmpp.org/extensions/xep-0065.html) (or mod_proxy65). Will be used to transfer +* [XEP-0065: SOCKS5 Bytestreams](http://xmpp.org/extensions/xep-0065.html) will be used to transfer files if both parties are behind a firewall (NAT). * [XEP-0163: Personal Eventing Protocol](http://xmpp.org/extensions/xep-0163.html) for avatars and OMEMO. * [XEP-0191: Blocking command](http://xmpp.org/extensions/xep-0191.html) lets you blacklist spammers or block contacts without removing them from your roster. * [XEP-0198: Stream Management](http://xmpp.org/extensions/xep-0198.html) allows XMPP to survive small network outages and changes of the underlying TCP connection. +* [XEP-0215: External Service Discovery](https://xmpp.org/extensions/xep-0215.html) will be used to discover STUN and TURN servers which facilitate P2P A/V calls. * [XEP-0280: Message Carbons](http://xmpp.org/extensions/xep-0280.html) which automatically syncs the messages you send to your desktop client and thus allows you to switch seamlessly from your mobile client to your desktop client and back within one conversation. From ce0992036a7b7dbf12a74c32714720e8e0ca6d87 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 29 Dec 2022 12:53:59 +0100 Subject: [PATCH 282/394] disable proximity sensor after switching from audio to video --- .../conversations/ui/RtpSessionActivity.java | 28 ++++++++++--------- .../xmpp/jingle/WebRTCWrapper.java | 5 ++++ 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index a052f8b39..592a28d59 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -1409,8 +1409,8 @@ public void onJingleRtpConnectionUpdate( @Override public void onAudioDeviceChanged( - AppRTCAudioManager.AudioDevice selectedAudioDevice, - Set availableAudioDevices) { + final AppRTCAudioManager.AudioDevice selectedAudioDevice, + final Set availableAudioDevices) { Log.d( Config.LOGTAG, "onAudioDeviceChanged in activity: selected:" @@ -1418,24 +1418,26 @@ public void onAudioDeviceChanged( + ", available:" + availableAudioDevices); try { - if (getMedia().contains(Media.VIDEO)) { - Log.d(Config.LOGTAG, "nothing to do; in video mode"); - return; - } final RtpEndUserState endUserState = requireRtpConnection().getEndUserState(); - if (endUserState == RtpEndUserState.CONNECTED) { - final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager(); - updateInCallButtonConfigurationSpeaker( - audioManager.getSelectedAudioDevice(), - audioManager.getAudioDevices().size()); - } else if (END_CARD.contains(endUserState)) { + final Set media = getMedia(); + if (END_CARD.contains(endUserState)) { Log.d( Config.LOGTAG, "onAudioDeviceChanged() nothing to do because end card has been reached"); } else { + if (Media.audioOnly(media) && endUserState == RtpEndUserState.CONNECTED) { + final AppRTCAudioManager audioManager = + requireRtpConnection().getAudioManager(); + updateInCallButtonConfigurationSpeaker( + audioManager.getSelectedAudioDevice(), + audioManager.getAudioDevices().size()); + } + Log.d( + Config.LOGTAG, + "put proximity wake lock into proper state after device update"); putProximityWakeLockInProperState(selectedAudioDevice); } - } catch (IllegalStateException e) { + } catch (final IllegalStateException e) { Log.d(Config.LOGTAG, "RTP connection was not available when audio device changed"); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 6c270fbad..d2979d57e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -433,9 +433,14 @@ void restartIce() { public void setIsReadyToReceiveIceCandidates(final boolean ready) { readyToReceivedIceCandidates.set(ready); + final int was = iceCandidates.size(); while (ready && iceCandidates.peek() != null) { eventCallback.onIceCandidate(iceCandidates.poll()); } + final int is = iceCandidates.size(); + Log.d( + EXTENDED_LOGGING_TAG, + "setIsReadyToReceiveCandidates(" + ready + ") was=" + was + " is=" + is); } synchronized void close() { From 1fbff835e1d285350a7135d18c30402a1a758e21 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 29 Dec 2022 13:03:14 +0100 Subject: [PATCH 283/394] bump webrtc to m108 --- build.gradle | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index e00bd688e..3e54620ea 100644 --- a/build.gradle +++ b/build.gradle @@ -76,8 +76,7 @@ dependencies { implementation 'com.google.guava:guava:31.1-android' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.49' - // temporarily use Snikket’s build of WebRTC. The next release will use our own build - implementation 'org.snikket:webrtc-android:107.0.0' + implementation 'im.conversations.webrtc:webrtc-android:108.0.0' } ext { From 13606aae6058cb8dee8100f06f6a999f8734a055 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 29 Dec 2022 14:53:05 +0100 Subject: [PATCH 284/394] add todo item in turn server code --- .../conversations/xmpp/jingle/JingleRtpConnection.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 4c327a31f..139153b4a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -2634,6 +2634,12 @@ private void discoverIceServers(final OnIceServersDiscovered onIceServersDiscove + ": skipping invalid combination of udp/tls in external services"); continue; } + // TODO Starting on milestone 110, Chromium will perform + // stricter validation of TURN and STUN URLs passed to the + // constructor of an RTCPeerConnection. More specifically, + // STUN URLs will not support a query section, and TURN URLs + // will support only a transport parameter in their query + // section. final PeerConnection.IceServer.Builder iceServerBuilder = PeerConnection.IceServer.builder( String.format( From 0c7d6947851cad30978b0f74d8173b4568a0bf05 Mon Sep 17 00:00:00 2001 From: hamburger1024 Date: Mon, 26 Dec 2022 10:52:36 +0000 Subject: [PATCH 285/394] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (956 of 956 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/zh_Hans/ --- src/main/res/values-zh-rCN/strings.xml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index 960e1ec41..adace59a8 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -31,7 +31,6 @@ %d分钟前 %d 未读会话 - 发送中… 正在解密信息。请稍候…… @@ -69,7 +68,7 @@ 保存 完成 %1$s已崩溃 - 用你的 XMPP 账户发送堆栈跟踪来帮助持续开发 %1$s + 用你的 XMPP 账户发送堆栈跟踪来帮助持续开发 %1$s。 立即发送 不再询问 账户无法连接 @@ -86,7 +85,9 @@ 清除聊天记录 您确定要删除此聊天中的所有消息吗?\n\n警告:这不会删除存储在其他设备或服务器上的那些消息的副本。 删除文件 - 您确定要删除此文件吗?\n\n 警告:这不会删除存储在其他设备或服务器上的此文件的副本。 + 您确定要删除此文件吗? +\n +\n警告:这不会删除存储在其他设备或服务器上的此文件的副本。 之后关闭此聊天 选择设备 发送未加密的信息 @@ -111,7 +112,7 @@ 因您的联系人未公布其公钥,无法加密您的信息。\n\n请通知您的联系人设置OpenPGP。 常规 接收文件 - 自动接收小于此大小的文件 + 自动接收小于此大小的文件… 附件 通知 振动 @@ -126,14 +127,14 @@ 在其他设备上检测到活动之后,通知在此时间段内将被静音。 高级 从不发送崩溃报告 - 通过发送堆栈跟踪,您可以帮助Conversations持续发展 + 通过发送堆栈跟踪,您可以为开发提供帮助 确认消息 让对方知道你收到并阅读了他们的消息 防止截屏 在应用切换中隐藏应用程序内容并阻止截图 用户界面 OpenKeychain报告一个错误。 - 错误的密钥 + 错误的加密密钥。 接受 产生了一个错误 错误 @@ -983,5 +984,4 @@ 使用 Tor 时通话被禁用 切换到视频 拒绝切换到视频的请求 - - + \ No newline at end of file From f53c13ab6228c7ec3f53f8f1392c44b374c06ca6 Mon Sep 17 00:00:00 2001 From: hamburger1024 Date: Mon, 26 Dec 2022 10:50:28 +0000 Subject: [PATCH 286/394] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (13 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/zh_Hans/ --- src/conversations/res/values-zh-rCN/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conversations/res/values-zh-rCN/strings.xml b/src/conversations/res/values-zh-rCN/strings.xml index dfe82d428..961973b8c 100644 --- a/src/conversations/res/values-zh-rCN/strings.xml +++ b/src/conversations/res/values-zh-rCN/strings.xml @@ -12,5 +12,5 @@ 点击分享按钮向您的联系人发送加入 %1$s 的邀请。 如果你的联系人在附近,他们也可以扫描下面的代码来接受你的邀请。 加入 %1$s 和我聊天:%2$s - 分享邀请 + 分享邀请… \ No newline at end of file From 2456048c7bb4ec885a8904f45cfe695f9fcf5e85 Mon Sep 17 00:00:00 2001 From: hamburger1024 Date: Mon, 26 Dec 2022 10:51:41 +0000 Subject: [PATCH 287/394] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (9 of 9 strings) Translation: Conversations/Android App (Quicksy) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-quicksy/zh_Hans/ --- src/quicksy/res/values-zh-rCN/strings.xml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/quicksy/res/values-zh-rCN/strings.xml b/src/quicksy/res/values-zh-rCN/strings.xml index 6e6c8e4da..3fecc8532 100644 --- a/src/quicksy/res/values-zh-rCN/strings.xml +++ b/src/quicksy/res/values-zh-rCN/strings.xml @@ -1,12 +1,12 @@ 发现在其它设备上的活动后,Conversations保持安静的时间 - 通过发送堆栈跟踪,您可以帮助Quicksy持续发展 + 通过发送堆栈跟踪,您可以帮助 Quicksy 持续发展 让你的所有联系人知道你使用Quicksy的时间 - 为了在屏幕关闭时也能收到消息提醒,您需要将Quicksy加入受保护的应用列表。 - Quicksy个人资料图片 + 为了在屏幕关闭时也能收到消息提醒,您需要将 Quicksy 加入受保护的应用列表。 + Quicksy 个人资料图片 Quicksy在您的国家无服务。 - 无法确认服务器身份 - 未知安全错误 - 服务器已超时 - + 无法验证服务器身份。 + 未知安全错误。 + 连接到服务器时超时。 + \ No newline at end of file From 395301e2a412c2ea59af2e47ee01ac8f805f535a Mon Sep 17 00:00:00 2001 From: nautilusx Date: Mon, 26 Dec 2022 15:32:07 +0000 Subject: [PATCH 288/394] Translated using Weblate (German) Currently translated at 100.0% (956 of 956 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/de/ --- src/main/res/values-de/strings.xml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 8a38abbc4..d59fde5ff 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -31,10 +31,7 @@ vor %d Minuten %d ungelesene Unterhaltung - - %d ungelesene Unterhaltungen - senden… Nachricht wird entschlüsselt. Bitte warten… @@ -262,7 +259,7 @@ Veröffentlichen Profilbild antippen, um ein Bild aus der Galerie auszuwählen Veröffentliche… - Der Server hat die Veröffentlichung des Avatars abgelehnt. + Der Server hat die Veröffentlichung des Profilbildes abgelehnt. Bild konnte nicht konvertiert werden Profilbild kann nicht gespeichert werden (Oder klicke lange, um den Standard wiederherzustellen) @@ -999,5 +996,4 @@ Anrufe sind bei der Verwendung von Tor deaktiviert Umschalten auf Video Umschalten auf Video ablehnen - - + \ No newline at end of file From 62023a68626ba7c61b9036f79563c44694436ce2 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Tue, 27 Dec 2022 13:16:08 +0000 Subject: [PATCH 289/394] Translated using Weblate (Spanish) Currently translated at 100.0% (956 of 956 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/es/ --- src/main/res/values-es/strings.xml | 113 ++++++++++++++--------------- 1 file changed, 56 insertions(+), 57 deletions(-) diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index 232cca375..daf7e6ebd 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -31,16 +31,11 @@ hace %d min %d conversación sin leer - - %dconversaciones sin leer - - %dconversaciones sin leer - enviando… - Descifrando mensaje. Por favor, espera... + Descifrando el mensaje. Espere por favor… Mensaje cifrado con OpenPGP El apodo ya está en uso Apodo inválido @@ -59,7 +54,7 @@ ¿Quieres eliminar %s de tus marcadores? Las conversaciones con este marcador no serán eliminadas. Registrar nueva cuenta en servidor Cambiar contraseña en servidor - Compartir con... + Compartir con… Comenzar conversación Invitar a contacto Invitar @@ -87,12 +82,14 @@ Error al enviar Preparando para enviar imagen Preparando para enviar imágenes - Compartiendo ficheros. Por favor, espera... + Compartiendo el archivo, por favor espere… Limpiar historial Limpiar historial de conversación ¿Quieres borrar todos los mensajes de esta conversación?\n\nAviso: Esto no afectará a los mensajes guardados en otros dispositivos o servidores. Eliminar fichero - ¿Estás seguro de que quieres borrar este fichero?\n\nAviso:Esto no borrará las copias de este fichero almacenado en otros dispositivos o servidores. + ¿Está seguro de que desea eliminar este archivo\? +\n +\nAdvertencia: Esto no eliminará las copias de este archivo almacenadas en otros dispositivos o servidores. Cerrar esta conversación después Seleccionar dispositivo Enviar mensaje sin cifrar @@ -129,10 +126,10 @@ Sonido de notificación para nuevos mensajes Tono para las nuevas llamadas Periodo de gracia - El periodo de tiempo en el que las notificaciones están silenciadas tras detectar actividad en otro de tus dispositivos. + Después de que se detecte actividad en otros dispositivos, las notificaciones se silenciarán durante este período de tiempo. Avanzado Nunca informar de errores - Al enviar las trazas de error estás ayudando en el desarrollo + Estará ayudando al desarrollo si elige enviar un informe de error Confirmar mensajes Permitir a tus contactos saber cuando has recibido y leído sus mensajes Impedir capturas de pantalla @@ -154,8 +151,10 @@ No se pudo comprimir el archivo de imagen Archivo no encontrado Error general. ¿Es posible que no tengas espacio en disco? - La aplicación usaste para seleccionar esta imagen no proporcionó suficientes permisos para leer el archivo.\n\nUtiliza un explorador de archivos diferente para seleccionar la imagen - La aplicación que has utilizado para compartir este archivo no presentó permisos suficientes + La aplicación que utilizó para seleccionar la imagen no tiene los permisos necesarios para ver la imagen. +\n +\nUse otro administrador de archivos para seleccionar una imagen. + La aplicación que utilizó para compartir este archivo no tiene suficientes permisos. Desconocido Deshabilitado temporalmente Conectado @@ -171,7 +170,7 @@ Token de registro inválido Error de negociación TLS Dominio no verificable - Policy violation + Violación de los términos Servidor incompatible Cliente incompatible Error de flujo @@ -200,15 +199,15 @@ ¿Quieres añadir a %s a tus contactos? Información de servidor XEP-0313: MAM - XEP-0280: Message Carbons - XEP-0352: Client State Indication - XEP-0191: Blocking Command - XEP-0237: Roster Versioning - XEP-0198: Stream Management - XEP-0215: External Service Discovery - XEP-0163: PEP (Avatars / OMEMO) - XEP-0363: HTTP File Upload - XEP-0357: Push + XEP-0280: Duplicado de los mensajes + XEP-0352: Visualización del estado del cliente + XEP-0191: Comandos de bloqueo + XEP-0237: Mantener versiones de la lista de contactos + XEP-0198: Gestión de corrientes + XEP-0215: Exploración de servicios externos + XEP-0163: Protocolo de eventos personales (Avatar/OMEMO) + XEP-0363: Carga de un archivo HTTP + XEP-0357: Notificaciones automáticas No Se han perdido las claves de anuncio públicas @@ -221,14 +220,14 @@ Visto última vez hace %d días Mensaje cifrado. Por favor instala OpenKeychain para descifrarlo. Encontrado un nuevo mensaje cifrado con OpenPGP - OpenPGP Key ID + Identificador de la clave OpenPGP Huella digital OMEMO Huella digital v\\OMEMO Huella digital OMEMO (origen del mensaje) Huella digital v\\OMEMO (origen del mensaje) Otros dispositivos Huellas digitales OMEMO de confianza - Buscando claves... + Descargando claves… Hecho Descifrar Marcadores @@ -254,7 +253,7 @@ No se ha podido destruir el canal Editar asunto de la conversación Asunto - Uniéndose a conversación... + Uniéndose a un chat de grupo… Salir El contacto te ha añadido a su lista de contactos Añadir contacto @@ -307,7 +306,7 @@ Has sido expulsado de esta conversación La conversación en grupo ha sido cerrada Ya no estás dentro de esta conversación en grupo - Has dejado esta conversación en grupo debido a razones técnicas. + Abandonaste esta conversación de grupo por motivos técnicos Usando cuenta %s alojado en %s Comprobando %s en servidor HTTP @@ -339,7 +338,7 @@ Los ficheros de respaldo han sido almacenados en %s Restaurando copia de respaldo Tu copia de respaldo ha sido restaurada - No olvides habilitar esta cuenta + No olvides activar la cuenta. Seleccionar archivo Recibiendo %1$s (%2$d%% completado) Descargar %s @@ -430,9 +429,9 @@ Enviando %s Ofreciendo %s Ocultar desconectados - %s está escribiendo... + %s está escribiendo… %s ha dejado de escribir - %s están escribiendo... + %s están escribiendo… %s han dejado de escribir Notificación de escritura Permitir a tus contactos saber cuando estás escribiendo un mensaje @@ -473,7 +472,7 @@ Error al descargar: No se ha podido conectar con el servidor Falló la descarga: No se puede escribir el fichero Error al descargar: Archivo no válido - Red Tor no disponible. + La red Tor no está disponible Fallo de enlace El servidor no es responsable de este dominio Error @@ -491,7 +490,7 @@ No se ha podido leer el certificado Preferencias de archivado Preferencias de archivado en servidor - Buscando preferencias de archivado. Por favor, espera... + Recuperando la configuración del archivo. Espere por favor… No se ha podido conseguir las preferencias de archivado Captcha requerido Introduce el texto de la imagen de arriba @@ -506,7 +505,7 @@ Todas las conexiones se realizan a través de la red TOR. Requiere Orbot Hostname Puerto - Server- or .onion-address + Dirección del servidor o .onion Éste no es un número de puerto válido Éste no es un hostame válido %1$d de %2$d cuentas conectadas @@ -546,10 +545,11 @@ Has deshabilitado esta cuenta Error de seguridad: ¡Acceso a archivo inválido! No se ha encontrado ninguna aplicación para compartir la URI - Compartir URI con... -
El registro se realiza con tu número de teléfono y Quicksy automáticamente—basado en los teléfonos de tu agenda de contactos—te sugerirá posibles contactos.

Registrándote en Quicksy aceptas nuestra política de privacidad.]]>
+ Compartir URI con… + Quicksy es un derivado del popular cliente XMPP Conversaciones con detección automática de contactos.<br><br>El registro se realiza con tu número de teléfono y Quicksy automáticamente—basado en los teléfonos de tu agenda de contactos—te sugerirá posibles contactos.<br><br>Registrándote en Quicksy aceptas nuestra <a href=https://quicksy.im/#privacy>política de privacidad</a>. Aceptar y continuar - Una guía te ayudará en el proceso de creación de la cuenta en conversations.im.¹\nCuando selecciones conversations.im como proveedor podrás comunicarte con usuarios de otros servidores proporcionándoles tu dirección XMPP completa. + Una guía te ayudará en el proceso de creación de la cuenta en conversaciones.¹ +\nCuando selecciones conversaciones.im como proveedor podrás comunicarte con usuarios de otros servidores proporcionándoles tu dirección XMPP completa. Tu dirección XMPP completa será: %s Crear cuenta Usar otro proveedor de mi elección @@ -567,14 +567,14 @@ El registro falló. Prueba de nuevo más tarde Error en el registro: La contraseña es demasiado débil Elige a los participantes - Creando conversación en grupo... + Creando un chat de grupo… Invitar de nuevo Deshabilitar Corto Medio Largo Uso de difusión - Permite que tus contactos sepan cuando usas Conversations + Permite que tus contactos sepan cuando usas Conversaciones Privacidad Tema Selecciona el color de la paleta @@ -587,7 +587,7 @@ Este dispositivo ya no está en uso Ordenador Teléfono móvil - Tablet + Tableta Navegador Consola Pago requerido @@ -599,11 +599,11 @@ Servidor no encontrado Tiempo de espera agotado al servidor remoto No se ha podido actualizar la cuenta - Reportar a esta dirección XMPP por enviar mensajes no deseados + Reporta esta dirección XMPP como spam. Eliminar identidades OMEMO Regenerar tus clave OMEMO. Todos tus contactos tendrán que verificarte de nuevo. Usa esta opción como último recurso. Eliminar claves seleccionadas - Debes estar conectado para publicar la imagen de perfil + Necesitas estar conectado para publicar un avatar. Mostrar mensaje de error Mensaje de error Optimización de datos habilitado @@ -630,7 +630,7 @@ Limpiar datos privados Limpiar datos privados de ficheros descargados (Pueden volver a descargarse desde el servidor) Enlace desde una fuente de confianza - Vas a verificar las claves OMEMO de %1$s después de hacer click en el enlace. Esto solo es seguro si conseguiste este enlace desde una fuente de confianza donde solo %2$s pudo haber publicado el enlace + Está a punto de verificar las claves OMEMO de %1$s después de hacer clic en un enlace. Esto solo es seguro si siguió este enlace desde una fuente confiable donde solo %2$s podría haber publicado este enlace. Está a punto de verificar las claves OMEMO de su propia cuenta. Esto solamente es seguro si ha seguido este enlace desde una fuente segura, donde solo usted lo haya publicado. Continuar Verificar claves OMEMO @@ -683,7 +683,7 @@ Conectado ahora mismo Reintentar descifrado Fallo de sesión - Downgraded SASL mechanism + Mecanismo SASL degradado El servidor requiere registro en su página web Abrir página web No se ha encontrado aplicación para abrir el sitio web @@ -701,7 +701,7 @@ Mensaje Los mensajes privados están deshabilitados Aplicaciones protegidas - Para seguir recibiendo notificaciones, incluso cuando la pantalla está apagada, necesitas añadir Conversations a la lista de aplicaciones protegidas. + Para recibir notificaciones de mensajes incluso cuando la pantalla está apagada, debe agregar Conversations a la lista de aplicaciones protegidas. ¿Aceptar certificado desconocido? El certificado del servidor no está firmado por una Autoridad Certificadora conocida. ¿Aceptar nombre del servidor no coincidente? @@ -747,7 +747,7 @@ Mostrar ubicación Compartir No se ha podido empezar la grabación - Por favor, espera... + Espere por favor… Permitir a %1$s acceder al micrófono Buscar mensajes GIF @@ -790,7 +790,7 @@ Ver galería Participantes Galería - Fichero omitido por violación de seguridad + El archivo se omitió debido a una violación de seguridad. Calidad del video Calidad más baja indica archivos más pequeños Medio (360p) @@ -820,8 +820,8 @@ ¿Estás seguro de que quieres abortar el proceso de registro? No - Verificando... - Solicitando mensaje SMS... + Verificando… + Solicitando un mensaje de texto… El código que has introducido no es correcto. El código que te hemos enviado ha expirado. Error desconocido de red. @@ -850,8 +850,8 @@ Este canal hará tu dirección XMPP visible públicamente e-book Original (sin comprimir) - Abrir con... - Foto de perfil en Conversations + Abrir con… + Establecer la foto del perfil Elige una cuenta Restaurar copia de respaldo Restaurar @@ -869,14 +869,14 @@ Dirección XMPP Por favor, proporciona un nombre para el canal Por favor, proporciona una dirección XMPP - Esto es una dirección XMPP. Por favor, proporciona un nombre - Creando canal público... + Esta es una dirección XMPP. Introduce un nombre. + Creando un canal público… Esta canal ya existe Te has unido a un canal existente No se ha podido guardar la configuración del canal Permitir a cualquiera editar el asunto Permitir a cualquiera invitar a otros contactos - Todos pueden editar el asunto + Cualquiera puede editar el tema. Los propietarios pueden editar el asunto. Los administradores pueden editar el asunto. Los propietarios pueden invitar a otros contactos. @@ -907,7 +907,7 @@ Esta cuenta ya fue configurada Por favor ingrese la contraseña para esta cuenta No se ha podido realizar esta acción - Unirse a canal público... + Uniéndose a un canal público… La aplicación de compartir no concedió permisos para acceder a este fichero. jabber.network @@ -969,7 +969,7 @@ Ayuda Cambiar a conversación Tu micrófono no está disponible - Solo puedes hacer una llamada a la vez + Solo se puede hacer una llamada a la vez. Volver a la llamada en curso No se ha podido cambiar de cámara Fijar en la parte superior @@ -1015,5 +1015,4 @@ Las llamadas están deshabilitadas cuando se usa Tor Cambiar a vídeo Rechazar petición de cambiar a vídeo - - + \ No newline at end of file From 563560bf8aa5525c13f4b0b89ee3ab5b7df2caa7 Mon Sep 17 00:00:00 2001 From: ghose Date: Tue, 27 Dec 2022 08:03:37 +0000 Subject: [PATCH 290/394] Translated using Weblate (Galician) Currently translated at 100.0% (956 of 956 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/gl/ --- src/main/res/values-gl/strings.xml | 89 +++++++++++++++--------------- 1 file changed, 46 insertions(+), 43 deletions(-) diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 32dce7a18..6a5016681 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -31,13 +31,10 @@ hai %d minutos %d conversa sen ler - - %d conversas sen ler - enviando… - Descifrando a mensaxe. Agarda por favor... + Descifrando a mensaxe. Agarda por favor… Mensaxe cifrado con OpenPGP O alcume xa está en uso Alcume non válido @@ -56,7 +53,7 @@ Desexas eliminar o marcador %s? As conversas deste marcador non se eliminarán. Rexistrar nova conta no servidor Cambiar o contrasinal no servidor - Compartir con + Compartir con… Comezar conversa Convidar contacto Convidar @@ -84,12 +81,16 @@ Erro ao enviar Preparándose para enviar a imaxe Preparándose para enviar imaxes - Compartindo ficheiros. Agarda por favor... + Compartindo ficheiros. Agarda… Baleirar historial Eliminar historial da conversa - ¿Queres eliminar as mensaxes desta conversa?\n\nAviso: Esto non lle afecta as mensaxes gardadas noutros dispositivos ou servidores. + ¿Queres eliminar tódalas mensaxes desta conversa\? +\n +\nAviso: Esto non lle afecta ás mensaxes gardadas noutros dispositivos ou servidores. Eliminar ficheiro - Tes a certeza de querer eliminar este ficheiro?\n\nAviso: Esto non eliminará as copias de este ficheiro que están gardadas noutros dispositivos ou servidores. + Tes a certeza de querer eliminar este ficheiro\? +\n +\nAviso: Esto non eliminará as copias de este ficheiro que están gardadas noutros dispositivos ou servidores. Pechar a conversa tras baleirar Escoller dispositivo Enviar mensaxe non cifrada @@ -107,7 +108,7 @@ Instalar Instala OpenKeychain por favor ofrecendo… - agardando... + agardando… Clave OpenPGP non atopada Non se cifrou a mensaxe porque o contacto non está publicando a súa chave pública.\n\nPídelle ao contacto que configure OpenPGP. Non se atoparon chaves OpenPGP @@ -126,7 +127,7 @@ Son da notificación para novas mensaxes Ton para as chamadas entrantes Período de graza - O tempo no que as notificacións son silenciadas tras detectar actividade en algún dos teus outros dispositivos. + O tempo no que as notificacións son silenciadas tras detectar actividade nalgún dos teus outros dispositivos. Avanzado Nunca enviar informe de erros Ao enviar trazas do sistema estás axudando ao desenvolvemento @@ -151,7 +152,9 @@ Non se puido converter o ficheiro de imaxe Arquivo non atopado Erro xeral de I/O. ¿Quedaches sen espazo no disco? - A app utilizada para seleccionar esta imaxe non deu permisos suficientes para ler o ficheiro.\n\nUsa un xestor de ficheiros diferente para escoller a imaxe + A app utilizada para seleccionar esta imaxe non deu permisos suficientes para ler o ficheiro. +\n +\nUsa un xestor de ficheiros diferente para escoller a imaxe. A app que usaches para compartir este ficheiro non concedeu os permisos suficientes. Descoñecido Desactivado temporalmente @@ -225,7 +228,7 @@ v\\Impresión dixital OMEMO (orixe da mensaxe) Outros dispositivos Confiar en impresións dixitais OMEMO - Obtendo chaves... + Obtendo chaves… Feito Descifrar Marcadores @@ -251,7 +254,7 @@ Non se puido eliminar a canle Editar o tema da conversa en grupo Asunto - Entrando na conversa en grupo + Entrando na conversa en grupo… Saír Contacto engadido a túa lista de contactos Volver a engadir @@ -261,7 +264,7 @@ Todas leron até aquí Publicar Toca no avatar para elixir a imaxe na galería - Publicando... + Publicando… O servidor rexeitou a túa publicación Non se puido converter a imaxe Non se puido salvar o avatar no disco @@ -368,7 +371,7 @@ Algo saíu mal Obtendo historial desde o servidor Non hai máis historial no servidor - Actualizando.... + Actualizando… Mudou o contrasinal! Non puido mudar o contrasinal Cambiar contrasinal @@ -423,13 +426,13 @@ documento PDF App Android Contacto - Publicouse o avatar + Publicouse o avatar! Enviando %s Ofrecendo %s Ocultar fora de liña - %s está a escribir... + %s está escribindo… %s deixou de escribir - %s están escribindo... + %s están escribindo… %s deixaron de escribir Notificacións de escritura Permitelle aos teus contactos que saiban cando lles estás a escribir @@ -487,7 +490,7 @@ Non se puido procesar o certificado Gardando axustes Axustes de gardado no servidor - Obtendo os axustes de gardado. Por favor agarde... + Obtendo os axustes de gardado. Agarda… Non se obtiveron os axustes gardados Requírese o CAPTCHA Introduza o texto da imaxe superior @@ -541,7 +544,7 @@ Desactivou esta conta Fallo de seguridade: Acceso non válido ao ficheiro! Non se atopou unha app para compartir URI - Compartir URI con... + Compartir URI con…
Podes rexistrarte co teu número de teléfono e Quicksy suxerirache automáticamente —tomando os números da túa libreta de enderezos como referencia— posibles contactos para ti.

Ao rexistrarte aceptas a nosa política de privacidade.]]>
Aceptar e continuar Tes unha guía para crear unha conta en conversations.im¹\nAo escoller conversations.im como provedor poderás comunicarte con outras usuarias de outros provedores con só darlles o teu enderezo XMPP completo. @@ -562,7 +565,7 @@ Fallo no rexistro: inténteo de novo Fallo no rexistro: contrasinal moi feble Escoller participantes - Creando unha conversa en grupo... + Creando conversa en grupo… Convidar de novo Desactivar Breve @@ -597,7 +600,7 @@ Denuncia esta conta XMPP por facer spam. Borrar identidades OMEMO Rexenerar chaves OMEMO. Todos os teus contactos terán que verificar a túa conta de novo. Utiliza esto só como último recurso. - Eliminar as chaves seleccionadas. + Eliminar as chaves seleccionadas Debes ter conexión para publicar o teu avatar. Mostrar mensaxe do fallo Mensaxe de fallo @@ -634,7 +637,7 @@ Retirar confianza a dispositivo Tes a certeza de que queres eliminar a verificación deste dispositivo?\nEste dispositivo e as súas mensaxes serán marcados como \"Non confiable\". - %d segundos + %d segundo %d segundos @@ -642,7 +645,7 @@ %d minutos - %d horas + %d hora %d horas @@ -654,8 +657,8 @@ %d semanas - %dmeses - %dmeses + %d mes + %d meses Borrado automático de mensaxes Borrar mensaxes de xeito automático de este dispositivo que anteriores ao marco temporal configurado. @@ -672,7 +675,7 @@ En liña neste momento Volver a intentar o descifrado Fallo na sesión - Downgraded SASL mechanism + Mecanismo SASL desactualizado O servidor require rexistro no sitio web Abrir sitio web Non se atopou app para abrir sitio web @@ -690,15 +693,15 @@ Mensaxe As mensaxes privadas están desactivadas Apps protexidos - Para seguir recibindo notificacións, incluso cando a pantalla está apagada, precisa engadir Conversations a lista de apps protexidos. + Para seguir recibindo notificacións, incluso cando a pantalla está apagada, tes que engadir Conversations á lista de apps protexidas. ¿Aceptar certificado descoñecido? O certificado do servidor non está asinado por unha autoridade de certificación coñecida. ¿Aceptar un nome de servidor que non coincida? - O servidor non pode autenticarse como \"%s\". O certificado só é válido para: + O servidor non pode autenticarse como \"%s\". O certificado só é válido para: Queres conectarte de todos os xeitos? Detalles do certificado: Unha vez - O escaner de código QR necesita acceso á cámara. + O escaner de código QR necesita acceso á cámara Desprazarse ata a parte inferior Desprazarse cara abaixo logo de enviar unha mensaxe Editar a Mensaxe de Estado @@ -708,7 +711,8 @@ Non se obtivo a lista de dispositivos Non se obtiveron as chaves de cifrado Suxestión: Nalgúns casos, isto pode solucionarse engadíndovos mutuamente as vosas listas de contactos. - ¿Tes a certeza de que queres desactivar o cifrado OMEMO para esta conversa? Isto permitirá que o administrador do teu servidor lea as túas mensaxes, pero pode ser a única forma de comunicarse con persoas que usan clientes obsoletos. + Tes a certeza de querer desactivar o cifrado OMEMO para esta conversa\? +\nIsto permitirá á administración do teu servidor ler as túas mensaxes, pero pode ser a única forma de comunicarse con persoas que usan clientes obsoletos. Desactivar agora Borrador: Cifrado OMEMO @@ -723,8 +727,8 @@ Pequena Mediana Grande - A mensaxe non foi encriptada para este disposivivo - Fallo ao descifrar a mensaxe OMEMO + A mensaxe non foi cifrada para este disposivivo. + Fallo ao descifrar a mensaxe OMEMO. desfacer Compartir Localización está desactivado Fixar posición @@ -736,7 +740,7 @@ Mostrar localización Compartir Non comezou a gravación - Por favor, agarde... + Por favor, agarda… Permitir que %1$s acceda ao micrófono Buscar mensaxes GIF @@ -791,7 +795,7 @@ Indica un país número de teléfono Valida o teu número de teléfono - Quicksy vaiche enviar unha mensaxe SMS (podería ter custos) para validar o teu número de teléfono. Escribe o código de país e número de teléfono. + Quicksy vaiche enviar unha mensaxe SMS (podería ter custos) para validar o teu número de teléfono. Escribe o código de país e número de teléfono:
%s

É correcto, ou quere modificar o número?]]>
%s non é un número de teléfono válido. Por favor escribe o teu número de teléfono. @@ -809,8 +813,8 @@ Seguro que quere cancelar o proceso de rexistro? Si Non - Validando... - Solicitando SMS... + Validando… + Solicitando SMS… O pin introducido non é correcto. O pin que che enviamos caducou. Fallo descoñecido na rede. @@ -839,7 +843,7 @@ Esta canle fará público o teu enderezo XMPP e-book Orixinal (non comprimido) - Abrir con... + Abrir con… Imaxe de perfil en Conversations Elixir conta Restablecer copia de apoio @@ -859,7 +863,7 @@ Por favor, escribe un nome para a canle Por favor, escribe un enderezo XMPP Esto é un enderezo XMPP. Por favor, escribe un nome. - Creando canle pública... + Creando canle pública… Esta canle xa existe Entraches nunha canle existente Non se gardaron os axustes da canle @@ -896,7 +900,7 @@ Esta conta xa foi configurada Introduza o contrasinal de esta conta Non se puido completar a acción - Unirse a canle pública... + Unirse a canle pública… A aplicación que comparte non proporciona permiso para acceder ao ficheiro. jabber.network @@ -973,7 +977,7 @@ Gravar correo de voz Reproducir audio Pausar audio - Engade un contacto, crea o únete a unha conversa en grupo ou descubre canles. + Engade un contacto, crea o únete a unha conversa en grupo ou descubre canles Ver %1$d Participante Ver %1$d Participantes @@ -999,5 +1003,4 @@ As chamadas están desactivadas cando usas Tor Cambiar a vídeo Rexeitar a solicitude para cambiar a vídeo - - + \ No newline at end of file From 7261a23e1347de816accba972bf007d6d6b5004d Mon Sep 17 00:00:00 2001 From: random_r Date: Mon, 26 Dec 2022 12:19:22 +0000 Subject: [PATCH 291/394] Translated using Weblate (Italian) Currently translated at 100.0% (956 of 956 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/it/ --- src/main/res/values-it/strings.xml | 40 ++++++++++++++---------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index ab03fb074..58104e5e0 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -31,16 +31,11 @@ %d min fa %d conversazione non letta - - %d conversazioni non lette - - %d conversazioni non lette - invio… - Decifrazione messaggio. Attendere prego... + Decifrazione messaggio. Attendere prego… Messaggio cifrato con OpenPGP Nome utente già in uso Nickname non valido @@ -59,7 +54,7 @@ Vuoi rimuovere %s dai segnalibri? Le conversazioni con questo segnalibro non verranno rimosse. Registra un nuovo profilo sul server Cambia la password sul server - Condividi con + Condividi con… Inizia conversazione Invita contatto Invita @@ -87,7 +82,7 @@ Invio fallito Preparazione per l\'invio dell\'immagine Preparazione per l\'invio delle immagini - Condivisione file. Attendere prego... + Condivisione file. Attendere prego… Pulisci la cronologia Pulisci la cronologia della conversazione Vuoi eliminare tutti i messaggi in questa conversazione?\n\nAttenzione: ciò non influenzerà i messaggi salvati su altri dispositivi o server. @@ -154,7 +149,9 @@ Impossibile convertire l\'immagine File non trovato Errore di I/O generico. Forse hai esaurito lo spazio? - L’app che hai usato per selezionare questa immagine non ha fornito autorizzazioni sufficienti per leggere il file.\n\nUsa un gestore di file differente per scegliere un’immagine + L’app che hai usato per selezionare questa immagine non ha fornito autorizzazioni sufficienti per leggere il file. +\n +\nUsa un gestore di file differente per scegliere un’immagine. L\'app che hai usato per condividere questo file non ha fornito autorizzazioni sufficienti. Sconosciuto Disattivato temporaneamente @@ -228,7 +225,7 @@ v\\Impronta OMEMO (origine del messaggio) Altri dispositivi Fidati delle impronte OMEMO - Ricezione chiavi... + Ricezione chiavi… Fatto Decripta Segnalibri @@ -254,7 +251,7 @@ Distruzione canale fallita Modifica titolo chat di gruppo Argomento - Ingresso nella chat di gruppo... + Ingresso nella chat di gruppo… Abbandona Il contatto ti ha aggiunto alla sua lista contatti Aggiungi anche tu @@ -282,7 +279,9 @@ Attiva La chat di gruppo richiede una password Inserisci la password - Richiedi gli aggiornamenti della presenza dal tuo contatto.\n\nCiò verrà usato per determinare quale app sta usando il tuo contatto. + Prima chiedi gli aggiornamenti della presenza dal tuo contatto. +\n +\nCiò verrà usato per determinare quale app sta usando il tuo contatto. Rechiedi adesso Ignora Attenzione: inviarlo senza aggiornamenti della presenza reciproci può causare problemi inaspettati.\n\nVai nei dettagli del contatto per verificare le tue sottoscrizioni alla presenza. @@ -430,7 +429,7 @@ Invio %s Offrendo %s Nascondi i contatti offline - %s sta digitando... + %s sta digitando… %s ha smesso di digitare %s stanno scrivendo… %s hanno smesso di scrivere @@ -491,7 +490,7 @@ Impossibile analizzare il certificato Preferenze di archiviazione Preferenze di archiviazione lato server - Raccolta preferenze di archiviazione. Attendere prego... + Ricezione preferenze di archiviazione. Attendere prego… Impossibile recuperare le preferenze di archiviazione CAPTCHA necessario Inserisci il testo dell\'immagine soprastante @@ -546,7 +545,7 @@ Hai disattivato questo profilo Errore di sicurezza: accesso file non valido! Nessuna app trovata per condividere l\'URI - Condividi l\'URI con... + Condividi URI con…
Ti registri con il tuo numero di telefono e Quicksy ti suggerirà—in base ai numeri di telefono nella tua rubrica—automaticamente i possibili contatti.

Registrandoti accetti la nostra politica sulla privacy.]]>
Accetta e continua È disponibile una guida per la creazione di un profilo su conversations.im.¹\nQuando scegli conversations.im come fornitore potrai comunicare con utenti di altri fornitori dando il tuo indirizzo XMPP completo. @@ -567,7 +566,7 @@ Registrazione fallita: riprova più tardi Registrazione fallita: password troppo debole Scegli i partecipanti - Creazione chat di gruppo... + Creazione chat di gruppo… Invita di nuovo Disattiva Breve @@ -747,7 +746,7 @@ Mostra la posizione Condividi Impossibile avviare la registrazione - Attendere prego... + Attendere prego… Dai a %1$s l\'accesso al microfono Cerca messaggi GIF @@ -805,7 +804,7 @@ Quicksy invierà un SMS (possono essere applicati costi dal gestore) per verificare il tuo numero. Inserisci il tuo codice nazionale e numero di telefono:
%s

È corretto o vuoi modificare il numero?]]>
%s non è un numero di telefono valido. - Inserisci il tuo numero di telefono: + Inserisci il tuo numero di telefono. Cerca nazioni Verifica %s %s.]]> @@ -907,7 +906,7 @@ Questo profilo è già stato configurato Inserisci la password per questo profilo Impossibile eseguire questa azione - Entra in un canale pubblico... + Entra in un canale pubblico… L\'app di condivisione non ha concesso l\'autorizzazione per accedere a questo file. jabber.network @@ -1015,5 +1014,4 @@ Le chiamate sono disattivate quando si usa Tor Passa al video Rifiuta richiesta di passare al video - - + \ No newline at end of file From 43742c923d0533b7a5db7e59d3b61e428c86165c Mon Sep 17 00:00:00 2001 From: hamburger1024 Date: Mon, 26 Dec 2022 11:14:50 +0000 Subject: [PATCH 292/394] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (956 of 956 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/zh_Hans/ --- src/main/res/values-zh-rCN/strings.xml | 101 +++++++++++++------------ 1 file changed, 54 insertions(+), 47 deletions(-) diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index adace59a8..827f47f6f 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -149,7 +149,9 @@ 无法转换图片 未找到文件 常规I/O错误。可能是存储空间不足? - 您用来选择图片的程序没有给予读取权限。\n\n </small>尝试其他文件管理器选择图片</small>。 + 你用来选择图片的应用没有提供读取文件的足够权限。 +\n +\n使用不同的文件管理器来选择图片. 你用来共享此文件的应用程序没有提供足够的权限。 未知 暂时不可用 @@ -181,7 +183,7 @@ 发布OpenPGP公钥 移除OpenPGP公钥 您确定要从在线状态中移除OpenPGP公钥吗?\n您的联系人将无法再向您发送 OpenPGP 加密信息。 - OpenPGP公钥已发布 + OpenPGP 公钥已发布。 启用账户 确定? 如果您删除帐户,您的所有聊天记录将会丢失 @@ -223,7 +225,7 @@ v\\OMEMO 指纹 (消息来源) 其他设备 信任的OMEMO指纹 - 获取密钥中 + 获取密钥中… 完成 解密 书签 @@ -277,7 +279,9 @@ 启用 需要密码才能进入该群聊 输入密码 - 请先发送更新在线状态请求。\n\n以判断您的联系人所用的客户端类型。 + 请先请求联系人在线状态更新。 +\n +\n这将被用来判断您的联系人正在使用的聊天应用 现在请求 忽略 警告:在没有相互更新在线状态的情况下发送将会出现未知问题。\n\n前往联系人详情以验证您订阅的在线状态。 @@ -361,7 +365,8 @@ 重新生成OMEMO密钥 清除设备 清除所有其他设备的OMEMO通告?下次设备连接时将重新通告,但可能收不到你发送的消息。 - 此联系人没有可用的密钥。\n从服务器获取密钥失败。也许你的联系人所在服务器发生问题。 + 此联系人没有可用的密钥。 +\n无法从服务器获取新密钥。也许你的联系人所在服务器发生问题了? 没有可以用于这个账户的密钥。\n请确保你有相互的在线状态的订阅。 出错了 正在从服务器获取历史记录 @@ -526,7 +531,9 @@ 仅大图片 已启用节电模式 你的设备正对 %1$s 实施强力电池优化,这可能导致通知延迟甚至消息丢失。\n建议禁用这些优化。 - 你的设备正对 %1$s 实施强力电池优化,这可能导致通知延迟甚至消息丢失。\n你将被请求禁用这些优化。 + 你的设备正对 %1$s 实施强力电池优化,这可能导致通知延迟甚至消息丢失。 +\n +\n你将被请求禁用这些优化。 禁用 选择区域过大 (没有启用的账户) @@ -535,7 +542,7 @@ 发送更正后的消息 您已经验证了该用户。点击“完成”让%s加入群聊。 你已经禁用了此账户 - 安全错误:文件访问无效 + 安全错误:文件访问无效! 未找到可以分享此链接的应用 分享链接……
您注册了电话号码,Quicksy就会根据您的通讯录中的电话号码自动为您建议可能的联系人

签署即表示您同意我们的隐私政策。]]>
@@ -546,7 +553,7 @@ 使用我自己的服务器 输入您的用户名 手动更改在线状态 - 编辑状态信息时,您的状态 + 编辑状态信息时设置您是否有空。 状态信息 有空聊天 在线 @@ -590,11 +597,11 @@ 找不到远程服务器 远程服务器超时 无法更新账户 - 举报此账户发送垃圾信息 + 举报此 XMPP 地址发送垃圾信息。 删除OMEMO身份 重新生成OMEMO密钥。所有联系人都需要再次认证。请将此作为最后的办法。 删除选择的密钥 - 你需要连接才能发布头像 + 你需要连接才能发布头像。 显示出错消息 出错信息 省流量模式已启用 @@ -613,7 +620,7 @@ 分享HTTP链接 验证前盲目信任 自动信任陌生人的设备,但在验证过联系人添加设备时手动确认。 - 盲目信任OMEMO密钥,可能会有人冒充对方发送消息 + 盲目信任的 OMEMO 密钥,表示它们可能时其他人或者某人可能冒充别人发送消息。 不信任的 无效二维码 清理缓存文件夹(由相机应用使用) @@ -648,14 +655,14 @@ %d个月
自动删除消息 - 自动从此设备上删除超过配置时间段的消息 + 自动从此设备上删除超过配置时间段的消息。 消息加密中 由于本地保留期限设置,无法提取消息。 正在压缩视频 相应的对话已关闭。 - 联系人已封禁 + 联系人已封禁。 陌生人的消息也通知 - 提醒来自陌生人的消息与通话 + 提醒来自陌生人的消息与通话。 已收到陌生人的信息 封禁陌生人 封禁整个域名 @@ -714,7 +721,7 @@ 消息未对本设备加密。 - 解密OMEMO消息失败 + 解密OMEMO消息失败。 撤销 位置分享已停用 固定位置 @@ -776,42 +783,42 @@ 高(720p) 已取消 你已经在起草一条消息了。 - 功能不支持。 + 功能未实现 无效国家代码 选择国家 手机号 验证手机号 Quicksy将发送验证码短信(运营商可能收费)。请输入国家代码和手机号:
%s

。电话号码正确吗?]]>
- %s不是有效的电话号码 + %s不是有效的电话号码。 请输入手机号。 搜索国家 验证%s %s。]]> - 已重新发送6位数验证码短信 - 输入6位数的PIN + 已发送另一条6位数验证码短信。 + 请在下方输入6位数的PIN。 重新发送短信 重发短信(%s) 请稍候(%s) 返回 - 已自动从剪贴板粘贴验证码 - 请输入6位代码 + 自动从剪贴板粘贴了可能是PIN码的数据。 + 请输入6位数PIN码。 确定放弃注册? - 正在验证... - 请求短信... + 正在验证… + 正请求短信… 验证码错误。 - 验证码已失效 - 未知网络错误 - 未知服务器应答 + 我们发给你的PIN码已失效。 + 未知网络错误。 + 未知服务器响应。 无法连接服务器。 无法建立安全连接。 - 找不到服务器 - 处理请求时出错 + 找不到服务器。 + 处理请求时出错。 用户输入无效 暂时无法连接。请稍候再试。 - 无网络连接 + 无网络连接. 请在%s后重试 你被限制速率 尝试次数过多 @@ -825,16 +832,16 @@ 拒绝请求 安装Orbot 启动Orbot - 软件商店未安装 + 未安装应用商店。 此频道将公开你的XMPP地址 电子书 原始(未压缩) - 打开方式 - 聊天头像 + 打开方式… + Conversations 个人资料图片 选择账户 恢复备份 恢复 - 输入%s的密码以恢复备份 + 输入%s账户的密码以恢复备份。 请勿使用恢复备份功能来尝试克隆安装的应用程序(同时运行)。恢复备份功能仅用于迁移或丢失原始设备的情况。 无法恢复备份。 无法解密备份。密码是否正确? @@ -846,24 +853,24 @@ 创建公开频道 频道名称 XMPP地址 - 请为频道提供一个名称。 - 请提供XMPP地址。 + 请提供频道名 + 请提供XMPP地址 这是一个XMPP地址。请提供一个名称。 - 创建公开频道 + 创建公开频道… 频道已存在 您加入了一个已经存在的频道 无法配置频道 允许任何成员修改主题 允许任何成员邀请其他人 - 允许任何成员修改主题 - 拥有者可修改主题 - 管理员可修改主题 - 所有者可以邀请其他人 - 允许任何成员邀请其他人 + 任何人可以编辑话题。 + 所有者可修改话题。 + 管理员可修改话题。 + 所有者可以邀请其他人。 + 允许任何成员邀请其他人。 XMPP地址对管理员可见。 - XMPP地址对所有人可见 + XMPP 地址对所有人可见。 此公开频道无成员。邀请成员或使用分享按钮分享地址。 - 此私密群聊无成员 + 此私密群聊无成员。 管理权限 搜索成员 文件过大 @@ -886,8 +893,8 @@ 账户已设置 请输入此账户的密码 无法执行此操作 - 加入公开频道 - 分享程序没有访问文件的权限 + 加入公开频道… + 分享程序没有访问文件的权限。 jabber.network 本地服务器 @@ -942,7 +949,7 @@ 帮助 切换到对话 麦克风不可用 - 只能同时打一通电话 + 一次只能打一通电话。 返回正在进行的通话 无法切换摄像头 置顶 @@ -975,7 +982,7 @@ 服务器不支持生成邀请 没有活跃帐户支持此功能 已启动备份。一旦完成,你会收到通知。 - 无法启用视频 + 无法启用视频。 纯文本文档 不支持注册账户 未找到 XMPP 地址 From 501eae9ec3b5522e5bf420508b7a20a8a9f91efd Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Tue, 27 Dec 2022 12:45:01 +0000 Subject: [PATCH 293/394] Translated using Weblate (Spanish) Currently translated at 100.0% (13 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/es/ --- src/conversations/res/values-es/strings.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/conversations/res/values-es/strings.xml b/src/conversations/res/values-es/strings.xml index b5ca254b5..81941ef3e 100644 --- a/src/conversations/res/values-es/strings.xml +++ b/src/conversations/res/values-es/strings.xml @@ -4,7 +4,8 @@ Usa conversations.im Crear nueva cuenta ¿Ya tienes una cuenta XMPP? Este puede ser el caso si ya estás usando un cliente XMPP diferente o has usado Conversations anteriormente. Si no es así, puedes crear una nueva cuenta XMPP ahora mismo.\nConsejo: Algunos proveedores de email también ofrecen una cuenta XMPP. - XMPP es una red de mensajería instantánea independiente del proveedor. Puedes usar este cliente con cualquier servidor XMPP que elijas.\nSin embargo, para tu conveniencia, hacemos de forma sencilla la creación de una cuenta en conversations.im; un proveedor especializado para el uso con Conversations + XMPP es una red de mensajería instantánea que no está vinculada a un proveedor específico. Puede usar el cliente con cualquier servidor que ejecute XMPP. +\nSin embargo, para su comodidad, ofrecemos una forma fácil de crear un perfil en conversaciones.im, un servidor diseñado para funcionar mejor con Conversaciones. Has sido invitado a %1$s. Te guiaremos durante el proceso de creación de la cuenta.\nCuando selecciones %1$s como proveedor podrás comunicarte con usuarios de otros servidores proporcionándoles tu dirección XMPP completa. Has sido invitado a %1$s. Un nombre de usuario ya ha sido escogido para ti. Te guiaremos durante el proceso de creación de la cuenta.\nPodrás comunicarte con otros usuarios de otros servidores proporcionándoles tu dirección XMPP completa. Tu invitación al servidor @@ -12,5 +13,5 @@ Pulsa el botón de compartir para enviar a tu contacto una invitación a %1$s. Si tu contacto está cerca, también puede escanear el código mostrado debajo para aceptar tu invitación. Únete a %1$s y chatea conmigo: %2$s - Compartir invitación con... + Comparte la invitación con… \ No newline at end of file From bca6a10a18f6fc6098aa31e20903c8ef33cfcaa5 Mon Sep 17 00:00:00 2001 From: ghose Date: Mon, 26 Dec 2022 20:26:46 +0000 Subject: [PATCH 294/394] Translated using Weblate (Galician) Currently translated at 100.0% (13 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/gl/ --- src/conversations/res/values-gl/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conversations/res/values-gl/strings.xml b/src/conversations/res/values-gl/strings.xml index ed3299863..2becd8bea 100644 --- a/src/conversations/res/values-gl/strings.xml +++ b/src/conversations/res/values-gl/strings.xml @@ -12,5 +12,5 @@ Toca no botón compartir para convidar ao teu contacto a %1$s. Se o contacto está preto de ti, pode escanear o código inferior para aceptar o teu convite. Únete a %1$s e conversa conmigo: %2$s - Enviar convite a... + Enviar convite a… \ No newline at end of file From 5bbcecf5a6e7ad119596fb89ec3069127e7422af Mon Sep 17 00:00:00 2001 From: random_r Date: Mon, 26 Dec 2022 12:11:44 +0000 Subject: [PATCH 295/394] Translated using Weblate (Italian) Currently translated at 100.0% (13 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/it/ --- src/conversations/res/values-it/strings.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/conversations/res/values-it/strings.xml b/src/conversations/res/values-it/strings.xml index 6e68c5eaa..428a2b032 100644 --- a/src/conversations/res/values-it/strings.xml +++ b/src/conversations/res/values-it/strings.xml @@ -3,10 +3,10 @@ Scegli il tuo fornitore XMPP Usa conversations.im Crea un nuovo profilo - Possiedi già un profilo XMPP? Questo succede se stai già usando un diverso client XMPP o hai già usato prima Conversations. In caso negativo puoi creare un profilo XMPP adesso. -Suggerimento: alcuni provider di email forniscono anche un account XMPP. + Hai già un profilo XMPP\? Può accadere se stai già usando un client XMPP diverso o hai già usato prima Conversations. In caso negativo, puoi creare un profilo XMPP adesso. +\nNota: alcuni fornitori di email offrono anche account XMPP. XMPP è una rete di messaggistica istantanea indipendente dal fornitore. Puoi usare questo client con qualsiasi server XMPP. -In ogni caso per facilitare puoi creare facilmente un account su conversations.im, un fornitore pensato apposta per essere usato con Conversations. +\nTuttavia, per comodità, puoi creare facilmente un account su conversations.im; un fornitore pensato apposta per essere usato con Conversations.
Hai ricevuto un invito per %1$s. Ti guideremo nel procedimento per creare un profilo.\nQuando scegli %1$s come fornitore sarai in grado di comunicare con utenti di altri fornitori dando loro l\'indirizzo XMPP completo. Hai ricevuto un invito per %1$s. È già stato scelto un nome utente per te. Ti guideremo nel procedimento per creare un profilo.\nSarai in grado di comunicare con utenti di altri fornitori dando loro l\'indirizzo XMPP completo. Il tuo invito al server @@ -14,5 +14,5 @@ In ogni caso per facilitare puoi creare facilmente un account su conversations.i Tocca il pulsante condividi per inviare al contatto un invito per %1$s. Se il contatto è vicino, può anche scansionare il codice sottostante per accettare il tuo invito. Unisciti a %1$s e chatta con me: %2$s - Condividi invito con... + Condividi invito con… \ No newline at end of file From 23bc43fbed7992d26161ffc67a4c9afc7e88dee6 Mon Sep 17 00:00:00 2001 From: nautilusx Date: Mon, 26 Dec 2022 14:56:25 +0000 Subject: [PATCH 296/394] Translated using Weblate (German) Currently translated at 100.0% (9 of 9 strings) Translation: Conversations/Android App (Quicksy) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-quicksy/de/ --- src/quicksy/res/values-de/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/quicksy/res/values-de/strings.xml b/src/quicksy/res/values-de/strings.xml index 17bce7af6..7a7480005 100644 --- a/src/quicksy/res/values-de/strings.xml +++ b/src/quicksy/res/values-de/strings.xml @@ -1,6 +1,6 @@ - Zeitspanne, in der Quicksy still bleibt, nachdem es Aktivitäten auf einem anderen Gerät erkannt hat. + Zeitspanne, in der Quicksy still bleibt, nachdem es Aktivitäten auf einem anderen Gerät erkannt hat Wenn du Absturzberichte einschickst, hilfst du Quicksy stetig zu verbessern Informiere deine Kontakte, wann du Quicksy nutzt Um weiterhin Benachrichtigungen zu erhalten, auch wenn der Bildschirm ausgeschaltet ist, musst du Quicksy zur Liste der geschützten Apps hinzufügen. @@ -9,4 +9,4 @@ Überprüfung der Serveridentität ist nicht möglich. Unbekannter Sicherheitsfehler. Zeitüberschreitung bei der Verbindung zum Server. - + \ No newline at end of file From 568eb3f3510642db0d8e377b65be64768e3da957 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Tue, 27 Dec 2022 12:47:54 +0000 Subject: [PATCH 297/394] Translated using Weblate (Spanish) Currently translated at 100.0% (9 of 9 strings) Translation: Conversations/Android App (Quicksy) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-quicksy/es/ --- src/quicksy/res/values-es/strings.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/quicksy/res/values-es/strings.xml b/src/quicksy/res/values-es/strings.xml index 7f8d24022..fb93a9971 100644 --- a/src/quicksy/res/values-es/strings.xml +++ b/src/quicksy/res/values-es/strings.xml @@ -1,12 +1,12 @@ - Periodo de tiempo en el que Quicksy deshabilita las notificaciones tras ver que tienes actividad en otro dispositivo - Si envías registros de error ayudas al desarrollo de Quicksy + Cuánto tiempo Quicksy permanece en silencio después de detectar una actividad en otro dispositivo + Si elige enviar un informe de error, estará ayudando al desarrollo de Quicksy Informar a tus contactos cuando usas Quicksy - Para seguir recibiendo notificaciones, incluso cuando la pantalla está apagada, necesitas añadir Quicksy a la lista de aplicaciones protegidas. - Foto de perfil en Quicksy + Para continuar recibiendo notificaciones incluso cuando la pantalla está apagada, debe agregar Quicksy a la lista de aplicaciones protegidas. + Foto de perfil de Quicksy Quicksy no está disponible en tu país. No se ha podido verificar la identidad del servidor. Error de seguridad desconocido. Se ha superado el tiempo máximo de espera conectando al servidor. - + \ No newline at end of file From f89b28c57e60b85cc8bb9c9276c28f439d3d6372 Mon Sep 17 00:00:00 2001 From: ghose Date: Tue, 27 Dec 2022 07:52:12 +0000 Subject: [PATCH 298/394] Translated using Weblate (Galician) Currently translated at 100.0% (9 of 9 strings) Translation: Conversations/Android App (Quicksy) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-quicksy/gl/ --- src/quicksy/res/values-gl/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/quicksy/res/values-gl/strings.xml b/src/quicksy/res/values-gl/strings.xml index 22b78d01b..0f1343d66 100644 --- a/src/quicksy/res/values-gl/strings.xml +++ b/src/quicksy/res/values-gl/strings.xml @@ -3,10 +3,10 @@ O período de tempo que Quicksy permanece acalado tras ver actividade noutro dispositivo Enviando trazas do rexistro estás axudando ao desenvolvemento de Quicksy Permitir a todos os teus contactos saber cando estás a utilizar Quicksy - Para seguir recibindo notificacións, mesmo coa pantalla apagada, precisas engadir a Quicksy na lista de apps protexidas. + Para seguir recibindo notificacións, mesmo coa pantalla apagada, tes que engadir a Quicksy á lista de apps protexidas. Imaxe de perfil Quicksy Quicksy non está dispoñible no teu país. Non se puido verificar a identidade do servidor. Fallo de seguridade descoñecido. Caducou a conexión mentras conectaba co servidor. - + \ No newline at end of file From c3c5d267271378e920f04091e651787ce3e792aa Mon Sep 17 00:00:00 2001 From: random_r Date: Mon, 26 Dec 2022 16:33:33 +0000 Subject: [PATCH 299/394] Translated using Weblate (Italian) Currently translated at 4.7% (2 of 42 strings) Translation: Conversations/App Store Metadata Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/it/ --- .../android/it-IT/full_description.txt | 39 +++++++++++++++++++ .../android/it-IT/short_description.txt | 1 + 2 files changed, 40 insertions(+) create mode 100644 fastlane/metadata/android/it-IT/full_description.txt create mode 100644 fastlane/metadata/android/it-IT/short_description.txt diff --git a/fastlane/metadata/android/it-IT/full_description.txt b/fastlane/metadata/android/it-IT/full_description.txt new file mode 100644 index 000000000..d390af661 --- /dev/null +++ b/fastlane/metadata/android/it-IT/full_description.txt @@ -0,0 +1,39 @@ +Facile da usare, affidabile, leggero sulla batteria. Con supporto integrato per immagini, chat di gruppo e crittografia e2e. + +Principi di design: + +* Essere il più bello e facile da usare possibile senza sacrificare la sicurezza o la privacy +* Affidarsi a protocolli esistenti ben affermati +* Non richiedere un account Google o nello specifico Google Cloud Messaging (GCM) +* Richiedere il minor numero di autorizzazioni possibile + +Caratteristiche: + +* Crittografia end-to-end con OMEMO o OpenPGP +* Invio e ricezione di immagini +* Chiamate audio e video crittografate (DTLS-SRTP) +* Interfaccia utente intuitiva che segue le linee guida del design di Android +* Immagini / Avatar per i tuoi contatti +* Sincronizzazione con client desktop +* Conferenze (con supporto ai segnalibri) +* Integrazione della rubrica +* Profili multipli / messaggi unificati +* Consumo molto basso della batteria + +Conversations rende veramente facile creare un profilo sul server gratuito conversations.im. Tuttavia Conversations funzionerà anche con qualsiasi altro server XMPP. Molti server XMPP vengono gestiti da volontari e sono gratuiti. + +Caratteristiche di XMPP: + +Conversations funziona con tutti i server XMPP. Tuttavia XMPP è un protocollo estensibile. Anche queste estensioni sono standardizzate, con il nome XEP. Conversations supporta alcune di esse per rendere migliore l'esperienza utente. È possibile che il server XMPP che stai usando non supporti queste estensioni. Perciò, per ottenere il meglio da Conversations dovresti considerare di passare ad un server XMPP che le supporta o, ancora meglio, installarne uno tuo per te e i tuoi amici. + +Queste XEP sono, ad oggi: + +* XEP-0065: SOCKS5 Bytestreams (o mod_proxy65). Usata per trasferire file se entrambe le parti sono dietro un firewall (NAT). +* XEP-0163: Personal Eventing Protocol. Per gli avatar. +* XEP-0191: Blocking command. Ti consente di bloccare lo spam o i contatti senza rimuoverli dal tuo elenco. +* XEP-0198: Stream Management. Consente a XMPP di resistere a brevi disconnessioni e cambi della connessione TCP sottostante. +* XEP-0280: Message Carbons. Sincronizza automaticamente i messaggi che invii al client desktop, quindi ti consente di passare senza problemi dal mobile al desktop e viceversa con un'unica conversazione. +* XEP-0237: Roster Versioning. Principalmente per risparmiare banda di rete in connessioni mobili deboli +* XEP-0313: Message Archive Management. Sincronizza la cronologia dei messaggi con il server. Recupera i messaggi che sono stati inviati mentre Conversations era offline. +* XEP-0352: Client State Indication. Fa sapere al server se Conversations è in secondo piano o no. Permette al server di risparmiare banda di rete trattenendo i pacchetti non importanti. +* XEP-0363: HTTP File Upload. Ti consente di condividere file nelle conferenze e con i contatti offline. Richiede un componente aggiuntivo sul tuo server. diff --git a/fastlane/metadata/android/it-IT/short_description.txt b/fastlane/metadata/android/it-IT/short_description.txt new file mode 100644 index 000000000..66e51b2d5 --- /dev/null +++ b/fastlane/metadata/android/it-IT/short_description.txt @@ -0,0 +1 @@ +Un client di messaggistica XMPP facile e criptato, ottimizzato per il mobile From 43ab64a94ca3db17a1110354175190654458f710 Mon Sep 17 00:00:00 2001 From: Luensche Date: Tue, 27 Dec 2022 10:31:10 +0000 Subject: [PATCH 300/394] Translated using Weblate (German) Currently translated at 23.8% (10 of 42 strings) Translation: Conversations/App Store Metadata Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/de/ --- fastlane/metadata/android/de-DE/changelogs/351.txt | 3 +++ fastlane/metadata/android/de-DE/changelogs/360.txt | 1 + fastlane/metadata/android/de-DE/changelogs/362.txt | 1 + fastlane/metadata/android/de-DE/changelogs/364.txt | 2 ++ 4 files changed, 7 insertions(+) create mode 100644 fastlane/metadata/android/de-DE/changelogs/351.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/360.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/362.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/364.txt diff --git a/fastlane/metadata/android/de-DE/changelogs/351.txt b/fastlane/metadata/android/de-DE/changelogs/351.txt new file mode 100644 index 000000000..5ee41b5bd --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/351.txt @@ -0,0 +1,3 @@ +* Fehlerkorrektur für Jingle IBB Dateitransfer +* Fehlerkorrektur für wiederholende Korrekturen, welche die Datenbank füllen +* Wechsel zu Last Message Correction v1.1 diff --git a/fastlane/metadata/android/de-DE/changelogs/360.txt b/fastlane/metadata/android/de-DE/changelogs/360.txt new file mode 100644 index 000000000..1a4f98051 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/360.txt @@ -0,0 +1 @@ +* Unterstützung für ?register und ?register;preauth XMPP URI-Parameter diff --git a/fastlane/metadata/android/de-DE/changelogs/362.txt b/fastlane/metadata/android/de-DE/changelogs/362.txt new file mode 100644 index 000000000..2c056af00 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/362.txt @@ -0,0 +1 @@ +* Unterstützung für automatische Motiv/Themewechsel in Android 10 diff --git a/fastlane/metadata/android/de-DE/changelogs/364.txt b/fastlane/metadata/android/de-DE/changelogs/364.txt new file mode 100644 index 000000000..9297ddc86 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/364.txt @@ -0,0 +1,2 @@ +* Bereitstellen von PDF-Vorschau ab Android 5+ +* Nutzung von 12 byte IVs für OMEMO From b3d21571fc3638b605384a30be7604a873ca7967 Mon Sep 17 00:00:00 2001 From: nautilusx Date: Wed, 28 Dec 2022 17:39:23 +0000 Subject: [PATCH 301/394] Translated using Weblate (German) Currently translated at 100.0% (956 of 956 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/de/ --- src/main/res/values-de/strings.xml | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index d59fde5ff..a135a9e90 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -86,7 +86,9 @@ Unterhaltungsverlauf löschen Möchtest du alle Nachrichten in dieser Unterhaltung löschen?\n\nAchtung: Dies beeinflusst nicht Nachrichten, die auf anderen Geräten oder Servern gespeichert sind. Datei löschen - Bist du sicher, dass du diese Datei löschen möchtest?\n\nAchtung: Dies löscht keine Kopien dieser Datei, die auf anderen Geräten oder Servern gespeichert sind. + Bist du sicher, dass du diese Datei löschen möchtest\? +\n +\nAchtung: Dies löscht keine Kopien dieser Datei, die auf anderen Geräten oder Servern gespeichert sind. Diese Unterhaltung danach beenden Gerät auswählen Unverschlüsselt schreiben… @@ -191,7 +193,7 @@ Passwort Ungültige XMPP-Adresse Zu wenig Speicher vorhanden. Bild ist zu groß - %s zum Telefonbuch hinzufügen + Möchtest du %s in dein Telefonbuch hinzufügen\? Server-Info XEP-0313: MAM XEP-0280: Message Carbons @@ -259,7 +261,7 @@ Veröffentlichen Profilbild antippen, um ein Bild aus der Galerie auszuwählen Veröffentliche… - Der Server hat die Veröffentlichung des Profilbildes abgelehnt. + Server hat die Veröffentlichung des Profilbildes abgelehnt Bild konnte nicht konvertiert werden Profilbild kann nicht gespeichert werden (Oder klicke lange, um den Standard wiederherzustellen) @@ -279,7 +281,9 @@ Bitte zuerst den Online-Status von deinem Kontakt anfragen.\n\nDies wird verwendet, um festzustellen, welche Chat-App dein Kontakt nutzt. Jetzt anfordern Ignorieren - Achtung: Ohne gegenseitige Kenntnis des Online-Status kann es zu unerwarteten Problemen kommen.\n\nGehe zu \"Kontaktdetails\", um die Einstellungen zu überprüfen. + Achtung: Ohne gegenseitige Kenntnis des Online-Status kann es zu unerwarteten Problemen kommen. +\n +\nGehe zu \"Kontaktdetails\", um deine Einstellungen zu überprüfen. Sicherheit Nachrichtenkorrektur erlauben Erlaube deinen Kontakten das nachträgliche Korrigieren ihrer Nachrichten @@ -426,7 +430,7 @@ Offline verstecken %s schreibt… %s schreibt nicht mehr - %s schreiben... + %s schreiben… %s schreiben nicht mehr Tipp-Benachrichtigung Informiere deine Kontakte, wenn du eine Nachricht schreibst @@ -733,7 +737,7 @@ Standort anzeigen Teilen Aufnahme konnte nicht gestartet werden - Bitte warten... + Bitte warten… %1$s den Zugriff auf das Mikrofon gewähren Nachrichten durchsuchen GIF @@ -806,8 +810,8 @@ Bist du sicher, dass du den Registrierungsprozess abbrechen willst? Ja Nein - Überprüfen... - SMS anfordern... + Überprüfen… + SMS anfordern… Die eingegebene PIN ist falsch. Die von uns zugesandte PIN ist abgelaufen. Unbekannter Netzwerkfehler. @@ -836,7 +840,7 @@ Dieser Channel wird deine XMPP-Adresse veröffentlichen E-Book Original (unkomprimiert) - Öffnen mit... + Öffnen mit… Conversations Profilbild Konto auswählen Sicherung wiederherstellen @@ -856,7 +860,7 @@ Bitte einen Namen für den Channel eingeben Bitte eine XMPP-Adresse eingeben Dies ist eine XMPP-Adresse. Bitte einen Namen eingeben. - Öffentlichen Channel erstellen... + Öffentlichen Channel erstellen… Dieser Channel existiert bereits Du bist einem bestehenden Channel beigetreten Channeleinstellung konnte nicht gespeichert werden @@ -893,7 +897,7 @@ Dieses Konto wurde bereits eingerichtet Bitte gib das Passwort für dieses Konto ein Diese Aktion konnte nicht durchgeführt werden - Öffentlichen Channel beitreten... + Öffentlichen Channel beitreten… Die teilende App hat keine Berechtigung für den Zugriff auf diese Datei erteilt. jabber.network From 034a86de77cda62d4ca36f87a76e11213568bc43 Mon Sep 17 00:00:00 2001 From: nautilusx Date: Wed, 28 Dec 2022 19:24:51 +0000 Subject: [PATCH 302/394] Translated using Weblate (German) Currently translated at 100.0% (9 of 9 strings) Translation: Conversations/Android App (Quicksy) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-quicksy/de/ --- src/quicksy/res/values-de/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/quicksy/res/values-de/strings.xml b/src/quicksy/res/values-de/strings.xml index 7a7480005..8dff5deae 100644 --- a/src/quicksy/res/values-de/strings.xml +++ b/src/quicksy/res/values-de/strings.xml @@ -1,7 +1,7 @@ Zeitspanne, in der Quicksy still bleibt, nachdem es Aktivitäten auf einem anderen Gerät erkannt hat - Wenn du Absturzberichte einschickst, hilfst du Quicksy stetig zu verbessern + Mit dem Einsenden von Absturzberichten hilfst du bei der Weiterentwicklung von Quicksy Informiere deine Kontakte, wann du Quicksy nutzt Um weiterhin Benachrichtigungen zu erhalten, auch wenn der Bildschirm ausgeschaltet ist, musst du Quicksy zur Liste der geschützten Apps hinzufügen. Quicksy Profilbild From ceed942876b663512ba903227f54fd44ab3f88bd Mon Sep 17 00:00:00 2001 From: hamburger1024 Date: Thu, 29 Dec 2022 00:45:49 +0000 Subject: [PATCH 303/394] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (9 of 9 strings) Translation: Conversations/Android App (Quicksy) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-quicksy/zh_Hans/ --- src/quicksy/res/values-zh-rCN/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/quicksy/res/values-zh-rCN/strings.xml b/src/quicksy/res/values-zh-rCN/strings.xml index 3fecc8532..a93ac4a09 100644 --- a/src/quicksy/res/values-zh-rCN/strings.xml +++ b/src/quicksy/res/values-zh-rCN/strings.xml @@ -1,7 +1,7 @@ 发现在其它设备上的活动后,Conversations保持安静的时间 - 通过发送堆栈跟踪,您可以帮助 Quicksy 持续发展 + 通过发送堆栈跟踪,您可以帮助 Quicksy 的持续开发 让你的所有联系人知道你使用Quicksy的时间 为了在屏幕关闭时也能收到消息提醒,您需要将 Quicksy 加入受保护的应用列表。 Quicksy 个人资料图片 From 1c42fb9c10f8712f4de77cb824cfbf673231c370 Mon Sep 17 00:00:00 2001 From: nautilusx Date: Wed, 28 Dec 2022 18:00:49 +0000 Subject: [PATCH 304/394] Translated using Weblate (German) Currently translated at 100.0% (42 of 42 strings) Translation: Conversations/App Store Metadata Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/de/ --- .../metadata/android/de-DE/changelogs/349.txt | 4 ++ .../metadata/android/de-DE/changelogs/353.txt | 4 ++ .../metadata/android/de-DE/changelogs/362.txt | 2 +- .../metadata/android/de-DE/changelogs/367.txt | 2 + .../metadata/android/de-DE/changelogs/379.txt | 1 + .../metadata/android/de-DE/changelogs/381.txt | 2 + .../metadata/android/de-DE/changelogs/382.txt | 2 + .../metadata/android/de-DE/changelogs/383.txt | 3 ++ .../metadata/android/de-DE/changelogs/387.txt | 2 + .../metadata/android/de-DE/changelogs/388.txt | 3 ++ .../metadata/android/de-DE/changelogs/390.txt | 1 + .../metadata/android/de-DE/changelogs/393.txt | 3 ++ .../metadata/android/de-DE/changelogs/394.txt | 2 + .../metadata/android/de-DE/changelogs/395.txt | 3 ++ .../metadata/android/de-DE/changelogs/397.txt | 3 ++ .../metadata/android/de-DE/changelogs/398.txt | 4 ++ .../metadata/android/de-DE/changelogs/401.txt | 2 + .../metadata/android/de-DE/changelogs/402.txt | 3 ++ .../metadata/android/de-DE/changelogs/403.txt | 3 ++ .../metadata/android/de-DE/changelogs/404.txt | 1 + .../metadata/android/de-DE/changelogs/405.txt | 1 + .../metadata/android/de-DE/changelogs/407.txt | 3 ++ .../android/de-DE/changelogs/42000.txt | 4 ++ .../android/de-DE/changelogs/42006.txt | 2 + .../android/de-DE/changelogs/42010.txt | 2 + .../android/de-DE/changelogs/42012.txt | 1 + .../android/de-DE/changelogs/42013.txt | 1 + .../android/de-DE/changelogs/42014.txt | 2 + .../android/de-DE/changelogs/42015.txt | 1 + .../android/de-DE/changelogs/42018.txt | 3 ++ .../android/de-DE/changelogs/42022.txt | 2 + .../android/de-DE/changelogs/42023.txt | 2 + .../android/de-DE/changelogs/42037.txt | 9 +++++ .../android/de-DE/changelogs/42038.txt | 2 + .../android/de-DE/changelogs/42041.txt | 5 +++ .../android/de-DE/changelogs/42042.txt | 2 + .../android/de-DE/changelogs/42043.txt | 1 + .../android/de-DE/full_description.txt | 39 +++++++++++++++++++ .../android/de-DE/short_description.txt | 1 + 39 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 fastlane/metadata/android/de-DE/changelogs/349.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/353.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/367.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/379.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/381.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/382.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/383.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/387.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/388.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/390.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/393.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/394.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/395.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/397.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/398.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/401.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/402.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/403.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/404.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/405.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/407.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/42000.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/42006.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/42010.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/42012.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/42013.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/42014.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/42015.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/42018.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/42022.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/42023.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/42037.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/42038.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/42041.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/42042.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/42043.txt create mode 100644 fastlane/metadata/android/de-DE/full_description.txt create mode 100644 fastlane/metadata/android/de-DE/short_description.txt diff --git a/fastlane/metadata/android/de-DE/changelogs/349.txt b/fastlane/metadata/android/de-DE/changelogs/349.txt new file mode 100644 index 000000000..9e38d16c1 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/349.txt @@ -0,0 +1,4 @@ +* Einführung einer Experteneinstellung zur Channel-Erkennung auf dem lokalen Server anstelle von search.jabber.network +* Standardmäßig Zustellungshäkchen aktiviert und Einstellung entfernt +* Standardmäßig 'Sendetaste zeigt Status an' aktiviert und die Einstellung entfernt +* Einstellungen für Sicherung und Vordergrunddienst in den Hauptbereich verschoben diff --git a/fastlane/metadata/android/de-DE/changelogs/353.txt b/fastlane/metadata/android/de-DE/changelogs/353.txt new file mode 100644 index 000000000..5b53a3cfb --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/353.txt @@ -0,0 +1,4 @@ +* Benutzer können ihren eigenen Nicknamen festlegen +* Wiederaufnahme des Downloads von OMEMO-verschlüsselten Dateien +* Channels verwenden jetzt '#' als Symbol im Profilbild +* Quicksy verwendet 'immer' als OMEMO-Verschlüsselungsstandard (versteckt das Schlosssymbol) diff --git a/fastlane/metadata/android/de-DE/changelogs/362.txt b/fastlane/metadata/android/de-DE/changelogs/362.txt index 2c056af00..507f33b1a 100644 --- a/fastlane/metadata/android/de-DE/changelogs/362.txt +++ b/fastlane/metadata/android/de-DE/changelogs/362.txt @@ -1 +1 @@ -* Unterstützung für automatische Motiv/Themewechsel in Android 10 +* Unterstützung für automatischen Designwechsel in Android 10 diff --git a/fastlane/metadata/android/de-DE/changelogs/367.txt b/fastlane/metadata/android/de-DE/changelogs/367.txt new file mode 100644 index 000000000..793fe26f9 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/367.txt @@ -0,0 +1,2 @@ +* Profilbildauswahl auf einigen Android 10 Geräten korrigiert +* Dateiübertragung für größere Dateien korrigiert diff --git a/fastlane/metadata/android/de-DE/changelogs/379.txt b/fastlane/metadata/android/de-DE/changelogs/379.txt new file mode 100644 index 000000000..1106e2c62 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/379.txt @@ -0,0 +1 @@ +* Audio-/Videoanrufe (erfordert Serverunterstützung in Form von STUN- und TURN-Servern, die über XEP-0215 ermittelt werden können) diff --git a/fastlane/metadata/android/de-DE/changelogs/381.txt b/fastlane/metadata/android/de-DE/changelogs/381.txt new file mode 100644 index 000000000..b4ad85ce2 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/381.txt @@ -0,0 +1,2 @@ +* Akustische Rückmeldungen (Wählen, Anruf begonnen, Anruf beendet) für Audioanrufe +* Problem mit der Wiederholung eines fehlgeschlagenen Videoanrufs behoben diff --git a/fastlane/metadata/android/de-DE/changelogs/382.txt b/fastlane/metadata/android/de-DE/changelogs/382.txt new file mode 100644 index 000000000..7f49cbb9e --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/382.txt @@ -0,0 +1,2 @@ +* Schaltfläche zum Umschalten der Kamera während eines Videoanrufs hinzugefügt +* Audioanrufe auf Tablets repariert diff --git a/fastlane/metadata/android/de-DE/changelogs/383.txt b/fastlane/metadata/android/de-DE/changelogs/383.txt new file mode 100644 index 000000000..df2b532fe --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/383.txt @@ -0,0 +1,3 @@ +* Anrufsymbol nach links verschoben, damit die anderen Symbole der Symbolleiste an einer einheitlichen Stelle bleiben +* Anzeige der Gesprächsdauer bei Sprachanrufen +* Unterbrechung der Verbindung bei A/V-Anrufen (zwei Personen rufen sich gleichzeitig an) diff --git a/fastlane/metadata/android/de-DE/changelogs/387.txt b/fastlane/metadata/android/de-DE/changelogs/387.txt new file mode 100644 index 000000000..aafeabd80 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/387.txt @@ -0,0 +1,2 @@ +* Überarbeitung der UI für die Anmeldung mit Zertifikat +* Integration der Möglichkeit, Chats ganz oben anzuheften (zu den Favoriten hinzufügen) diff --git a/fastlane/metadata/android/de-DE/changelogs/388.txt b/fastlane/metadata/android/de-DE/changelogs/388.txt new file mode 100644 index 000000000..434dbfe84 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/388.txt @@ -0,0 +1,3 @@ +* Reduzierung des Echos bei Anrufen auf einigen Geräten +* Anmeldung korrigiert, wenn Passwörter Sonderzeichen enthalten +* Wähl- und Besetztzeichen bei Videoanrufen auf dem Lautsprecher abspielen diff --git a/fastlane/metadata/android/de-DE/changelogs/390.txt b/fastlane/metadata/android/de-DE/changelogs/390.txt new file mode 100644 index 000000000..f9743617d --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/390.txt @@ -0,0 +1 @@ +* Möglichkeit zur Aufnahme einer Sprachnachricht, wenn der Anrufer beschäftigt ist diff --git a/fastlane/metadata/android/de-DE/changelogs/393.txt b/fastlane/metadata/android/de-DE/changelogs/393.txt new file mode 100644 index 000000000..272afee94 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/393.txt @@ -0,0 +1,3 @@ +* Hilfe-Schaltfläche anzeigen, wenn A/V-Anruf fehlschlägt +* Einige lästige Abstürze behoben +* Jingle-Verbindungen (Dateiübertragung + Anrufe) mit bloßen JIDs behoben diff --git a/fastlane/metadata/android/de-DE/changelogs/394.txt b/fastlane/metadata/android/de-DE/changelogs/394.txt new file mode 100644 index 000000000..aa8559931 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/394.txt @@ -0,0 +1,2 @@ +* Benachrichtigungen wurden unter bestimmten Bedingungen nicht mehr angezeigt +* Kompatibilitätsprobleme und Abstürze im Zusammenhang mit A/V-Anrufen behoben diff --git a/fastlane/metadata/android/de-DE/changelogs/395.txt b/fastlane/metadata/android/de-DE/changelogs/395.txt new file mode 100644 index 000000000..9f3c4a22c --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/395.txt @@ -0,0 +1,3 @@ +* Hinzufügen von 'Zurück zum Chat' zum Audio-Anruf-Bildschirm +* Verbesserung der Tastaturkürzel +* Fehlerbehebungen diff --git a/fastlane/metadata/android/de-DE/changelogs/397.txt b/fastlane/metadata/android/de-DE/changelogs/397.txt new file mode 100644 index 000000000..75edd1633 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/397.txt @@ -0,0 +1,3 @@ +* Verarbeitung von GPX-Dateien +* Verbesserte Leistung bei der Wiederherstellung von Sicherungen +* Fehlerbehebungen diff --git a/fastlane/metadata/android/de-DE/changelogs/398.txt b/fastlane/metadata/android/de-DE/changelogs/398.txt new file mode 100644 index 000000000..9c82ee472 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/398.txt @@ -0,0 +1,4 @@ +* Suche in einzelnen Unterhaltungen +* Benutzer werden benachrichtigt, wenn die Nachrichtenzustellung fehlschlägt +* Anzeigenamen (Nicks) von Quicksy-Benutzern über Neustarts hinweg speichern +* Hinzufügen einer Schaltfläche zum Starten von Orbot (Tor) aus der Benachrichtigung heraus, falls erforderlich diff --git a/fastlane/metadata/android/de-DE/changelogs/401.txt b/fastlane/metadata/android/de-DE/changelogs/401.txt new file mode 100644 index 000000000..bad3aaea4 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/401.txt @@ -0,0 +1,2 @@ +* Suche auf Android <= 5 korrigiert +* Optimierung des Speicherverbrauchs diff --git a/fastlane/metadata/android/de-DE/changelogs/402.txt b/fastlane/metadata/android/de-DE/changelogs/402.txt new file mode 100644 index 000000000..346c8244a --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/402.txt @@ -0,0 +1,3 @@ +* Bietet einfache Einladungserstellung auf unterstützenden Servern +* GIFs anzeigen, die von Movim gesendet werden +* Profilbilder im Cache speichern diff --git a/fastlane/metadata/android/de-DE/changelogs/403.txt b/fastlane/metadata/android/de-DE/changelogs/403.txt new file mode 100644 index 000000000..8561ef554 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/403.txt @@ -0,0 +1,3 @@ +* Behebung von Verbindungsproblemen, wenn verschiedene Konten unterschiedliche SCRAM-Mechanismen verwenden +* Unterstützung für SCRAM-SHA-512 hinzugefügt +* P2P (Jingle) Dateiübertragung mit eigenem Kontakt zulassen diff --git a/fastlane/metadata/android/de-DE/changelogs/404.txt b/fastlane/metadata/android/de-DE/changelogs/404.txt new file mode 100644 index 000000000..39e76f5f3 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/404.txt @@ -0,0 +1 @@ +* Kleinere Stabilitätsverbesserungen für A/V-Anrufe diff --git a/fastlane/metadata/android/de-DE/changelogs/405.txt b/fastlane/metadata/android/de-DE/changelogs/405.txt new file mode 100644 index 000000000..0f2a94978 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/405.txt @@ -0,0 +1 @@ +* Quicksy: Automatischer Empfang der Bestätigungs-SMS diff --git a/fastlane/metadata/android/de-DE/changelogs/407.txt b/fastlane/metadata/android/de-DE/changelogs/407.txt new file mode 100644 index 000000000..424db96c8 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/407.txt @@ -0,0 +1,3 @@ +* Anzeige der Anruftaste für Offline-Kontakte, wenn diese zuvor Unterstützung gemeldet haben +* Zurück-Taste beendet den Anruf nicht mehr, wenn der Anruf gerade läuft +* Fehlerbehebungen diff --git a/fastlane/metadata/android/de-DE/changelogs/42000.txt b/fastlane/metadata/android/de-DE/changelogs/42000.txt new file mode 100644 index 000000000..d08a8b03c --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/42000.txt @@ -0,0 +1,4 @@ +* Möglichkeit zur Auswahl des Klingeltons für eingehende Anrufe +* Behebung der OpenPGP-Schlüsselerkennung für OpenKeychain 5.6+ +* Korrekte Verifizierung von Punycode-TLS-Zertifikaten +* Verbesserte Stabilität des RTP-Sitzungsaufbaus (Anrufe) diff --git a/fastlane/metadata/android/de-DE/changelogs/42006.txt b/fastlane/metadata/android/de-DE/changelogs/42006.txt new file mode 100644 index 000000000..f577f3552 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/42006.txt @@ -0,0 +1,2 @@ +* Verifizierung von A/V-Anrufen mit bereits bestehenden OMEMO-Sitzungen +* Verbesserung der Kompatibilität mit WebRTC-Implementierungen, die nicht von libwebrtc stammen diff --git a/fastlane/metadata/android/de-DE/changelogs/42010.txt b/fastlane/metadata/android/de-DE/changelogs/42010.txt new file mode 100644 index 000000000..d0550817e --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/42010.txt @@ -0,0 +1,2 @@ +* Verschiedene Fehlerbehebungen rund um die Tor-Unterstützung +* Verbesserung der Anrufkompatibilität mit Dino diff --git a/fastlane/metadata/android/de-DE/changelogs/42012.txt b/fastlane/metadata/android/de-DE/changelogs/42012.txt new file mode 100644 index 000000000..ff8728591 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/42012.txt @@ -0,0 +1 @@ +* Problembehebung beim HTTP-Upload/Download für Benutzer, die den System-CAs nicht vertrauen diff --git a/fastlane/metadata/android/de-DE/changelogs/42013.txt b/fastlane/metadata/android/de-DE/changelogs/42013.txt new file mode 100644 index 000000000..c40304f55 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/42013.txt @@ -0,0 +1 @@ +* Probleme mit "Keine Verbindung" unter Android 7.1 behoben diff --git a/fastlane/metadata/android/de-DE/changelogs/42014.txt b/fastlane/metadata/android/de-DE/changelogs/42014.txt new file mode 100644 index 000000000..6bc08c626 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/42014.txt @@ -0,0 +1,2 @@ +* Domänenname immer überprüfen. Kein Überschreiben von Benutzern +* Unterstützung der Kontaktlisten-Vorauthentifizierung diff --git a/fastlane/metadata/android/de-DE/changelogs/42015.txt b/fastlane/metadata/android/de-DE/changelogs/42015.txt new file mode 100644 index 000000000..d63b15514 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/42015.txt @@ -0,0 +1 @@ +* Kleinere A/V-Verbesserungen diff --git a/fastlane/metadata/android/de-DE/changelogs/42018.txt b/fastlane/metadata/android/de-DE/changelogs/42018.txt new file mode 100644 index 000000000..29dab642b --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/42018.txt @@ -0,0 +1,3 @@ +* Schwarze Balken anzeigen, wenn das entfernte Video nicht dem Seitenverhältnis des Bildschirms entspricht +* Verbesserung der Suchleistung +* Einstellung hinzugefügt, um Bildschirmfotos zu verhindern diff --git a/fastlane/metadata/android/de-DE/changelogs/42022.txt b/fastlane/metadata/android/de-DE/changelogs/42022.txt new file mode 100644 index 000000000..6de1129ac --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/42022.txt @@ -0,0 +1,2 @@ +* Fehler behoben, bei dem einige Videos nicht komprimiert wurden +* Seltenen Absturz beim Öffnen von Benachrichtigungen behoben diff --git a/fastlane/metadata/android/de-DE/changelogs/42023.txt b/fastlane/metadata/android/de-DE/changelogs/42023.txt new file mode 100644 index 000000000..f7e8dba64 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/42023.txt @@ -0,0 +1,2 @@ +* Absturz beim Rendern einiger Anführungszeichen behoben +* Absturz im Willkommensbildschirm behoben diff --git a/fastlane/metadata/android/de-DE/changelogs/42037.txt b/fastlane/metadata/android/de-DE/changelogs/42037.txt new file mode 100644 index 000000000..585b4f896 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/42037.txt @@ -0,0 +1,9 @@ +* Abfrage der Bluetooth-Berechtigung bei A/V-Anrufen (nur bei Bluetooth-Headsets erforderlich) +* Fehler beim Anrufen von Movim behoben +* Anzeige eines falschen Profilbilds bei Gruppenchats behoben +* Immer nach dem Opt-Out für Akku-Optimierungen fragen +* Interaktion mit Google Maps Share Location Plugin behoben +* Fußnote bezüglich der Servergebühr entfernt +* Dateien an einem für Android 11 geeigneten Ort speichern +* Anruf nach Netzwechsel erneut versuchen zu verbinden +* JID des Anrufers und JID des Kontos im Bildschirm für eingehende Anrufe anzeigen diff --git a/fastlane/metadata/android/de-DE/changelogs/42038.txt b/fastlane/metadata/android/de-DE/changelogs/42038.txt new file mode 100644 index 000000000..e18587226 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/42038.txt @@ -0,0 +1,2 @@ +* Kleinere Fehlerbehebungen +* Wiederherstellung der Möglichkeit, über JMP und andere Dienste aufzurufen (Playstore-Version) diff --git a/fastlane/metadata/android/de-DE/changelogs/42041.txt b/fastlane/metadata/android/de-DE/changelogs/42041.txt new file mode 100644 index 000000000..759914db1 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/42041.txt @@ -0,0 +1,5 @@ +* Implementierung von Extensible SASL Profile, Bind 2.0 und Fast für schnellere Wiederverbindungen +* Implementierung von Channel Binding +* Möglichkeit von einem Audioanruf zu einem Videoanruf zu wechseln +* Möglichkeit zum Löschen des eigenen Profilbildes hinzugefügt +* Benachrichtigung für verpasste Anrufe hinzugefügt diff --git a/fastlane/metadata/android/de-DE/changelogs/42042.txt b/fastlane/metadata/android/de-DE/changelogs/42042.txt new file mode 100644 index 000000000..62a20d7cf --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/42042.txt @@ -0,0 +1,2 @@ +* Wiederholungsschleife auf Servern beheben, die nur sm:2 unterstützen +* "Umschalten auf Video" nur anzeigen, wenn die Gegenseite Video unterstützt diff --git a/fastlane/metadata/android/de-DE/changelogs/42043.txt b/fastlane/metadata/android/de-DE/changelogs/42043.txt new file mode 100644 index 000000000..011abd98c --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/42043.txt @@ -0,0 +1 @@ +* Fehler bei der P2P-Dateiübertragung behoben diff --git a/fastlane/metadata/android/de-DE/full_description.txt b/fastlane/metadata/android/de-DE/full_description.txt new file mode 100644 index 000000000..d6b5d4b1e --- /dev/null +++ b/fastlane/metadata/android/de-DE/full_description.txt @@ -0,0 +1,39 @@ +Einfach zu bedienen, zuverlässig, batteriefreundlich. Mit integrierter Unterstützung für Bilder, Gruppenchats und E2E-Verschlüsselung. + +Designprinzipien: + +* Möglichst schön und benutzerfreundlich, ohne Abstriche bei der Sicherheit und Privatsphäre +* Auf bestehende, gut etablierte Protokolle zurückgreifen +* Kein Google-Konto oder speziell Google Cloud Messaging (GCM) erforderlich +* So wenig Berechtigungen wie möglich erfordern + +Funktionen: + +* Ende-zu-Ende-Verschlüsselung entweder mit OMEMO oder OpenPGP +* Senden und Empfangen von Bildern +* Verschlüsselte Audio- und Videoanrufe (DTLS-SRTP) +* Intuitives UI, das den Android Design Richtlinien folgt +* Bilder / Profilbilder für deine Kontakte +* Synchronisation mit Desktop-Client +* Konferenzen (mit Unterstützung für Lesezeichen) +* Adressbucheinbindung +* Mehrere Konten / einheitlicher Posteingang +* Sehr geringe Auswirkungen auf die Akkulaufzeit + +Mit Conversations ist es sehr einfach, ein Konto auf dem kostenlosen conversations.im-Server zu erstellen. Dennoch funktioniert Conversations auch mit jedem anderen XMPP-Server. Zahlreiche XMPP-Server werden von Freiwilligen betrieben und sind kostenlos. + +XMPP-Funktionen: + +Conversations funktioniert mit jedem XMPP-Server. XMPP ist jedoch ein erweiterbares Protokoll. Diese Erweiterungen sind ebenfalls in sogenannten XEP's standardisiert. Conversations unterstützt einige davon, um die Benutzerfreundlichkeit zu verbessern. Es besteht die Möglichkeit, dass Ihr aktueller XMPP-Server diese Erweiterungen nicht unterstützt. Um Conversations optimal nutzen zu können, solltest du daher entweder zu einem XMPP-Server wechseln, der dies unterstützt, oder - noch besser - einen eigenen XMPP-Server für dich und deine Freunde betreiben. + +Diese XEPs sind es derzeit: + +* XEP-0065: SOCKS5 Bytestreams (oder mod_proxy65). Wird für die Übertragung von Dateien verwendet, wenn sich beide Parteien hinter einer Firewall (NAT) befinden. +* XEP-0163: Personal Eventing Protocol für Profilbilder +* XEP-0191: Mit dem Blockierungsbefehl kannst du Spammer auf eine schwarze Liste setzen oder Kontakte blockieren, ohne sie aus deiner Liste zu entfernen. +* XEP-0198: Stream Management ermöglicht es XMPP, kleinere Netzwerkausfälle und Änderungen der zugrunde liegenden TCP-Verbindung zu überstehen. +* XEP-0280: Message Carbons, das die von dir gesendeten Nachrichten automatisch mit deinem Desktop-Client synchronisiert und es dir somit ermöglicht, innerhalb einer Unterhaltung nahtlos von deinem mobilen Client zu deinem Desktop-Client und zurück zu wechseln. +* XEP-0237: Roster Versioning hauptsächlich, um Bandbreite bei schlechten mobilen Verbindungen zu sparen +* XEP-0313: Nachrichtenarchiv-Management synchronisiert den Nachrichtenverlauf mit dem Server. Aufholen von Nachrichten, die gesendet wurden, während Conversations offline war. +* XEP-0352: Client State Indication lässt den Server wissen, ob Conversations im Hintergrund läuft oder nicht. Ermöglicht es dem Server, Bandbreite zu sparen, indem er unwichtige Pakete zurückhält. +* XEP-0363: HTTP File Upload ermöglicht den Austausch von Dateien in Konferenzen und mit Offline-Kontakten. Erfordert eine zusätzliche Komponente auf deinem Server. diff --git a/fastlane/metadata/android/de-DE/short_description.txt b/fastlane/metadata/android/de-DE/short_description.txt new file mode 100644 index 000000000..52910ff20 --- /dev/null +++ b/fastlane/metadata/android/de-DE/short_description.txt @@ -0,0 +1 @@ +Ein verschlüsselter, benutzerfreundlicher XMPP-Instant-Messaging-Client, der für Smartphones optimiert ist From a52ec4998ce25153838d58902df259ec06c21a31 Mon Sep 17 00:00:00 2001 From: inputmice Date: Thu, 29 Dec 2022 13:25:08 +0000 Subject: [PATCH 305/394] Translated using Weblate (Spanish) Currently translated at 100.0% (956 of 956 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/es/ --- src/main/res/values-es/strings.xml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index daf7e6ebd..559051cc9 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -199,15 +199,15 @@ ¿Quieres añadir a %s a tus contactos? Información de servidor XEP-0313: MAM - XEP-0280: Duplicado de los mensajes - XEP-0352: Visualización del estado del cliente - XEP-0191: Comandos de bloqueo - XEP-0237: Mantener versiones de la lista de contactos - XEP-0198: Gestión de corrientes - XEP-0215: Exploración de servicios externos - XEP-0163: Protocolo de eventos personales (Avatar/OMEMO) - XEP-0363: Carga de un archivo HTTP - XEP-0357: Notificaciones automáticas + XEP-0280: Message Carbons + XEP-0352: Client State Indication + XEP-0191: Blocking Command + XEP-0237: Roster Versioning + XEP-0198: Stream Management + XEP-0215: External Service Discovery + XEP-0163: PEP (Avatars / OMEMO) + XEP-0363: HTTP File Upload + XEP-0357: Push No Se han perdido las claves de anuncio públicas @@ -546,10 +546,10 @@ Error de seguridad: ¡Acceso a archivo inválido! No se ha encontrado ninguna aplicación para compartir la URI Compartir URI con… - Quicksy es un derivado del popular cliente XMPP Conversaciones con detección automática de contactos.<br><br>El registro se realiza con tu número de teléfono y Quicksy automáticamente—basado en los teléfonos de tu agenda de contactos—te sugerirá posibles contactos.<br><br>Registrándote en Quicksy aceptas nuestra <a href=https://quicksy.im/#privacy>política de privacidad</a>. + Quicksy es un derivado del popular cliente XMPP Conversations con detección automática de contactos.<br><br>El registro se realiza con tu número de teléfono y Quicksy automáticamente—basado en los teléfonos de tu agenda de contactos—te sugerirá posibles contactos.<br><br>Registrándote en Quicksy aceptas nuestra <a href=https://quicksy.im/#privacy>política de privacidad</a>. Aceptar y continuar - Una guía te ayudará en el proceso de creación de la cuenta en conversaciones.¹ -\nCuando selecciones conversaciones.im como proveedor podrás comunicarte con usuarios de otros servidores proporcionándoles tu dirección XMPP completa. + Una guía te ayudará en el proceso de creación de la cuenta en conversations.im.¹ +\nCuando selecciones conversations.im como proveedor podrás comunicarte con usuarios de otros servidores proporcionándoles tu dirección XMPP completa. Tu dirección XMPP completa será: %s Crear cuenta Usar otro proveedor de mi elección @@ -574,7 +574,7 @@ Medio Largo Uso de difusión - Permite que tus contactos sepan cuando usas Conversaciones + Permite que tus contactos sepan cuando usas Conversations Privacidad Tema Selecciona el color de la paleta From bea0be2cfe9bc8689f059e8e9ff70f079a0c90eb Mon Sep 17 00:00:00 2001 From: inputmice Date: Thu, 29 Dec 2022 13:36:52 +0000 Subject: [PATCH 306/394] Translated using Weblate (Spanish) Currently translated at 100.0% (13 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/es/ --- src/conversations/res/values-es/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/conversations/res/values-es/strings.xml b/src/conversations/res/values-es/strings.xml index 81941ef3e..80958fadc 100644 --- a/src/conversations/res/values-es/strings.xml +++ b/src/conversations/res/values-es/strings.xml @@ -4,8 +4,8 @@ Usa conversations.im Crear nueva cuenta ¿Ya tienes una cuenta XMPP? Este puede ser el caso si ya estás usando un cliente XMPP diferente o has usado Conversations anteriormente. Si no es así, puedes crear una nueva cuenta XMPP ahora mismo.\nConsejo: Algunos proveedores de email también ofrecen una cuenta XMPP. - XMPP es una red de mensajería instantánea que no está vinculada a un proveedor específico. Puede usar el cliente con cualquier servidor que ejecute XMPP. -\nSin embargo, para su comodidad, ofrecemos una forma fácil de crear un perfil en conversaciones.im, un servidor diseñado para funcionar mejor con Conversaciones. + XMPP es una red de mensajería instantánea independiente del proveedor. Puedes usar este cliente con cualquier servidor XMPP que elijas. +\nSin embargo, para tu conveniencia, hacemos de forma sencilla la creación de una cuenta en conversations.im; un proveedor especializado para el uso con Conversations. Has sido invitado a %1$s. Te guiaremos durante el proceso de creación de la cuenta.\nCuando selecciones %1$s como proveedor podrás comunicarte con usuarios de otros servidores proporcionándoles tu dirección XMPP completa. Has sido invitado a %1$s. Un nombre de usuario ya ha sido escogido para ti. Te guiaremos durante el proceso de creación de la cuenta.\nPodrás comunicarte con otros usuarios de otros servidores proporcionándoles tu dirección XMPP completa. Tu invitación al servidor From 01624fb13d4649f2828146c412f56806cfc5c5f5 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 29 Dec 2022 17:51:56 +0100 Subject: [PATCH 307/394] bump libwebrtc to 108.0.1 this version is build using the Threema Docker script and follows the instructions here: https://codeberg.org/iNPUTmice/webrtc-android --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 3e54620ea..c793726e8 100644 --- a/build.gradle +++ b/build.gradle @@ -76,7 +76,7 @@ dependencies { implementation 'com.google.guava:guava:31.1-android' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.49' - implementation 'im.conversations.webrtc:webrtc-android:108.0.0' + implementation 'im.conversations.webrtc:webrtc-android:108.0.1' } ext { From f5b7fbc441d5d345841ee6669ab3c6509ddf551e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 30 Dec 2022 09:00:42 +0100 Subject: [PATCH 308/394] =?UTF-8?q?add=20log=20when=20we=20requested=20tok?= =?UTF-8?q?en=20but=20didn=E2=80=99t=20get=20one?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eu/siacs/conversations/xmpp/XmppConnection.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 407aa7eab..09df8a3a3 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -810,8 +810,16 @@ private boolean processSuccess(final Element success) tokenMechanism = null; } if (tokenMechanism != null && !Strings.isNullOrEmpty(token)) { - this.account.setFastToken(tokenMechanism,token); - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": storing hashed token "+tokenMechanism); + this.account.setFastToken(tokenMechanism, token); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": storing hashed token " + tokenMechanism); + } else if (this.hashTokenRequest != null) { + Log.w( + Config.LOGTAG, + account.getJid().asBareJid() + + ": no response to our hashed token request " + + this.hashTokenRequest); } // a successful resume will not send stream features if (processNopStreamFeatures) { From 01fba162f0b1fe4743233eb6f984648c2d184fd7 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 30 Dec 2022 10:53:49 +0100 Subject: [PATCH 309/394] =?UTF-8?q?code=20clean=20up.=20use=20Optional=20t?= =?UTF-8?q?o=20parse=20SM=E2=80=99s=20h=20attribute?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eu/siacs/conversations/xml/Element.java | 11 +++ .../conversations/xmpp/XmppConnection.java | 81 +++++++++---------- 2 files changed, 49 insertions(+), 43 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xml/Element.java b/src/main/java/eu/siacs/conversations/xml/Element.java index 9c570df93..fe5ad57b5 100644 --- a/src/main/java/eu/siacs/conversations/xml/Element.java +++ b/src/main/java/eu/siacs/conversations/xml/Element.java @@ -1,5 +1,8 @@ package eu.siacs.conversations.xml; +import com.google.common.base.Optional; +import com.google.common.primitives.Ints; + import org.jetbrains.annotations.NotNull; import java.util.ArrayList; @@ -150,6 +153,14 @@ public String getAttribute(String name) { } } + public Optional getOptionalIntAttribute(final String name) { + final String value = getAttribute(name); + if (value == null) { + return Optional.absent(); + } + return Optional.fromNullable(Ints.tryParse(value)); + } + public Jid getAttributeAsJid(String name) { final String jid = this.getAttribute(name); if (jid != null && !jid.isEmpty()) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 09df8a3a3..ca7b53884 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -16,6 +16,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.google.common.base.Optional; import com.google.common.base.Strings; import org.xmlpull.v1.XmlPullParserException; @@ -631,20 +632,21 @@ private void processStream() throws XmlPullParserException, IOException { } final Element ack = tagReader.readElement(nextTag); lastPacketReceived = SystemClock.elapsedRealtime(); - try { - final boolean acknowledgedMessages; - synchronized (this.mStanzaQueue) { - final int serverSequence = Integer.parseInt(ack.getAttribute("h")); - acknowledgedMessages = acknowledgeStanzaUpTo(serverSequence); - } - if (acknowledgedMessages) { - mXmppConnectionService.updateConversationUi(); + final boolean acknowledgedMessages; + synchronized (this.mStanzaQueue) { + final Optional serverSequence = ack.getOptionalIntAttribute("h"); + if (serverSequence.isPresent()) { + acknowledgedMessages = acknowledgeStanzaUpTo(serverSequence.get()); + } else { + acknowledgedMessages = false; + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": server send ack without sequence number"); } - } catch (NumberFormatException | NullPointerException e) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": server send ack without sequence number"); + } + if (acknowledgedMessages) { + mXmppConnectionService.updateConversationUi(); } } else if (nextTag.isStart("failed")) { final Element failed = tagReader.readElement(nextTag); @@ -942,15 +944,11 @@ private void processResumed(final Element resumed) throws StateChangingException this.isBound = true; this.tagWriter.writeStanzaAsync(new RequestPacket()); lastPacketReceived = SystemClock.elapsedRealtime(); - final String h = resumed.getAttribute("h"); - if (h == null) { - resetStreamId(); - throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); - } + final Optional h = resumed.getOptionalIntAttribute("h"); final int serverCount; - try { - serverCount = Integer.parseInt(h); - } catch (final NumberFormatException e) { + if (h.isPresent()) { + serverCount = h.get(); + } else { resetStreamId(); throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } @@ -999,28 +997,22 @@ private void changeStatusToOnline() { } private void processFailed(final Element failed, final boolean sendBindRequest) { - final int serverCount; - try { - serverCount = Integer.parseInt(failed.getAttribute("h")); - } catch (final NumberFormatException | NullPointerException e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resumption failed"); - resetStreamId(); - if (sendBindRequest) { - sendBindRequest(); + final Optional serverCount = failed.getOptionalIntAttribute("h"); + if (serverCount.isPresent()) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": resumption failed but server acknowledged stanza #" + + serverCount.get()); + final boolean acknowledgedMessages; + synchronized (this.mStanzaQueue) { + acknowledgedMessages = acknowledgeStanzaUpTo(serverCount.get()); } - return; - } - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": resumption failed but server acknowledged stanza #" - + serverCount); - final boolean acknowledgedMessages; - synchronized (this.mStanzaQueue) { - acknowledgedMessages = acknowledgeStanzaUpTo(serverCount); - } - if (acknowledgedMessages) { - mXmppConnectionService.updateConversationUi(); + if (acknowledgedMessages) { + mXmppConnectionService.updateConversationUi(); + } + } else { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resumption failed"); } resetStreamId(); if (sendBindRequest) { @@ -1028,7 +1020,7 @@ private void processFailed(final Element failed, final boolean sendBindRequest) } } - private boolean acknowledgeStanzaUpTo(int serverCount) { + private boolean acknowledgeStanzaUpTo(final int serverCount) { if (serverCount > stanzasSent) { Log.e( Config.LOGTAG, @@ -2277,6 +2269,9 @@ private synchronized void sendPacket(final AbstractStanza packet, final boolean } ++stanzasSent; + if (Config.EXTENDED_SM_LOGGING) { + Log.d(Config.LOGTAG, account.getJid().asBareJid()+": counting outbound "+packet.getName()+" as #" + stanzasSent); + } this.mStanzaQueue.append(stanzasSent, stanza); if (stanza instanceof MessagePacket && stanza.getId() != null && inSmacksSession) { if (Config.EXTENDED_SM_LOGGING) { From a7fe3e8372536e21165a6faf8b5f7de2d6428a7b Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 30 Dec 2022 12:09:16 +0100 Subject: [PATCH 310/394] reset stanza count when enabling SM via SASL inline --- .../conversations/xmpp/XmppConnection.java | 43 ++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index ca7b53884..563868d2a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -163,6 +163,7 @@ public class XmppConnection implements Runnable { private String streamId = null; private int stanzasReceived = 0; private int stanzasSent = 0; + private int stanzasSentBeforeAuthentication; private long lastPacketReceived = 0; private long lastPingSent = 0; private long lastConnect = 0; @@ -786,6 +787,7 @@ private boolean processSuccess(final Element success) final Element carbonsEnabled = bound.findChild("enabled", Namespace.CARBONS); final boolean waitForDisco; if (streamManagementEnabled != null) { + resetOutboundStanzaQueue(); processEnabled(streamManagementEnabled); waitForDisco = true; } else { @@ -845,6 +847,37 @@ private boolean processSuccess(final Element success) } } + private void resetOutboundStanzaQueue() { + synchronized (this.mStanzaQueue) { + final List intermediateStanzas = new ArrayList<>(); + if (Config.EXTENDED_SM_LOGGING) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": stanzas sent before auth: " + + this.stanzasSentBeforeAuthentication); + } + for (int i = this.stanzasSentBeforeAuthentication + 1; i <= this.stanzasSent; ++i) { + final AbstractAcknowledgeableStanza stanza = this.mStanzaQueue.get(i); + if (stanza != null) { + intermediateStanzas.add(stanza); + } + } + this.mStanzaQueue.clear(); + for (int i = 0; i < intermediateStanzas.size(); ++i) { + this.mStanzaQueue.put(i, intermediateStanzas.get(i)); + } + this.stanzasSent = intermediateStanzas.size(); + if (Config.EXTENDED_SM_LOGGING) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": resetting outbound stanza queue to " + + this.stanzasSent); + } + } + } + private void processNopStreamFeatures() throws IOException { final Tag tag = tagReader.readTag(); if (tag != null && tag.isStart("features", Namespace.STREAMS)) { @@ -1446,7 +1479,10 @@ private void authenticate(final SaslMechanism.Version version) throws IOExceptio + "/" + this.saslMechanism.getMechanism()); authenticate.setAttribute("mechanism", this.saslMechanism.getMechanism()); - tagWriter.writeElement(authenticate); + synchronized (this.mStanzaQueue) { + this.stanzasSentBeforeAuthentication = this.stanzasSent; + tagWriter.writeElement(authenticate); + } } private static boolean isFastTokenAvailable(final Element authentication) { @@ -2173,7 +2209,10 @@ private boolean establishStream(final SSLSockets.Version sslVersion) generateAuthenticationRequest(quickStartMechanism.getClientFirstMessage(sslSocketOrNull(this.socket)), usingFast); authenticate.setAttribute("mechanism", quickStartMechanism.getMechanism()); sendStartStream(true, false); - tagWriter.writeElement(authenticate); + synchronized (this.mStanzaQueue) { + this.stanzasSentBeforeAuthentication = this.stanzasSent; + tagWriter.writeElement(authenticate); + } Log.d( Config.LOGTAG, account.getJid().toString() From 0cec499565f4b0b2260d5d3f9c4fa32c5ee9893b Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 30 Dec 2022 12:16:19 +0100 Subject: [PATCH 311/394] =?UTF-8?q?make=20sure=20we=20don=E2=80=99t=20disp?= =?UTF-8?q?ose=20video=20source=20twice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../conversations/xmpp/jingle/VideoSourceWrapper.java | 10 +++++++++- .../siacs/conversations/xmpp/jingle/WebRTCWrapper.java | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/VideoSourceWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/VideoSourceWrapper.java index b837131e8..c1ff24521 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/VideoSourceWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/VideoSourceWrapper.java @@ -87,7 +87,15 @@ public void stopCapture() throws InterruptedException { public void dispose() { this.cameraVideoCapturer.dispose(); if (this.videoSource != null) { - this.videoSource.dispose(); + dispose(this.videoSource); + } + } + + private static void dispose(final VideoSource videoSource) { + try { + videoSource.dispose(); + } catch (final IllegalStateException e) { + Log.e(Config.LOGTAG, "unable to dispose video source", e); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index d2979d57e..08154260a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -460,6 +460,7 @@ synchronized void close() { this.localVideoTrack = null; this.remoteVideoTrack = null; if (videoSourceWrapper != null) { + this.videoSourceWrapper = null; try { videoSourceWrapper.stopCapture(); } catch (final InterruptedException e) { From 93c2fd4da699926a19a6c1eae268d7e8179c4cc6 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 30 Dec 2022 12:26:00 +0100 Subject: [PATCH 312/394] downgrade webrtc to m104 m107 shipped with 2.11.0 (both fdroid and play) was causing problems when calling between some (not all) devices. The callee (repsonder) would not see the video of the caller (initiator). Our best guess is that this has something to do with the new av1 decoder and it only occurs between devices where av1 is selected as the codec. It's probably selected on 'modern' devices. It's not happening between a Pixel 4a and a Xiaomi Mi A1 but it is happening between two Pixels --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c793726e8..99355e36b 100644 --- a/build.gradle +++ b/build.gradle @@ -76,7 +76,7 @@ dependencies { implementation 'com.google.guava:guava:31.1-android' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.49' - implementation 'im.conversations.webrtc:webrtc-android:108.0.1' + implementation 'im.conversations.webrtc:webrtc-android:104.0.0' } ext { From 41da2a5957481bfd8b25645e7c185303becca228 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 30 Dec 2022 17:14:18 +0100 Subject: [PATCH 313/394] fix client crashing on empty passwords (regression) --- .../crypto/sasl/ScramMechanism.java | 21 +++++++++++++++++-- .../conversations/crypto/sasl/ScramSha1.java | 4 +++- .../crypto/sasl/ScramSha1Plus.java | 4 +++- .../crypto/sasl/ScramSha256.java | 4 +++- .../crypto/sasl/ScramSha256Plus.java | 4 +++- .../crypto/sasl/ScramSha512.java | 4 +++- .../crypto/sasl/ScramSha512Plus.java | 4 +++- 7 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java index 931debe01..e5708e504 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java @@ -1,7 +1,6 @@ package eu.siacs.conversations.crypto.sasl; import android.util.Base64; -import android.util.Log; import com.google.common.base.CaseFormat; import com.google.common.base.Objects; @@ -13,14 +12,32 @@ import java.security.InvalidKeyException; import java.util.concurrent.ExecutionException; +import javax.crypto.SecretKey; import javax.net.ssl.SSLSocket; -import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.utils.CryptoHelper; abstract class ScramMechanism extends SaslMechanism { + public static final SecretKey EMPTY_KEY = + new SecretKey() { + @Override + public String getAlgorithm() { + return "HMAC"; + } + + @Override + public String getFormat() { + return "RAW"; + } + + @Override + public byte[] getEncoded() { + return new byte[0]; + } + }; + private static final byte[] CLIENT_KEY_BYTES = "Client Key".getBytes(); private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes(); private static final Cache CACHE = diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java index 6f00c49eb..1e0fc32b2 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java @@ -15,7 +15,9 @@ public ScramSha1(final Account account) { @Override protected HashFunction getHMac(final byte[] key) { - return Hashing.hmacSha1(key); + return (key == null || key.length == 0) + ? Hashing.hmacSha1(EMPTY_KEY) + : Hashing.hmacSha1(key); } @Override diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java index d353bd9ee..2ca27570f 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java @@ -15,7 +15,9 @@ public ScramSha1Plus(final Account account, final ChannelBinding channelBinding) @Override protected HashFunction getHMac(final byte[] key) { - return Hashing.hmacSha1(key); + return (key == null || key.length == 0) + ? Hashing.hmacSha1(EMPTY_KEY) + : Hashing.hmacSha1(key); } @Override diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java index 9d7d62c36..b330f1fe7 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java @@ -19,7 +19,9 @@ public ScramSha256(final Account account) { @Override protected HashFunction getHMac(final byte[] key) { - return Hashing.hmacSha256(key); + return (key == null || key.length == 0) + ? Hashing.hmacSha256(EMPTY_KEY) + : Hashing.hmacSha256(key); } @Override diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256Plus.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256Plus.java index 5f15e9bf1..4db33a2fa 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256Plus.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256Plus.java @@ -15,7 +15,9 @@ public ScramSha256Plus(final Account account, final ChannelBinding channelBindin @Override protected HashFunction getHMac(final byte[] key) { - return Hashing.hmacSha256(key); + return (key == null || key.length == 0) + ? Hashing.hmacSha256(EMPTY_KEY) + : Hashing.hmacSha256(key); } @Override diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java index 8194ac0ac..e6dcf2efd 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java @@ -19,7 +19,9 @@ public ScramSha512(final Account account) { @Override protected HashFunction getHMac(final byte[] key) { - return Hashing.hmacSha512(key); + return (key == null || key.length == 0) + ? Hashing.hmacSha512(EMPTY_KEY) + : Hashing.hmacSha512(key); } @Override diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512Plus.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512Plus.java index 610c87e23..5d8461973 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512Plus.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512Plus.java @@ -15,7 +15,9 @@ public ScramSha512Plus(final Account account, final ChannelBinding channelBindin @Override protected HashFunction getHMac(final byte[] key) { - return Hashing.hmacSha512(key); + return (key == null || key.length == 0) + ? Hashing.hmacSha512(EMPTY_KEY) + : Hashing.hmacSha512(key); } @Override From 33850ae60318791abe2d2c636cc0087398d985b9 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 1 Jan 2023 10:39:48 +0100 Subject: [PATCH 314/394] version bump to 2.11.3 + changelog --- CHANGELOG.md | 6 ++++++ build.gradle | 4 ++-- fastlane/metadata/android/en-US/changelogs/42044.txt | 3 +++ 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/42044.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index fd8df902e..44f9829f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +### Version 2.11.3 + +* Fix messages getting resend when using SASL2 +* Fix black video between some devices +* Fix crash on empty passwords + ### Version 2.11.2 * Fixed regression in P2P file transfer diff --git a/build.gradle b/build.gradle index 99355e36b..cc35ecb56 100644 --- a/build.gradle +++ b/build.gradle @@ -91,8 +91,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 32 - versionCode 42043 - versionName "2.11.2" + versionCode 42044 + versionName "2.11.3" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId diff --git a/fastlane/metadata/android/en-US/changelogs/42044.txt b/fastlane/metadata/android/en-US/changelogs/42044.txt new file mode 100644 index 000000000..d84707ecf --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/42044.txt @@ -0,0 +1,3 @@ +* Fix messages getting resend when using SASL2 +* Fix black video between some devices +* Fix crash on empty passwords From 97d9cb7dd53c127b170db41d2e3ed3b4da415e8f Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 1 Jan 2023 12:05:49 +0100 Subject: [PATCH 315/394] remove work arounds for slack --- .../siacs/conversations/entities/Account.java | 8 ----- .../services/XmppConnectionService.java | 12 +++---- .../eu/siacs/conversations/xmpp/Patches.java | 3 -- .../conversations/xmpp/XmppConnection.java | 33 ------------------- 4 files changed, 4 insertions(+), 52 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 7c5f22b27..bfbe817cb 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -222,14 +222,6 @@ public void setDisplayName(String displayName) { this.displayName = displayName; } - public XmppConnection.Identity getServerIdentity() { - if (xmppConnection == null) { - return XmppConnection.Identity.UNKNOWN; - } else { - return xmppConnection.getServerIdentity(); - } - } - public Contact getSelfContact() { return getRoster().getContact(jid); } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 483016364..4981f0473 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -61,7 +61,6 @@ import org.openintents.openpgp.util.OpenPgpServiceConnection; import java.io.File; -import java.security.SecureRandom; import java.security.Security; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; @@ -154,7 +153,6 @@ import eu.siacs.conversations.xmpp.OnPresencePacketReceived; import eu.siacs.conversations.xmpp.OnStatusChanged; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; -import eu.siacs.conversations.xmpp.Patches; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.forms.Data; @@ -818,7 +816,7 @@ public int onStartCommand(Intent intent, int flags, int startId) { case Intent.ACTION_SEND: Uri uri = intent.getData(); if (uri != null) { - Log.d(Config.LOGTAG, "received uri permission for " + uri.toString()); + Log.d(Config.LOGTAG, "received uri permission for " + uri); } return START_STICKY; } @@ -1520,9 +1518,7 @@ private void sendMessage(final Message message, final boolean resend, final bool } MessagePacket packet = null; - final boolean addToConversation = (conversation.getMode() != Conversation.MODE_MULTI - || !Patches.BAD_MUC_REFLECTION.contains(account.getServerIdentity())) - && !message.edited(); + final boolean addToConversation = !message.edited(); boolean saveInDb = addToConversation; message.setStatus(Message.STATUS_WAITING); @@ -3654,7 +3650,7 @@ private void publishMucAvatar(Conversation conversation, Avatar avatar, OnAvatar } }); } else { - Log.d(Config.LOGTAG, "failed to request vcard " + response.toString()); + Log.d(Config.LOGTAG, "failed to request vcard " + response); callback.onAvatarPublicationFailed(R.string.error_publish_avatar_no_server_support); } }); @@ -4680,7 +4676,7 @@ public void publishDisplayName(Account account) { mAvatarService.clear(account); sendIqPacket(account, request, (account1, packet) -> { if (packet.getType() == IqPacket.TYPE.ERROR) { - Log.d(Config.LOGTAG, account1.getJid().asBareJid() + ": unable to modify nick name " + packet.toString()); + Log.d(Config.LOGTAG, account1.getJid().asBareJid() + ": unable to modify nick name " + packet); } }); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/Patches.java b/src/main/java/eu/siacs/conversations/xmpp/Patches.java index a5b35e811..2332da1ca 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/Patches.java +++ b/src/main/java/eu/siacs/conversations/xmpp/Patches.java @@ -8,7 +8,4 @@ public class Patches { public static final List DISCO_EXCEPTIONS = Arrays.asList( "nimbuzz.com" ); - public static final List BAD_MUC_REFLECTION = Arrays.asList( - XmppConnection.Identity.SLACK - ); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 563868d2a..abde8557f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -2545,43 +2545,10 @@ public void setInteractive(boolean interactive) { this.mInteractive = interactive; } - public Identity getServerIdentity() { - synchronized (this.disco) { - ServiceDiscoveryResult result = disco.get(account.getJid().getDomain()); - if (result == null) { - return Identity.UNKNOWN; - } - for (final ServiceDiscoveryResult.Identity id : result.getIdentities()) { - if (id.getType().equals("im") - && id.getCategory().equals("server") - && id.getName() != null) { - switch (id.getName()) { - case "Prosody": - return Identity.PROSODY; - case "ejabberd": - return Identity.EJABBERD; - case "Slack-XMPP": - return Identity.SLACK; - } - } - } - } - return Identity.UNKNOWN; - } - private IqGenerator getIqGenerator() { return mXmppConnectionService.getIqGenerator(); } - public enum Identity { - FACEBOOK, - SLACK, - EJABBERD, - PROSODY, - NIMBUZZ, - UNKNOWN - } - private class MyKeyManager implements X509KeyManager { @Override public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) { From 1000d927a79db8b96834fe8dc8da82924704797b Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 1 Jan 2023 12:20:10 +0100 Subject: [PATCH 316/394] remove work arounds for nimbuzz.com --- .../java/eu/siacs/conversations/xmpp/Patches.java | 11 ----------- .../eu/siacs/conversations/xmpp/XmppConnection.java | 11 +---------- 2 files changed, 1 insertion(+), 21 deletions(-) delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/Patches.java diff --git a/src/main/java/eu/siacs/conversations/xmpp/Patches.java b/src/main/java/eu/siacs/conversations/xmpp/Patches.java deleted file mode 100644 index 2332da1ca..000000000 --- a/src/main/java/eu/siacs/conversations/xmpp/Patches.java +++ /dev/null @@ -1,11 +0,0 @@ -package eu.siacs.conversations.xmpp; - - -import java.util.Arrays; -import java.util.List; - -public class Patches { - public static final List DISCO_EXCEPTIONS = Arrays.asList( - "nimbuzz.com" - ); -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index abde8557f..a47613895 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -1907,16 +1907,7 @@ private void sendPostBindInitialization( } Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": starting service discovery"); mPendingServiceDiscoveries.set(0); - if (!waitForDisco - || Patches.DISCO_EXCEPTIONS.contains( - account.getJid().getDomain().toEscapedString())) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": do not wait for service discovery"); - mWaitForDisco.set(false); - } else { - mWaitForDisco.set(true); - } + mWaitForDisco.set(waitForDisco); lastDiscoStarted = SystemClock.elapsedRealtime(); mXmppConnectionService.scheduleWakeUpCall( Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode()); From b1f95d2e39e64f3f066afa410c69d0704b1fdfb5 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 4 Jan 2023 10:23:20 +0100 Subject: [PATCH 317/394] integrate UnifiedPush distributor --- src/main/AndroidManifest.xml | 18 ++ .../conversations/parser/AbstractParser.java | 32 ++ .../siacs/conversations/parser/IqParser.java | 18 ++ .../persistance/UnifiedPushDatabase.java | 244 +++++++++++++++ .../services/UnifiedPushBroker.java | 277 ++++++++++++++++++ .../services/UnifiedPushDistributor.java | 152 ++++++++++ .../services/XmppConnectionService.java | 30 +- .../conversations/ui/SettingsActivity.java | 38 ++- .../eu/siacs/conversations/xml/Namespace.java | 1 + src/main/res/values/defaults.xml | 2 + src/main/res/values/strings.xml | 6 + src/main/res/xml/preferences.xml | 15 + 12 files changed, 831 insertions(+), 2 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java create mode 100644 src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java create mode 100644 src/main/java/eu/siacs/conversations/services/UnifiedPushDistributor.java diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 24265da61..c18addf27 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -67,6 +67,9 @@ + + + @@ -102,6 +105,21 @@ + + + + + + + + + + + + diff --git a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java index f4b01b7d3..5de637399 100644 --- a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java +++ b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java @@ -4,6 +4,7 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Date; import java.util.List; import java.util.Locale; @@ -86,6 +87,37 @@ public static long parseTimestamp(String timestamp) throws ParseException { return Math.min(dateFormat.parse(timestamp).getTime()+ms, System.currentTimeMillis()); } + public static long getTimestamp(final String input) throws ParseException { + if (input == null) { + throw new IllegalArgumentException("timestamp should not be null"); + } + final String timestamp = input.replace("Z", "+0000"); + final SimpleDateFormat simpleDateFormat = + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US); + final long milliseconds = getMilliseconds(timestamp); + final String formatted = + timestamp.substring(0, 19) + timestamp.substring(timestamp.length() - 5); + final Date date = simpleDateFormat.parse(formatted); + if (date == null) { + throw new IllegalArgumentException("Date was null"); + } + return date.getTime() + milliseconds; + } + + private static long getMilliseconds(final String timestamp) { + if (timestamp.length() >= 25 && timestamp.charAt(19) == '.') { + final String millis = timestamp.substring(19, timestamp.length() - 5); + try { + double fractions = Double.parseDouble("0" + millis); + return Math.round(1000 * fractions); + } catch (NumberFormatException e) { + return 0; + } + } else { + return 0; + } + } + protected void updateLastseen(final Account account, final Jid from) { final Contact contact = account.getRoster().getContact(from); contact.setLastResource(from.isBareJid() ? "" : from.getResource()); diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java index d02d69ddc..0c08c557e 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -452,6 +452,24 @@ public void onIqPacketReceived(final Account account, final IqPacket packet) { response = mXmppConnectionService.getIqGenerator().entityTimeResponse(packet); } mXmppConnectionService.sendIqPacket(account, response, null); + } else if (packet.hasChild("push", Namespace.UNIFIED_PUSH) && packet.getType() == IqPacket.TYPE.SET) { + final Jid transport = packet.getFrom(); + final Element push = packet.findChild("push", Namespace.UNIFIED_PUSH); + final boolean success = + push != null + && mXmppConnectionService.processUnifiedPushMessage( + account, transport, push); + final IqPacket response; + if (success) { + response = packet.generateResponse(IqPacket.TYPE.RESULT); + } else { + response = packet.generateResponse(IqPacket.TYPE.ERROR); + final Element error = response.addChild("error"); + error.setAttribute("type", "cancel"); + error.setAttribute("code", "404"); + error.addChild("item-not-found", "urn:ietf:params:xml:ns:xmpp-stanzas"); + } + mXmppConnectionService.sendIqPacket(account, response, null); } else { if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) { final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR); diff --git a/src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java b/src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java new file mode 100644 index 000000000..9b17406f7 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java @@ -0,0 +1,244 @@ +package eu.siacs.conversations.persistance; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; + +import androidx.annotation.Nullable; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; + +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +import eu.siacs.conversations.Config; + +public class UnifiedPushDatabase extends SQLiteOpenHelper { + private static final String DATABASE_NAME = "unified-push-distributor"; + private static final int DATABASE_VERSION = 1; + + private static UnifiedPushDatabase instance; + + public static UnifiedPushDatabase getInstance(final Context context) { + synchronized (UnifiedPushDatabase.class) { + if (instance == null) { + instance = new UnifiedPushDatabase(context.getApplicationContext()); + } + return instance; + } + } + + private UnifiedPushDatabase(@Nullable Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(final SQLiteDatabase sqLiteDatabase) { + sqLiteDatabase.execSQL( + "CREATE TABLE push (account TEXT, transport TEXT, application TEXT NOT NULL, instance TEXT NOT NULL UNIQUE, endpoint TEXT, expiration NUMBER DEFAULT 0)"); + } + + public boolean register(final String application, final String instance) { + final SQLiteDatabase sqLiteDatabase = getWritableDatabase(); + sqLiteDatabase.beginTransaction(); + final Optional existingApplication; + try (final Cursor cursor = + sqLiteDatabase.query( + "push", + new String[] {"application"}, + "instance=?", + new String[] {instance}, + null, + null, + null)) { + if (cursor != null && cursor.moveToFirst()) { + existingApplication = Optional.of(cursor.getString(0)); + } else { + existingApplication = Optional.absent(); + } + } + if (existingApplication.isPresent()) { + sqLiteDatabase.setTransactionSuccessful(); + sqLiteDatabase.endTransaction(); + return application.equals(existingApplication.get()); + } + final ContentValues contentValues = new ContentValues(); + contentValues.put("application", application); + contentValues.put("instance", instance); + contentValues.put("expiration", 0); + final long inserted = sqLiteDatabase.insert("push", null, contentValues); + if (inserted > 0) { + Log.d(Config.LOGTAG, "inserted new application/instance tuple into unified push db"); + } + sqLiteDatabase.setTransactionSuccessful(); + sqLiteDatabase.endTransaction(); + return true; + } + + public List getRenewals(final String account, final String transport) { + final ImmutableList.Builder renewalBuilder = ImmutableList.builder(); + final SQLiteDatabase sqLiteDatabase = getReadableDatabase(); + try (final Cursor cursor = + sqLiteDatabase.query( + "push", + new String[] {"application", "instance"}, + "account <> ? OR transport <> ? OR expiration < " + + System.currentTimeMillis(), + new String[] {account, transport}, + null, + null, + null)) { + while (cursor != null && cursor.moveToNext()) { + renewalBuilder.add( + new PushTarget( + cursor.getString(cursor.getColumnIndexOrThrow("application")), + cursor.getString(cursor.getColumnIndexOrThrow("instance")))); + } + } + return renewalBuilder.build(); + } + + public ApplicationEndpoint getEndpoint( + final String account, final String transport, final String instance) { + final SQLiteDatabase sqLiteDatabase = getReadableDatabase(); + try (final Cursor cursor = + sqLiteDatabase.query( + "push", + new String[] {"application", "endpoint"}, + "account = ? AND transport = ? AND instance = ? ", + new String[] {account, transport, instance}, + null, + null, + null)) { + if (cursor != null && cursor.moveToFirst()) { + return new ApplicationEndpoint( + cursor.getString(cursor.getColumnIndexOrThrow("application")), + cursor.getString(cursor.getColumnIndexOrThrow("endpoint"))); + } + } + return null; + } + + @Override + public void onUpgrade( + final SQLiteDatabase sqLiteDatabase, final int oldVersion, final int newVersion) {} + + public boolean updateEndpoint( + final String instance, + final String account, + final String transport, + final String endpoint, + final long expiration) { + final SQLiteDatabase sqLiteDatabase = getWritableDatabase(); + sqLiteDatabase.beginTransaction(); + final String existingEndpoint; + try (final Cursor cursor = + sqLiteDatabase.query( + "push", + new String[] {"endpoint"}, + "instance=?", + new String[] {instance}, + null, + null, + null)) { + if (cursor != null && cursor.moveToFirst()) { + existingEndpoint = cursor.getString(0); + } else { + existingEndpoint = null; + } + } + final ContentValues contentValues = new ContentValues(); + contentValues.put("account", account); + contentValues.put("transport", transport); + contentValues.put("endpoint", endpoint); + contentValues.put("expiration", expiration); + sqLiteDatabase.update("push", contentValues, "instance=?", new String[] {instance}); + sqLiteDatabase.setTransactionSuccessful(); + sqLiteDatabase.endTransaction(); + return !endpoint.equals(existingEndpoint); + } + + public List getPushTargets(final String account, final String transport) { + final ImmutableList.Builder renewalBuilder = ImmutableList.builder(); + final SQLiteDatabase sqLiteDatabase = getReadableDatabase(); + try (final Cursor cursor = + sqLiteDatabase.query( + "push", + new String[] {"application", "instance"}, + "account = ?", + new String[] {account}, + null, + null, + null)) { + while (cursor != null && cursor.moveToNext()) { + renewalBuilder.add( + new PushTarget( + cursor.getString(cursor.getColumnIndexOrThrow("application")), + cursor.getString(cursor.getColumnIndexOrThrow("instance")))); + } + } + return renewalBuilder.build(); + } + + public boolean deleteInstance(final String instance) { + final SQLiteDatabase sqLiteDatabase = getReadableDatabase(); + final int rows = sqLiteDatabase.delete("push", "instance=?", new String[] {instance}); + return rows >= 1; + } + + public boolean deleteApplication(final String application) { + final SQLiteDatabase sqLiteDatabase = getReadableDatabase(); + final int rows = sqLiteDatabase.delete("push", "application=?", new String[] {application}); + return rows >= 1; + } + + public static class ApplicationEndpoint { + public final String application; + public final String endpoint; + + public ApplicationEndpoint(String application, String endpoint) { + this.application = application; + this.endpoint = endpoint; + } + } + + public static class PushTarget { + public final String application; + public final String instance; + + public PushTarget(final String application, final String instance) { + this.application = application; + this.instance = instance; + } + + @NotNull + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("application", application) + .add("instance", instance) + .toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PushTarget that = (PushTarget) o; + return Objects.equal(application, that.application) + && Objects.equal(instance, that.instance); + } + + @Override + public int hashCode() { + return Objects.hashCode(application, instance); + } + } +} diff --git a/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java new file mode 100644 index 000000000..101a09fc3 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java @@ -0,0 +1,277 @@ +package eu.siacs.conversations.services; + +import android.content.ComponentName; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.preference.PreferenceManager; +import android.util.Log; + +import com.google.common.base.Optional; +import com.google.common.base.Strings; +import com.google.common.collect.Iterables; +import com.google.common.io.BaseEncoding; + +import java.nio.charset.StandardCharsets; +import java.text.ParseException; +import java.util.List; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.parser.AbstractParser; +import eu.siacs.conversations.persistance.UnifiedPushDatabase; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +public class UnifiedPushBroker { + + private final XmppConnectionService service; + + public UnifiedPushBroker(final XmppConnectionService xmppConnectionService) { + this.service = xmppConnectionService; + } + + public Optional renewUnifiedPushEndpoints() { + final Optional transportOptional = getTransport(); + if (transportOptional.isPresent()) { + renewUnifiedEndpoint(transportOptional.get()); + } else { + Log.d(Config.LOGTAG, "skipping UnifiedPush endpoint renewal. No transport selected"); + } + return transportOptional; + } + + private void renewUnifiedEndpoint(final Transport transport) { + final Account account = transport.account; + final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service); + final List renewals = + unifiedPushDatabase.getRenewals( + account.getUuid(), transport.transport.toEscapedString()); + for (final UnifiedPushDatabase.PushTarget renewal : renewals) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": try to renew UnifiedPush " + renewal); + final String hashedApplication = + UnifiedPushDistributor.hash(account.getUuid(), renewal.application); + final String hashedInstance = + UnifiedPushDistributor.hash(account.getUuid(), renewal.instance); + final IqPacket registration = new IqPacket(IqPacket.TYPE.SET); + registration.setTo(transport.transport); + final Element register = registration.addChild("register", Namespace.UNIFIED_PUSH); + register.setAttribute("application", hashedApplication); + register.setAttribute("instance", hashedInstance); + this.service.sendIqPacket( + account, + registration, + (a, response) -> processRegistration(transport, renewal, response)); + } + } + + private void processRegistration( + final Transport transport, + final UnifiedPushDatabase.PushTarget renewal, + final IqPacket response) { + if (response.getType() == IqPacket.TYPE.RESULT) { + final Element registered = response.findChild("registered", Namespace.UNIFIED_PUSH); + if (registered == null) { + return; + } + final String endpoint = registered.getAttribute("endpoint"); + if (Strings.isNullOrEmpty(endpoint)) { + Log.w(Config.LOGTAG, "endpoint was null in up registration"); + return; + } + final long expiration; + try { + expiration = AbstractParser.getTimestamp(registered.getAttribute("expiration")); + } catch (final IllegalArgumentException | ParseException e) { + Log.d(Config.LOGTAG, "could not parse expiration", e); + return; + } + renewUnifiedPushEndpoint(transport, renewal, endpoint, expiration); + } + } + + private void renewUnifiedPushEndpoint( + final Transport transport, + final UnifiedPushDatabase.PushTarget renewal, + final String endpoint, + final long expiration) { + Log.d(Config.LOGTAG, "registered endpoint " + endpoint + " expiration=" + expiration); + final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service); + final boolean modified = + unifiedPushDatabase.updateEndpoint( + renewal.instance, + transport.account.getUuid(), + transport.transport.toEscapedString(), + endpoint, + expiration); + if (modified) { + Log.d( + Config.LOGTAG, + "endpoint for " + + renewal.application + + "/" + + renewal.instance + + " was updated to " + + endpoint); + broadcastEndpoint( + renewal.instance, + new UnifiedPushDatabase.ApplicationEndpoint(renewal.application, endpoint)); + } + } + + public boolean reconfigurePushDistributor() { + final boolean enabled = getTransport().isPresent(); + setUnifiedPushDistributorEnabled(enabled); + return enabled; + } + + private void setUnifiedPushDistributorEnabled(final boolean enabled) { + final PackageManager packageManager = service.getPackageManager(); + final ComponentName componentName = + new ComponentName(service, UnifiedPushDistributor.class); + if (enabled) { + packageManager.setComponentEnabledSetting( + componentName, + PackageManager.COMPONENT_ENABLED_STATE_ENABLED, + PackageManager.DONT_KILL_APP); + Log.d(Config.LOGTAG, "UnifiedPushDistributor has been enabled"); + } else { + packageManager.setComponentEnabledSetting( + componentName, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP); + Log.d(Config.LOGTAG, "UnifiedPushDistributor has been disabled"); + } + } + + public boolean processPushMessage( + final Account account, final Jid transport, final Element push) { + final String instance = push.getAttribute("instance"); + final String application = push.getAttribute("application"); + if (Strings.isNullOrEmpty(instance) || Strings.isNullOrEmpty(application)) { + return false; + } + final String content = push.getContent(); + final byte[] payload; + if (Strings.isNullOrEmpty(content)) { + payload = new byte[0]; + } else if (BaseEncoding.base64().canDecode(content)) { + payload = BaseEncoding.base64().decode(content); + } else { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": received invalid unified push payload"); + return false; + } + final Optional pushTarget = + getPushTarget(account, transport, application, instance); + if (pushTarget.isPresent()) { + final UnifiedPushDatabase.PushTarget target = pushTarget.get(); + // TODO check if app is still installed? + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": broadcasting a " + + payload.length + + " bytes push message to " + + target.application); + broadcastPushMessage(target, payload); + return true; + } else { + Log.d(Config.LOGTAG, "could not find application for push"); + return false; + } + } + + public Optional getTransport() { + final SharedPreferences sharedPreferences = + PreferenceManager.getDefaultSharedPreferences(service.getApplicationContext()); + final String accountPreference = + sharedPreferences.getString(UnifiedPushDistributor.PREFERENCE_ACCOUNT, "none"); + final String pushServerPreference = + sharedPreferences.getString( + UnifiedPushDistributor.PREFERENCE_PUSH_SERVER, + service.getString(R.string.default_push_server)); + if (Strings.isNullOrEmpty(accountPreference) + || "none".equalsIgnoreCase(accountPreference) + || Strings.nullToEmpty(pushServerPreference).trim().isEmpty()) { + return Optional.absent(); + } + final Jid transport; + final Jid jid; + try { + transport = Jid.ofEscaped(Strings.nullToEmpty(pushServerPreference).trim()); + jid = Jid.ofEscaped(Strings.nullToEmpty(accountPreference).trim()); + } catch (final IllegalArgumentException e) { + return Optional.absent(); + } + final Account account = service.findAccountByJid(jid); + if (account == null) { + return Optional.absent(); + } + return Optional.of(new Transport(account, transport)); + } + + private Optional getPushTarget( + final Account account, + final Jid transport, + final String application, + final String instance) { + final String uuid = account.getUuid(); + final List pushTargets = + UnifiedPushDatabase.getInstance(service) + .getPushTargets(uuid, transport.toEscapedString()); + return Iterables.tryFind( + pushTargets, + pt -> + UnifiedPushDistributor.hash(uuid, pt.application).equals(application) + && UnifiedPushDistributor.hash(uuid, pt.instance).equals(instance)); + } + + private void broadcastPushMessage( + final UnifiedPushDatabase.PushTarget target, final byte[] payload) { + final Intent updateIntent = new Intent(UnifiedPushDistributor.ACTION_MESSAGE); + updateIntent.setPackage(target.application); + updateIntent.putExtra("token", target.instance); + updateIntent.putExtra("bytesMessage", payload); + updateIntent.putExtra("message", new String(payload, StandardCharsets.UTF_8)); + service.sendBroadcast(updateIntent); + } + + private void broadcastEndpoint( + final String instance, final UnifiedPushDatabase.ApplicationEndpoint endpoint) { + Log.d(Config.LOGTAG, "broadcasting endpoint to " + endpoint.application); + final Intent updateIntent = new Intent(UnifiedPushDistributor.ACTION_NEW_ENDPOINT); + updateIntent.setPackage(endpoint.application); + updateIntent.putExtra("token", instance); + updateIntent.putExtra("endpoint", endpoint.endpoint); + service.sendBroadcast(updateIntent); + } + + public void rebroadcastEndpoint(final String instance, final Transport transport) { + final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service); + final UnifiedPushDatabase.ApplicationEndpoint endpoint = + unifiedPushDatabase.getEndpoint( + transport.account.getUuid(), + transport.transport.toEscapedString(), + instance); + if (endpoint != null) { + broadcastEndpoint(instance, endpoint); + } + } + + public static class Transport { + public final Account account; + public final Jid transport; + + public Transport(Account account, Jid transport) { + this.account = account; + this.transport = transport; + } + } +} diff --git a/src/main/java/eu/siacs/conversations/services/UnifiedPushDistributor.java b/src/main/java/eu/siacs/conversations/services/UnifiedPushDistributor.java new file mode 100644 index 000000000..64c16dbcd --- /dev/null +++ b/src/main/java/eu/siacs/conversations/services/UnifiedPushDistributor.java @@ -0,0 +1,152 @@ +package eu.siacs.conversations.services; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.util.Log; + +import com.google.common.base.Charsets; +import com.google.common.base.Joiner; +import com.google.common.base.Strings; +import com.google.common.collect.Lists; +import com.google.common.hash.Hashing; +import com.google.common.io.BaseEncoding; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.persistance.UnifiedPushDatabase; +import eu.siacs.conversations.utils.Compatibility; + +public class UnifiedPushDistributor extends BroadcastReceiver { + + public static final String ACTION_REGISTER = "org.unifiedpush.android.distributor.REGISTER"; + public static final String ACTION_UNREGISTER = "org.unifiedpush.android.distributor.UNREGISTER"; + public static final String ACTION_BYTE_MESSAGE = + "org.unifiedpush.android.distributor.feature.BYTES_MESSAGE"; + public static final String ACTION_REGISTRATION_FAILED = + "org.unifiedpush.android.connector.REGISTRATION_FAILED"; + public static final String ACTION_MESSAGE = "org.unifiedpush.android.connector.MESSAGE"; + public static final String ACTION_NEW_ENDPOINT = + "org.unifiedpush.android.connector.NEW_ENDPOINT"; + + public static final String PREFERENCE_ACCOUNT = "up_push_account"; + public static final String PREFERENCE_PUSH_SERVER = "up_push_server"; + + public static final List PREFERENCES = + Arrays.asList(PREFERENCE_ACCOUNT, PREFERENCE_PUSH_SERVER); + + @Override + public void onReceive(final Context context, final Intent intent) { + if (intent == null) { + return; + } + final String action = intent.getAction(); + final String application = intent.getStringExtra("application"); + final String instance = intent.getStringExtra("token"); + final List features = intent.getStringArrayListExtra("features"); + switch (Strings.nullToEmpty(action)) { + case ACTION_REGISTER: + register(context, application, instance, features); + break; + case ACTION_UNREGISTER: + unregister(context, instance); + break; + case Intent.ACTION_PACKAGE_FULLY_REMOVED: + unregisterApplication(context, intent.getData()); + break; + default: + Log.d(Config.LOGTAG, "UnifiedPushDistributor received unknown action " + action); + break; + } + } + + private void register( + final Context context, + final String application, + final String instance, + final Collection features) { + if (Strings.isNullOrEmpty(application) || Strings.isNullOrEmpty(instance)) { + Log.w(Config.LOGTAG, "ignoring invalid UnifiedPush registration"); + return; + } + final List receivers = getBroadcastReceivers(context, application); + if (receivers.contains(application)) { + final boolean byteMessage = features != null && features.contains(ACTION_BYTE_MESSAGE); + Log.d( + Config.LOGTAG, + "received up registration from " + + application + + "/" + + instance + + " features: " + + features); + if (UnifiedPushDatabase.getInstance(context).register(application, instance)) { + Log.d( + Config.LOGTAG, + "successfully created UnifiedPush entry. waking up XmppConnectionService"); + final Intent serviceIntent = new Intent(context, XmppConnectionService.class); + serviceIntent.setAction(XmppConnectionService.ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS); + serviceIntent.putExtra("instance", instance); + Compatibility.startService(context, serviceIntent); + } else { + Log.d(Config.LOGTAG, "not successful. sending error message back to application"); + final Intent registrationFailed = new Intent(ACTION_REGISTRATION_FAILED); + registrationFailed.setPackage(application); + registrationFailed.putExtra("token", instance); + context.sendBroadcast(registrationFailed); + } + } else { + Log.d( + Config.LOGTAG, + "ignoring invalid UnifiedPush registration. Unknown application " + + application); + } + } + + private List getBroadcastReceivers(final Context context, final String application) { + final Intent messageIntent = new Intent(ACTION_MESSAGE); + messageIntent.setPackage(application); + final List resolveInfo = + context.getPackageManager().queryBroadcastReceivers(messageIntent, 0); + return Lists.transform( + resolveInfo, ri -> ri.activityInfo == null ? null : ri.activityInfo.packageName); + } + + private void unregister(final Context context, final String instance) { + if (Strings.isNullOrEmpty(instance)) { + Log.w(Config.LOGTAG, "ignoring invalid UnifiedPush un-registration"); + return; + } + final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(context); + if (unifiedPushDatabase.deleteInstance(instance)) { + Log.d(Config.LOGTAG, "successfully removed " + instance + " from UnifiedPush"); + } + } + + private void unregisterApplication(final Context context, final Uri uri) { + if (uri != null && "package".equalsIgnoreCase(uri.getScheme())) { + final String application = uri.getSchemeSpecificPart(); + if (Strings.isNullOrEmpty(application)) { + return; + } + Log.d(Config.LOGTAG, "app " + application + " has been removed from the system"); + final UnifiedPushDatabase database = UnifiedPushDatabase.getInstance(context); + if (database.deleteApplication(application)) { + Log.d(Config.LOGTAG, "successfully removed " + application + " from UnifiedPush"); + } + } + } + + public static String hash(String... components) { + return BaseEncoding.base64() + .encode( + Hashing.sha256() + .hashString(Joiner.on('\0').join(components), Charsets.UTF_8) + .asBytes()); + } +} diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 4981f0473..22e7b38e5 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -53,6 +53,7 @@ import androidx.core.content.ContextCompat; import com.google.common.base.Objects; +import com.google.common.base.Optional; import com.google.common.base.Strings; import org.conscrypt.Conscrypt; @@ -64,6 +65,7 @@ import java.security.Security; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -117,6 +119,7 @@ import eu.siacs.conversations.parser.PresenceParser; import eu.siacs.conversations.persistance.DatabaseBackend; import eu.siacs.conversations.persistance.FileBackend; +import eu.siacs.conversations.persistance.UnifiedPushDatabase; import eu.siacs.conversations.ui.ChooseAccountForProfilePictureActivity; import eu.siacs.conversations.ui.RtpSessionActivity; import eu.siacs.conversations.ui.SettingsActivity; @@ -124,6 +127,7 @@ import eu.siacs.conversations.ui.interfaces.OnAvatarPublication; import eu.siacs.conversations.ui.interfaces.OnMediaLoaded; import eu.siacs.conversations.ui.interfaces.OnSearchResultsAvailable; +import eu.siacs.conversations.utils.AccountUtils; import eu.siacs.conversations.utils.Compatibility; import eu.siacs.conversations.utils.ConversationsFileObserver; import eu.siacs.conversations.utils.CryptoHelper; @@ -185,6 +189,7 @@ public class XmppConnectionService extends Service { public static final String ACTION_END_CALL = "end_call"; public static final String ACTION_PROVISION_ACCOUNT = "provision_account"; private static final String ACTION_POST_CONNECTIVITY_CHANGE = "eu.siacs.conversations.POST_CONNECTIVITY_CHANGE"; + public static final String ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS = "eu.siacs.conversations.UNIFIED_PUSH_RENEW"; private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp"; @@ -217,6 +222,7 @@ public class XmppConnectionService extends Service { private final FileBackend fileBackend = new FileBackend(this); private MemorizingTrustManager mMemorizingTrustManager; private final NotificationService mNotificationService = new NotificationService(this); + private final UnifiedPushBroker unifiedPushBroker = new UnifiedPushBroker(this); private final ChannelDiscoveryService mChannelDiscoveryService = new ChannelDiscoveryService(this); private final ShortcutService mShortcutService = new ShortcutService(this); private final AtomicBoolean mInitialAddressbookSyncCompleted = new AtomicBoolean(false); @@ -804,6 +810,13 @@ public int onStartCommand(Intent intent, int flags, int startId) { case ACTION_FCM_TOKEN_REFRESH: refreshAllFcmTokens(); break; + case ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS: + final String instance = intent.getStringExtra("instance"); + final Optional transport = renewUnifiedPushEndpoints(); + if (instance != null && transport.isPresent()) { + unifiedPushBroker.rebroadcastEndpoint(instance, transport.get()); + } + break; case ACTION_IDLE_PING: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { scheduleNextIdlePing(); @@ -933,6 +946,10 @@ private boolean processAccountState(Account account, boolean interactive, boolea return pingNow; } + public boolean processUnifiedPushMessage(final Account account, final Jid transport, final Element push) { + return unifiedPushBroker.processPushMessage(account, transport, push); + } + public void reinitializeMuclumbusService() { mChannelDiscoveryService.initializeMuclumbusService(); } @@ -1167,6 +1184,7 @@ protected int sizeOf(final String key, final Bitmap bitmap) { editor.putBoolean(EventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts).apply(); editor.apply(); toggleSetProfilePictureActivity(hasEnabledAccounts); + reconfigurePushDistributor(); restoreFromDatabase(); @@ -2334,10 +2352,18 @@ private void toggleSetProfilePictureActivity(final boolean enabled) { final int targetState = enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; getPackageManager().setComponentEnabledSetting(name, targetState, PackageManager.DONT_KILL_APP); } catch (IllegalStateException e) { - Log.d(Config.LOGTAG, "unable to toggle profile picture actvitiy"); + Log.d(Config.LOGTAG, "unable to toggle profile picture activity"); } } + public boolean reconfigurePushDistributor() { + return this.unifiedPushBroker.reconfigurePushDistributor(); + } + + public Optional renewUnifiedPushEndpoints() { + return this.unifiedPushBroker.renewUnifiedPushEndpoints(); + } + private void provisionAccount(final String address, final String password) { final Jid jid = Jid.ofEscaped(address); final Account account = new Account(jid, password); @@ -4499,6 +4525,8 @@ private void refreshAllFcmTokens() { } } + + private void sendOfflinePresence(final Account account) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending offline presence"); sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account)); diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java index 07c8a55db..2b2f8110a 100644 --- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java @@ -24,6 +24,8 @@ import androidx.core.content.ContextCompat; import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; import java.io.File; import java.security.KeyStoreException; @@ -40,6 +42,7 @@ import eu.siacs.conversations.services.ExportBackupService; import eu.siacs.conversations.services.MemorizingTrustManager; import eu.siacs.conversations.services.QuickConversationsService; +import eu.siacs.conversations.services.UnifiedPushDistributor; import eu.siacs.conversations.ui.util.SettingsUtils; import eu.siacs.conversations.ui.util.StyledAttributes; import eu.siacs.conversations.utils.GeoHelper; @@ -88,7 +91,36 @@ protected void onCreate(Bundle savedInstanceState) { } @Override - void onBackendConnected() {} + void onBackendConnected() { + final Preference accountPreference = + mSettingsFragment.findPreference(UnifiedPushDistributor.PREFERENCE_ACCOUNT); + reconfigureUpAccountPreference(accountPreference); + } + + private void reconfigureUpAccountPreference(final Preference preference) { + final ListPreference listPreference; + if (preference instanceof ListPreference) { + listPreference = (ListPreference) preference; + } else { + return; + } + final List accounts = + ImmutableList.copyOf( + Lists.transform( + xmppConnectionService.getAccounts(), + a -> a.getJid().asBareJid().toEscapedString())); + final ImmutableList.Builder entries = new ImmutableList.Builder<>(); + final ImmutableList.Builder entryValues = new ImmutableList.Builder<>(); + entries.add(getString(R.string.no_account_deactivated)); + entryValues.add("none"); + entries.addAll(accounts); + entryValues.addAll(accounts); + listPreference.setEntries(entries.build().toArray(new CharSequence[0])); + listPreference.setEntryValues(entryValues.build().toArray(new CharSequence[0])); + if (!accounts.contains(listPreference.getValue())) { + listPreference.setValue("none"); + } + } @Override public void onStart() { @@ -472,6 +504,10 @@ public void onSharedPreferenceChanged(SharedPreferences preferences, String name } } else if (name.equals(PREVENT_SCREENSHOTS)) { SettingsUtils.applyScreenshotPreventionSetting(this); + } else if (UnifiedPushDistributor.PREFERENCES.contains(name)) { + if (xmppConnectionService.reconfigurePushDistributor()) { + xmppConnectionService.renewUnifiedPushEndpoints(); + } } } diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 55f45c6b5..b614251bd 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -65,4 +65,5 @@ public final class Namespace { public static final String PARS = "urn:xmpp:pars:0"; public static final String EASY_ONBOARDING_INVITE = "urn:xmpp:invite#invite"; public static final String OMEMO_DTLS_SRTP_VERIFICATION = "http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification"; + public static final String UNIFIED_PUSH = "http://gultsch.de/xmpp/drafts/unified-push"; } diff --git a/src/main/res/values/defaults.xml b/src/main/res/values/defaults.xml index 60085d0f9..288e4ae74 100644 --- a/src/main/res/values/defaults.xml +++ b/src/main/res/values/defaults.xml @@ -45,4 +45,6 @@ 360 JABBER_NETWORK false + up.conversations.im + none diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 3fc93601f..e5399c15e 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -999,5 +999,11 @@ Calls are disabled when using Tor Switch to video Reject switch to video request + UnifiedPush Distributor + XMPP Account + The account through which push messages will be received. + Push Server + A user-chosen push server to relay push messages via XMPP to your device. + None (deactivated) diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml index b46155836..b9ea7e871 100644 --- a/src/main/res/xml/preferences.xml +++ b/src/main/res/xml/preferences.xml @@ -206,6 +206,21 @@ android:summary="@string/pref_create_backup_summary" android:title="@string/pref_create_backup" /> + + + + + From 4ee5c167bedd4e1105c4a7b52536c8ab0eb6e37d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 4 Jan 2023 20:59:08 +0100 Subject: [PATCH 318/394] do not attempt endpoint renewal when account is disabled. renew on bind --- .../persistance/UnifiedPushDatabase.java | 8 +++++--- .../services/UnifiedPushBroker.java | 20 ++++++++++++++++++- .../services/XmppConnectionService.java | 1 + 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java b/src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java index 9b17406f7..e49db54d3 100644 --- a/src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java +++ b/src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java @@ -84,13 +84,14 @@ public boolean register(final String application, final String instance) { public List getRenewals(final String account, final String transport) { final ImmutableList.Builder renewalBuilder = ImmutableList.builder(); + // TODO use a date somewhat in the future to account for period renewal triggers + final long expiration = System.currentTimeMillis(); final SQLiteDatabase sqLiteDatabase = getReadableDatabase(); try (final Cursor cursor = sqLiteDatabase.query( "push", new String[] {"application", "instance"}, - "account <> ? OR transport <> ? OR expiration < " - + System.currentTimeMillis(), + "account <> ? OR transport <> ? OR expiration < " + expiration, new String[] {account, transport}, null, null, @@ -112,7 +113,8 @@ public ApplicationEndpoint getEndpoint( sqLiteDatabase.query( "push", new String[] {"application", "endpoint"}, - "account = ? AND transport = ? AND instance = ? ", + "account = ? AND transport = ? AND instance = ? AND endpoint IS NOT NULL AND expiration >= " + + System.currentTimeMillis(), new String[] {account, transport, instance}, null, null, diff --git a/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java index 101a09fc3..6e675cc27 100644 --- a/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java +++ b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java @@ -34,10 +34,28 @@ public UnifiedPushBroker(final XmppConnectionService xmppConnectionService) { this.service = xmppConnectionService; } + public void renewUnifiedPushEndpointsOnBind(final Account account) { + final Optional transport = getTransport(); + if (transport.isPresent()) { + final Account transportAccount = transport.get().account; + if (transportAccount != null && transportAccount.getUuid().equals(account.getUuid())) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": trigger endpoint renewal on bind"); + renewUnifiedEndpoint(transport.get()); + } + } + } + public Optional renewUnifiedPushEndpoints() { final Optional transportOptional = getTransport(); if (transportOptional.isPresent()) { - renewUnifiedEndpoint(transportOptional.get()); + final Transport transport = transportOptional.get(); + if (transport.account.isEnabled()) { + renewUnifiedEndpoint(transportOptional.get()); + } else { + Log.d(Config.LOGTAG, "skipping UnifiedPush endpoint renewal. Account is disabled"); + } } else { Log.d(Config.LOGTAG, "skipping UnifiedPush endpoint renewal. No transport selected"); } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 22e7b38e5..d3722053f 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -381,6 +381,7 @@ public void onBind(final Account account) { connectMultiModeConversations(account); syncDirtyContacts(account); + unifiedPushBroker.renewUnifiedPushEndpointsOnBind(account); } }; private final AtomicLong mLastExpiryRun = new AtomicLong(0); From 7af0dda5afa8dbf4f7561409504eee73ba86adcf Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 6 Jan 2023 12:40:51 +0100 Subject: [PATCH 319/394] make short store description shorter --- fastlane/metadata/android/en-US/short_description.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt index 4b41cb27b..7a2205c2b 100644 --- a/fastlane/metadata/android/en-US/short_description.txt +++ b/fastlane/metadata/android/en-US/short_description.txt @@ -1 +1 @@ -An encrypted, user friendly XMPP instant messaging client optimized for mobile \ No newline at end of file +Encrypted, easy-to-use XMPP instant messenger for your mobile device From 1e0904a48db4fd85c02381c07062042cba1a765a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 6 Jan 2023 12:41:26 +0100 Subject: [PATCH 320/394] use less entropy in SASL2 device id --- .../conversations/utils/AccountUtils.java | 18 ++++++++++++++++++ .../conversations/xmpp/XmppConnection.java | 3 ++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/utils/AccountUtils.java b/src/main/java/eu/siacs/conversations/utils/AccountUtils.java index 8f0453218..b8f4855d0 100644 --- a/src/main/java/eu/siacs/conversations/utils/AccountUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/AccountUtils.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.UUID; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; @@ -34,6 +35,23 @@ public static boolean hasEnabledAccounts(final XmppConnectionService service) { return false; } + public static String publicDeviceId(final Account account) { + final UUID uuid; + try { + uuid = UUID.fromString(account.getUuid()); + } catch (final IllegalArgumentException e) { + return account.getUuid(); + } + final UUID publicDeviceId = getUuid(uuid.getLeastSignificantBits(), uuid.getLeastSignificantBits()); + return publicDeviceId.toString(); + } + + protected static UUID getUuid(final long msb, final long lsb) { + final long msb0 = (msb & 0xffffffffffff0fffL) | 4; // set version + final long lsb0 = (lsb & 0x3fffffffffffffffL) | 0x8000000000000000L; // set variant + return new UUID(msb0, lsb0); + } + public static List getEnabledAccounts(final XmppConnectionService service) { ArrayList accounts = new ArrayList<>(); for (Account account : service.getAccounts()) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index a47613895..010cb76c7 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -77,6 +77,7 @@ import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.NotificationService; import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.AccountUtils; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.Patterns; import eu.siacs.conversations.utils.PhoneHelper; @@ -1534,7 +1535,7 @@ private Element generateAuthenticationRequest( authenticate.addChild("initial-response").setContent(firstMessage); } final Element userAgent = authenticate.addChild("user-agent"); - userAgent.setAttribute("id", account.getUuid()); + userAgent.setAttribute("id", AccountUtils.publicDeviceId(account)); userAgent .addChild("software") .setContent(mXmppConnectionService.getString(R.string.app_name)); From 0e10ae387a775eca63c25b61474cfe99ad973d21 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 6 Jan 2023 15:44:39 +0100 Subject: [PATCH 321/394] periodically renew endpoints --- .../persistance/UnifiedPushDatabase.java | 7 ++--- .../services/UnifiedPushBroker.java | 26 +++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java b/src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java index e49db54d3..1b1adc92f 100644 --- a/src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java +++ b/src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java @@ -19,6 +19,7 @@ import java.util.List; import eu.siacs.conversations.Config; +import eu.siacs.conversations.services.UnifiedPushBroker; public class UnifiedPushDatabase extends SQLiteOpenHelper { private static final String DATABASE_NAME = "unified-push-distributor"; @@ -84,8 +85,7 @@ public boolean register(final String application, final String instance) { public List getRenewals(final String account, final String transport) { final ImmutableList.Builder renewalBuilder = ImmutableList.builder(); - // TODO use a date somewhat in the future to account for period renewal triggers - final long expiration = System.currentTimeMillis(); + final long expiration = System.currentTimeMillis() + UnifiedPushBroker.TIME_TO_RENEW; final SQLiteDatabase sqLiteDatabase = getReadableDatabase(); try (final Cursor cursor = sqLiteDatabase.query( @@ -108,13 +108,14 @@ public List getRenewals(final String account, final String transport public ApplicationEndpoint getEndpoint( final String account, final String transport, final String instance) { + final long expiration = System.currentTimeMillis() + UnifiedPushBroker.TIME_TO_RENEW; final SQLiteDatabase sqLiteDatabase = getReadableDatabase(); try (final Cursor cursor = sqLiteDatabase.query( "push", new String[] {"application", "endpoint"}, "account = ? AND transport = ? AND instance = ? AND endpoint IS NOT NULL AND expiration >= " - + System.currentTimeMillis(), + + expiration, new String[] {account, transport, instance}, null, null, diff --git a/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java index 6e675cc27..5c6f1bbc8 100644 --- a/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java +++ b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java @@ -15,6 +15,9 @@ import java.nio.charset.StandardCharsets; import java.text.ParseException; import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; @@ -28,10 +31,24 @@ public class UnifiedPushBroker { + // time to expiration before a renewal attempt is made (24 hours) + public static final long TIME_TO_RENEW = 86_400_000L; + + // interval for the 'cron tob' that attempts renewals for everything that expires is lass than + // `TIME_TO_RENEW` + public static final long RENEWAL_INTERVAL = 3_600_000L; + + private static final ScheduledExecutorService SCHEDULER = Executors.newScheduledThreadPool(1); + private final XmppConnectionService service; public UnifiedPushBroker(final XmppConnectionService xmppConnectionService) { this.service = xmppConnectionService; + SCHEDULER.scheduleAtFixedRate( + this::renewUnifiedPushEndpoints, + RENEWAL_INTERVAL, + RENEWAL_INTERVAL, + TimeUnit.MILLISECONDS); } public void renewUnifiedPushEndpointsOnBind(final Account account) { @@ -68,6 +85,13 @@ private void renewUnifiedEndpoint(final Transport transport) { final List renewals = unifiedPushDatabase.getRenewals( account.getUuid(), transport.transport.toEscapedString()); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": " + + renewals.size() + + " UnifiedPush endpoints scheduled for renewal on " + + transport.transport); for (final UnifiedPushDatabase.PushTarget renewal : renewals) { Log.d( Config.LOGTAG, @@ -110,6 +134,8 @@ private void processRegistration( return; } renewUnifiedPushEndpoint(transport, renewal, endpoint, expiration); + } else { + Log.d(Config.LOGTAG, "could not register UP endpoint " + response.getErrorCondition()); } } From b7c7c40b946bc21086e55db65c8dceb17a1907cf Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 6 Jan 2023 17:04:15 +0100 Subject: [PATCH 322/394] send directed presence to transport if endpoints are configured --- .../persistance/UnifiedPushDatabase.java | 15 ++++++++++++++ .../services/UnifiedPushBroker.java | 20 +++++++++++++++---- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java b/src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java index 1b1adc92f..f36506bd1 100644 --- a/src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java +++ b/src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java @@ -129,6 +129,21 @@ public ApplicationEndpoint getEndpoint( return null; } + public boolean hasEndpoints(final UnifiedPushBroker.Transport transport) { + final SQLiteDatabase sqLiteDatabase = getReadableDatabase(); + try (final Cursor cursor = + sqLiteDatabase.rawQuery( + "SELECT EXISTS(SELECT endpoint FROM push WHERE account = ? AND transport = ?)", + new String[] { + transport.account.getUuid(), transport.transport.toEscapedString() + })) { + if (cursor != null && cursor.moveToFirst()) { + return cursor.getInt(0) > 0; + } + } + return false; + } + @Override public void onUpgrade( final SQLiteDatabase sqLiteDatabase, final int oldVersion, final int newVersion) {} diff --git a/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java index 5c6f1bbc8..8bfd018a7 100644 --- a/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java +++ b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java @@ -28,6 +28,7 @@ import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import eu.siacs.conversations.xmpp.stanzas.PresencePacket; public class UnifiedPushBroker { @@ -52,18 +53,29 @@ public UnifiedPushBroker(final XmppConnectionService xmppConnectionService) { } public void renewUnifiedPushEndpointsOnBind(final Account account) { - final Optional transport = getTransport(); - if (transport.isPresent()) { - final Account transportAccount = transport.get().account; + final Optional transportOptional = getTransport(); + if (transportOptional.isPresent()) { + final Transport transport = transportOptional.get(); + final Account transportAccount = transport.account; if (transportAccount != null && transportAccount.getUuid().equals(account.getUuid())) { + final UnifiedPushDatabase database = UnifiedPushDatabase.getInstance(service); + if (database.hasEndpoints(transport)) { + sendDirectedPresence(transportAccount, transport.transport); + } Log.d( Config.LOGTAG, account.getJid().asBareJid() + ": trigger endpoint renewal on bind"); - renewUnifiedEndpoint(transport.get()); + renewUnifiedEndpoint(transportOptional.get()); } } } + private void sendDirectedPresence(final Account account, Jid to) { + final PresencePacket presence = new PresencePacket(); + presence.setTo(to); + service.sendPresencePacket(account, presence); + } + public Optional renewUnifiedPushEndpoints() { final Optional transportOptional = getTransport(); if (transportOptional.isPresent()) { From e99655585219fd52c509efad8d0f1e7ede237c72 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 6 Jan 2023 20:32:35 +0100 Subject: [PATCH 323/394] remove footnote in magic create text --- src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index e5399c15e..4edf1ae7d 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -544,7 +544,7 @@ Have some Quick Conversations
You sign up with your phone number and Quicksy will automatically—based on the phone numbers in your address book—suggest possible contacts to you.

By signing up you agree to our privacy policy.]]>
Agree and continue - A guide is set up for account creation on conversations.im.¹\nWhen picking conversations.im as a provider you will be able to communicate with users of other providers by giving them your full XMPP address. + A guide is set up for account creation on conversations.im.\nWhen picking conversations.im as a provider you will be able to communicate with users of other providers by giving them your full XMPP address. Your full XMPP address will be: %s Create Account Use my own provider From 20eb80d34915ca1df70061444daef2bc96f58e01 Mon Sep 17 00:00:00 2001 From: licaon-kter Date: Sun, 1 Jan 2023 14:28:39 +0000 Subject: [PATCH 324/394] Translated using Weblate (Romanian) Currently translated at 100.0% (956 of 956 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ro/ --- src/main/res/values-ro-rRO/strings.xml | 83 ++++++++++++++------------ 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index ad37ec02e..41eed3c7e 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -31,16 +31,11 @@ acum %d minute %d conversație necitită - - %d conversații necitite - - %d de conversații necitite - - trimitere... - Decriptez mesaj. Te rog așteaptă... + trimitere… + Decriptez mesaj. Te rog așteaptă… Mesaj criptat cu OpenPGP Numele de utilizator este deja alocat Nume invalid @@ -59,7 +54,7 @@ Ați dori să ștergeți %s din semne de carte? Conversațiile asociate cu acest semn de carte nu vor fi șterse. Înregistrează un cont nou pe server Schimbă parola pe server - Partajează cu... + Partajează cu… Pornește o conversație Invită contact Invită @@ -87,12 +82,14 @@ trimitere eșuată Se pregătește trimiterea imaginii Se pregătește trimiterea imaginilor - Trimitere fișiere. Te rog asteaptă... + Trimitere fișiere. Vă rugăm așteptați… Șterge istoric Șterge istoricul conversației Doriți să ștergeți toate mesajele din această conversație?\n\nAtenție: Această acțiune nu va afecta mesajele aflate pe alte dispozitive sau servere. Șterge fișierul - Sigur doriți să ștergeți acest fișier?\n\nAtenție: Această acțiune nu va șterge copiile acestui fișier care sunt stocate pe alte dispozitive sau servere. + Sigur doriți să ștergeți acest fișier\? +\n +\nAtenție: Această acțiune nu va șterge copiile acestui fișier care sunt stocate pe alte dispozitive sau servere. Închide conversația după ștergere Alege dispozitiv Trimite mesaje necriptate @@ -105,19 +102,19 @@ Trimite necriptat Decriptarea a eșuat. Poate nu aveți cheia privată corectă. OpenKeychain - OpenKeychain pentru a cripta și decripta mesaje și a administra cheile publice.\n\nOpenKeychain este licențiat sub GPLv3+ și este disponibil în F-Droid și Google Play.\n\n(Vă rugăm să reporniți %1$s după instalare.)]]> + %1$s utilizează <b>OpenKeychain</b> pentru a cripta și decripta mesaje și a administra cheile publice.<br><br>OpenKeychain este licențiat sub GPLv3+ și este disponibil în F-Droid și Google Play.<br><br><small>(Vă rugăm să reporniți %1$s după instalare.)</small> Repornește Instalare Va rugăm să instalați OpenKeychain - transmit... - în așteptare... + transmit… + în așteptare… Nu am găsit cheia OpenPGP Nu s-au putut cripta mesajele deoarece contactul nu își anunță cheile publice.\n\nRugați-vă contactul să își configureze OpenPGP. Nu am găsit chei OpenPGP Nu s-au putut cripta mesajele deoarece contactele nu își anunță cheile publice.\n\nRugați-vă contactele să își configureze OpenPGP. General Acceptă fișiere - Mai mici decât... + Mai mici decât… Atașamente Notificare Vibrează @@ -154,7 +151,9 @@ Nu s-a putut face convertirea imaginii Fișierul nu a fost găsit Eroare I/O generala. Poate ați rămas fără spațiu liber? - Aplicația folosită pentru selecția acestei imagini nu a oferit destule permisiuni pentru a putea citii fișierul.\n\nFolosiți un alt manager de fișiere pentru a alege o imagine + Aplicația folosită pentru selecția acestei imagini nu a oferit destule permisiuni pentru a putea citii fișierul. +\n +\nFolosiți un alt manager de fișiere pentru a alege o imagine. Aplicația pe care ați folosit-o pentru a partaja acest fișier nu a furnizat suficiente permisiuni. Necunoscut Dezactivat temporar @@ -196,7 +195,7 @@ numeutilizator@exemplu.ro Parolă Aceasta nu este o adresă XMPP valabilă - Memorie epuizată. Imaginea este prea mare. + Memorie epuizată. Imaginea este prea mare Vreți să adăugați pe %s în lista de contacte? Informații server XEP-0313: MAM @@ -228,7 +227,7 @@ v\\Amprenta OMEMO (originea mesajului) Alte dispozitive Amprente OMEMO de încredere - Se preiau cheile... + Se preiau cheile… Gata Decriptează Semne de carte @@ -254,7 +253,7 @@ Nu s-a putut distruge canalul Editează subiectul discuției de grup Subiect discuție - Vă alăturați discuției de grup... + Vă alăturați discuției de grup… Paraseste Contactul v-a adăugat în lista de contacte Adaugă contact @@ -264,7 +263,7 @@ Toate persoanele au citit până aici Publică Atingeți avatarul pentru a selecta o poză din galerie - Se publică... + Se publică… Acest server v-a refuzat publicarea Nu s-a putut face convertirea pozei Nu s-a putut salva avatarul pe disc @@ -282,7 +281,9 @@ Activează Discuția de grup necesită o parolă Introduceți parola - Vă rugăm să cereți mai întâi actualizări de prezență de la acest contact.\n\nAcestea vor fi folosite pentru a determina ce aplicații folosește contactul dumneavoastră. + Vă rugăm să cereți mai întâi actualizări de prezență de la acest contact. +\n +\nAcestea vor fi folosite pentru a determina ce aplicații folosește contactul dumneavoastră.. Cere acum Ignora Atenție: Trimițând aceasta fără actualizări de prezență reciproce ar putea produce probleme neprevazute.\n\nMergeți la \"Detalii contact\" pentru a verifica abonările la actualizările de prezență. @@ -371,8 +372,8 @@ A apărut o problemă Descarc istoric de pe server Nu mai exista istoric pe server - Actualizare... - Parolă schimbată + Actualizare… + Parolă schimbată! Nu s-a putut schimba parola Schimbare parolă Parola curentă @@ -430,9 +431,9 @@ Trimit %s Ofer %s Ascunde deconectat - %s tastează... + %s tastează… %s s-a oprit din scris - %s tastează... + %s tastează… %s s-au oprit din scris Notificare tastare Contactele sunt anunțate atunci când le scrieți un nou mesaj @@ -470,7 +471,7 @@ Acesta nu este un nume de utilizator valabil Descărcare eșuată: Serverul nu a fost găsit Descărcare eșuată: Fișierul nu a fost găsit - Descărcare eșuată: Nu s-a putut realiza conexiunea cu gazda. + Descărcare eșuată: Nu s-a putut realiza conexiunea cu gazda Descărcare eșuată: Nu s-a putut scrie fișierul Descărcarea a eșuat: Fișier invalid Rețeaua Tor nu este disponibilă @@ -491,7 +492,7 @@ Nu s-a putut analiza certificatul Preferințe arhivare Preferințe arhivare pe server - Se descarcă preferințe arhivare. Vă rugăm să așteptați... + Se descarcă preferințe arhivare. Vă rugăm să așteptați… Nu s-au putut descărca preferințele de arhivare Text captcha de verificare necesar Introduceți textul din imaginea de mai sus @@ -499,7 +500,7 @@ Adresa XMPP nu corespunde cu certificatul Înnoiește certificatul Eroare la preluarea cheii OMEMO! - Verifica cheia OMEMO cu un certificat + Sa verificat cheia OMEMO cu un certificat! Dispozitivul nu permite selectia unui certificat pentru client! Opțiuni conexiune Conectare prin Tor @@ -523,7 +524,10 @@ Permiteți %1$s acces la stocarea externă Permiteți %1$s acces la camera foto Sincronizează cu contactele - %1$s dorește permisiunea de a vă accesa contactele pentru a putea potrivi lista de contacte XMPP cu cea din dispozitiv și a afișa numele lor complete și avatarele.\n\n%1$s va citi și potrivi local fără a fi încărcate pe serverul dumneavoastră. + %1$s dorește permisiunea de a vă accesa contactele pentru a putea potrivi lista de contacte XMPP cu cea din dispozitiv. +\nAșa v-a afișa numele lor complete și avatarele. +\n +\n%1$s va citi și potrivi local fără a fi încărcate pe serverul dumneavoastră.
Nu vom stoca o copie a acestor numere the telefon.\n\nPentru mai multe informații puteți citii politica noastră de confidențialitate.

Urmează să fiți întrebați dacă doriți să permiteți accesul la contacte.]]>
Notifică la toate mesajele Notifică doar atunci când cineva vă menționează numele @@ -535,7 +539,9 @@ Doar imaginile mari Optimizare baterie activată Dispozitivul dumneavoastră încearcă să optimizeze agresiv consumul bateriei pentru %1$s, aceasta poate duce la notificări întârziate sau chiar pierderea mesajelor.\nEste recomandat sa le dezactivați. - Dispozitivul dumneavoastră încearcă să optimizeze agresiv consumul bateriei pentru %1$s, aceasta poate duce la notificări întârziate sau chiar pierderea mesajelor.\nÎn continuare veți fi rugați să le dezactivați. + Dispozitivul dumneavoastră încearcă să optimizeze agresiv consumul bateriei pentru %1$s, aceasta poate duce la notificări întârziate sau chiar pierderea mesajelor. +\n +\nÎn continuare veți fi rugați să le dezactivați. Dezactivează Zona selectată este prea mare (Nici un cont activat) @@ -546,7 +552,7 @@ Ați dezactivat acest cont Eroare de securitate.: Acces fișier invalid! Nu s-a găsit nici o aplicație care să partajeze adresa - Partajează adresa cu... + Partajează adresa cu…
Vă înscrieți cu numărul de telefon și Quicksy—pe baza numerelor de telefon din agenda dumneavoastră—vă va sugera automat posibile contacte.

Înscriindu-vă sunteți de acord cu politica noastră de confidențialitate.]]>
Sunt de acord și continuă Ghidul va configura un cont pe conversations.im.¹\nCând alegeți conversations.im ca furnizor veți putea comunica cu utilizatorii altor furnizori oferindu-le adresa dumneavoastră completă XMPP. @@ -567,7 +573,7 @@ Înregistrare eșuată: Încercați din nou mai târziu Înregistrare eșuată: Parolă nesigură Alegeți participanți - Se creează discuția de grup... + Se creează discuția de grup… Trimite din nou invitația Dezactivată Scurtă @@ -747,7 +753,7 @@ Arată locația Partajare Nu s-a putut pornii înregistrarea - Vă rugăm să așteptați... + Vă rugăm să așteptați… Permiteți %1$s acces la microfon Caută mesaje GIF @@ -815,13 +821,13 @@ Retrimite SMS (%s) %s înapoi - S-a copiat automat un posibil cod din memorie + S-a copiat automat un posibil cod din memorie. Vă rugăm să vă introduceți codul de 6 cifre. Sigur doriți să anulați procedura de înregistrare? Da Nu - Verificare... - Se cere SMS... + Se verifică… + Se cere SMS… Codul introdus este incorect. Codul pe care vi l-am trimis a expirat. Eroare de rețea necunoscută. @@ -870,7 +876,7 @@ Vă rugăm să furnizați un nume pentru canal Vă rugăm să furnizați o adresă XMPP Aceasta este o adresă XMPP. Vă rugăm să furnizați un nume. - Se creează canalul public... + Se creează canalul public… Acest canal există deja V-ați alăturat unui canal existent Nu s-a putut salva configurația canalului @@ -907,7 +913,7 @@ Acest cont a fost deja configurat Va rugăm să introduceți parola pentru acest cont Nu s-a putut realiza această acțiune - Alătură-te unui canal public... + Alătură-te unui canal public… Aplicația care a partajat nu a permis accesul la acest fișier. jabber.network @@ -1015,5 +1021,4 @@ Apelurile sunt dezactivate atunci când utilizați Tor Comută la video Respinge solicitarea de comutare la video - - + \ No newline at end of file From c515d30c559ac07e16ca89bc1aa164eab2d2f8ad Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Mon, 2 Jan 2023 18:23:33 +0000 Subject: [PATCH 325/394] Translated using Weblate (Spanish) Currently translated at 100.0% (9 of 9 strings) Translation: Conversations/Android App (Quicksy) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-quicksy/es/ --- src/quicksy/res/values-es/strings.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/quicksy/res/values-es/strings.xml b/src/quicksy/res/values-es/strings.xml index fb93a9971..153e61b43 100644 --- a/src/quicksy/res/values-es/strings.xml +++ b/src/quicksy/res/values-es/strings.xml @@ -1,10 +1,10 @@ - Cuánto tiempo Quicksy permanece en silencio después de detectar una actividad en otro dispositivo - Si elige enviar un informe de error, estará ayudando al desarrollo de Quicksy + El tiempo que Quicksy permanece en silencio después de ver actividad en otro dispositivo + Al enviar seguimientos del registro, está ayudando al desarrollo de Quicksy Informar a tus contactos cuando usas Quicksy - Para continuar recibiendo notificaciones incluso cuando la pantalla está apagada, debe agregar Quicksy a la lista de aplicaciones protegidas. - Foto de perfil de Quicksy + Para seguir recibiendo notificaciones, aunque la pantalla esté apagada, tienes que añadir Quicksy a la lista de aplicaciones protegidas. + Imagen de perfil Quicksy Quicksy no está disponible en tu país. No se ha podido verificar la identidad del servidor. Error de seguridad desconocido. From 1d7abd86a7442a13f361deb621dcfae4668ea82d Mon Sep 17 00:00:00 2001 From: nautilusx Date: Mon, 2 Jan 2023 16:46:34 +0000 Subject: [PATCH 326/394] Translated using Weblate (German) Currently translated at 100.0% (43 of 43 strings) Translation: Conversations/App Store Metadata Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/de/ --- fastlane/metadata/android/de-DE/changelogs/42044.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 fastlane/metadata/android/de-DE/changelogs/42044.txt diff --git a/fastlane/metadata/android/de-DE/changelogs/42044.txt b/fastlane/metadata/android/de-DE/changelogs/42044.txt new file mode 100644 index 000000000..27548eeab --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/42044.txt @@ -0,0 +1,3 @@ +* Nachrichten werden bei Verwendung von SASL2 nicht mehr erneut gesendet +* Schwarzes Video zwischen einigen Geräten behoben +* Absturz bei leeren Passwörtern behoben From 88d82375a68af9419060724c43c1b73edf9228a0 Mon Sep 17 00:00:00 2001 From: mmbd Date: Wed, 4 Jan 2023 19:39:30 +0000 Subject: [PATCH 327/394] Translated using Weblate (Japanese) Currently translated at 99.4% (951 of 956 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ja/ --- src/main/res/values-ja/strings.xml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml index d788c54a5..54035ec75 100644 --- a/src/main/res/values-ja/strings.xml +++ b/src/main/res/values-ja/strings.xml @@ -31,7 +31,6 @@ %d分前 %d件の未読の会話 - 送信中… メッセージを復号しています。しばらくお待ちください… @@ -170,7 +169,7 @@ 互換性のない端末 ストリーム エラー ストリームを開く際にエラー - 暗号化されていない + 暗号化しない OTR OpenPGP OMEMO @@ -682,7 +681,7 @@ 未知の証明書を受け入れますか? サーバー証明書が既知の認証局によって署名されていません。 不一致のサーバー名を受け入れますか? - サーバーは\"%s\"として認証できませんでした。証明書は次の場合にのみ有効です: + サーバーは\"%s\"として認証できませんでした。証明書は次の場合にのみ有効です: それでも接続を希望しますか? 証明書の詳細: 一度だけ @@ -902,7 +901,7 @@ 通話受入 通話終了 応答 - 解散 + 拒否 デバイス発見 鳴動 取込中 @@ -978,4 +977,4 @@ アバターを削除 Tor使用中のため通話できません ビデオ通話切替 - + \ No newline at end of file From abb5a732ac0d4fc36f3be6690b3bc6ce2b1999c4 Mon Sep 17 00:00:00 2001 From: licaon-kter Date: Tue, 3 Jan 2023 20:46:59 +0000 Subject: [PATCH 328/394] Translated using Weblate (Romanian) Currently translated at 100.0% (956 of 956 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ro/ --- src/main/res/values-ro-rRO/strings.xml | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index 41eed3c7e..3f44591ed 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -35,7 +35,7 @@ %d de conversații necitite trimitere… - Decriptez mesaj. Te rog așteaptă… + Decriptare mesaj. Vă rugăm să așteptați… Mesaj criptat cu OpenPGP Numele de utilizator este deja alocat Nume invalid @@ -82,7 +82,7 @@ trimitere eșuată Se pregătește trimiterea imaginii Se pregătește trimiterea imaginilor - Trimitere fișiere. Vă rugăm așteptați… + Trimitere fișiere. Vă rugăm să așteptați… Șterge istoric Șterge istoricul conversației Doriți să ștergeți toate mesajele din această conversație?\n\nAtenție: Această acțiune nu va afecta mesajele aflate pe alte dispozitive sau servere. @@ -500,7 +500,7 @@ Adresa XMPP nu corespunde cu certificatul Înnoiește certificatul Eroare la preluarea cheii OMEMO! - Sa verificat cheia OMEMO cu un certificat! + Cheia OMEMO s-a verificat cu un certificat! Dispozitivul nu permite selectia unui certificat pentru client! Opțiuni conexiune Conectare prin Tor @@ -538,10 +538,11 @@ Mereu Doar imaginile mari Optimizare baterie activată - Dispozitivul dumneavoastră încearcă să optimizeze agresiv consumul bateriei pentru %1$s, aceasta poate duce la notificări întârziate sau chiar pierderea mesajelor.\nEste recomandat sa le dezactivați. + Dispozitivul dumneavoastră încearcă să optimizeze agresiv consumul bateriei pentru %1$s, aceasta poate duce la notificări întârziate sau chiar pierderea mesajelor. +\nEste recomandat să dezactivați optimizarea. Dispozitivul dumneavoastră încearcă să optimizeze agresiv consumul bateriei pentru %1$s, aceasta poate duce la notificări întârziate sau chiar pierderea mesajelor. \n -\nÎn continuare veți fi rugați să le dezactivați. +\nÎn continuare veți fi rugați să dezactivați optimizarea.
Dezactivează Zona selectată este prea mare (Nici un cont activat) @@ -590,7 +591,7 @@ Fundal verde Pentru mesajele primite Nu s-a putut face conectarea la OpenKeychain - Acest dispozitiv nu mai este in uz + Acest dispozitiv nu mai este în uz PC Telefon mobil Tabletă @@ -622,7 +623,7 @@ Codul de bare nu conține amprente pentru această conversație. Amprente verificate Folosește camera pentru a scana codul de bare al contactului - Asteptati cat se preiau cheile + Vă rugăm să așteptați până se preiau cheile Partajează un cod de bare Partajează ca adresă XMPP Partajează ca legatură HTTP @@ -808,7 +809,7 @@ Alegeți o țară număr de telefon Verificare număr de telefon - Quicksy va trimite un mesaj SMS (pot exista costuri în funcție de furnizor) pentru a vă verifica numărul de telefon. Introduceți codul țării dumneavoastră si numărul de telefon: + Quicksy va trimite un mesaj SMS (pot exista costuri în funcție de furnizor) pentru a vă verifica numărul de telefon. Introduceți codul țării dumneavoastră și numărul de telefon:
%s

Este în regulă sau ați dori să editați numărul?]]>
%s nu este un număr de telefon valid. Vă rugăm să vă introduceți numărul de telefon. @@ -819,7 +820,7 @@ Vă rugăm să introduceți codul de 6 cifre mai jos. Retrimitere SMS Retrimite SMS (%s) - %s + Vă rugăm să așteptați (%s) înapoi S-a copiat automat un posibil cod din memorie. Vă rugăm să vă introduceți codul de 6 cifre. From 58a8cdd36872718480f682bca1505f2f271d7a0e Mon Sep 17 00:00:00 2001 From: wiktor Date: Thu, 5 Jan 2023 11:12:08 +0000 Subject: [PATCH 329/394] Translated using Weblate (Polish) Currently translated at 100.0% (13 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/pl/ --- src/conversations/res/values-pl/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conversations/res/values-pl/strings.xml b/src/conversations/res/values-pl/strings.xml index ac746f8e6..ecc67da8c 100644 --- a/src/conversations/res/values-pl/strings.xml +++ b/src/conversations/res/values-pl/strings.xml @@ -2,7 +2,7 @@ Wybierz dostawcę XMPP Użyj conversations.im - Stwórz nowe konto + Utwórz nowe konto Czy masz już konto XMPP? Tak może być jeśli używasz już innego klienta XMPP lub używałeś już Conversations. Jeśli nie możesz stworzyć nowe konto XMPP teraz.\nPodpowiedź: Niektórzy dostawcy poczty oferują również konta XMPP. XMPP to niezależna od dostawcy sieć komunikacji błyskawicznej. Możesz użyć tego klienta z dowolnym serwerem XMPP.\nDla twojej wygody jednak ułatwiliśmy stworzenie konta na conversations.im; dostawcy specjalnie dostosowanego do pracy z Conversations. Zostałeś zaproszony do %1$s. Poprowadzimy ciebie przez proces tworzenia konta.\nWybierając %1$s jako dostawcę będziesz mógł komunikować się z innymi użytkownikami podając swój pełny adres XMPP. From 8ca882d4aabde0f5712f3480490250e5ae493dbc Mon Sep 17 00:00:00 2001 From: nautilusx Date: Thu, 5 Jan 2023 07:51:47 +0000 Subject: [PATCH 330/394] Translated using Weblate (German) Currently translated at 100.0% (43 of 43 strings) Translation: Conversations/App Store Metadata Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/de/ --- fastlane/metadata/android/de-DE/short_description.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/metadata/android/de-DE/short_description.txt b/fastlane/metadata/android/de-DE/short_description.txt index 52910ff20..5249bddf2 100644 --- a/fastlane/metadata/android/de-DE/short_description.txt +++ b/fastlane/metadata/android/de-DE/short_description.txt @@ -1 +1 @@ -Ein verschlüsselter, benutzerfreundlicher XMPP-Instant-Messaging-Client, der für Smartphones optimiert ist +Verschlüsselter, benutzerfreundlicher XMPP-Instant-Messenger für dein Smartphone From 50bb8ab67ace0a598ec9d24bb75f86694857502a Mon Sep 17 00:00:00 2001 From: wiktor Date: Thu, 5 Jan 2023 11:14:22 +0000 Subject: [PATCH 331/394] Translated using Weblate (Polish) Currently translated at 2.3% (1 of 43 strings) Translation: Conversations/App Store Metadata Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/pl/ --- fastlane/metadata/android/pl-PL/short_description.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 fastlane/metadata/android/pl-PL/short_description.txt diff --git a/fastlane/metadata/android/pl-PL/short_description.txt b/fastlane/metadata/android/pl-PL/short_description.txt new file mode 100644 index 000000000..b5e958701 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/short_description.txt @@ -0,0 +1 @@ +Szyfrowany, prosty w użyciu klient XMPP przystosowany do urządzeń mobilnych From 89d2009e2fe4f99ca050d52b1f8c1628158fab87 Mon Sep 17 00:00:00 2001 From: nautilusx Date: Thu, 5 Jan 2023 22:03:12 +0000 Subject: [PATCH 332/394] Translated using Weblate (German) Currently translated at 100.0% (962 of 962 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/de/ --- src/main/res/values-de/strings.xml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index a135a9e90..401b6b3ee 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -614,7 +614,7 @@ Nutze die Kamera, um Barcodes deiner Kontakte zu scannen Bitte warten, bis die Schlüssel abgerufen werden Als Barcode teilen - Als XMPP URI teilen + Als XMPP-URI teilen Als HTTP Link teilen Blind vertrauen vor der Überprüfung Neuen Geräten von nicht verifizierten Kontakten vertrauen, aber bei verifizierten Kontakten eine manuelle Bestätigung der neuen Geräte verlangen. @@ -1000,4 +1000,10 @@ Anrufe sind bei der Verwendung von Tor deaktiviert Umschalten auf Video Umschalten auf Video ablehnen + XMPP-Konto + Push-Server + Ein selbst gewählter Push-Server, der Push-Nachrichten über XMPP an dein Gerät weiterleitet. + Kein (deaktiviert) + UnifiedPush Verteiler + Das Konto, über das Push-Nachrichten empfangen werden sollen. \ No newline at end of file From bb298eebd0fd51bffc974d17659d67492eafb747 Mon Sep 17 00:00:00 2001 From: licaon-kter Date: Fri, 6 Jan 2023 10:25:44 +0000 Subject: [PATCH 333/394] Translated using Weblate (Romanian) Currently translated at 100.0% (962 of 962 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ro/ --- src/main/res/values-ro-rRO/strings.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index 3f44591ed..42a918606 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -1022,4 +1022,10 @@ Apelurile sunt dezactivate atunci când utilizați Tor Comută la video Respinge solicitarea de comutare la video + Cont XMPP + Server Push + Un server ales de utilizator pentru a intermedia mesajele push către dispozitivul vostru prin XMPP. + Nici unul (dezactivat) + Distribuitor UnifiedPush + Contul prin care vor fi primite notificările push. \ No newline at end of file From c7541cdd37718d029ebbeb01b0e3d9db9459ba25 Mon Sep 17 00:00:00 2001 From: hamburger1024 Date: Fri, 6 Jan 2023 02:18:24 +0000 Subject: [PATCH 334/394] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (962 of 962 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/zh_Hans/ --- src/main/res/values-zh-rCN/strings.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index 827f47f6f..f5d2d2e04 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -991,4 +991,10 @@ 使用 Tor 时通话被禁用 切换到视频 拒绝切换到视频的请求 + XMPP 账户 + 推送服务器 + 无(未激活) + UnifiedPush 分发程序 + 将通过该账户接收推送消息。 + 用户选择的推送服务器,通过 XMPP 将推送消息传递到你的设备。 \ No newline at end of file From 2b8dad3006c96ee4c18ff91c67016807d9041220 Mon Sep 17 00:00:00 2001 From: nautilusx Date: Fri, 6 Jan 2023 22:08:16 +0000 Subject: [PATCH 335/394] Translated using Weblate (German) Currently translated at 100.0% (962 of 962 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/de/ --- src/main/res/values-de/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 401b6b3ee..e51229cc2 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -545,7 +545,8 @@ Teile URI mit…
Du registrierst dich mit deiner Telefonnummer und Quicksy wird automatisch auf der Grundlage der Telefonnummern in deinem Adressbuch mögliche Kontakte vorschlagen.

Mit der Anmeldung erklärst du dich mit unserer Datenschutzerklärung einverstanden.]]>
Zustimmen und fortfahren - Ein Guide hilft bei der Kontoerstellung auf conversations.im.¹\nWenn du conversations.im als Provider wählst, kannst du mit Nutzern anderer Anbieter kommunizieren, indem du ihnen deine vollständige XMPP-Adresse gibst. + Ein Guide hilft bei der Kontoerstellung auf conversations.im. +\nWenn du conversations.im als Provider wählst, kannst du mit Nutzern anderer Anbieter kommunizieren, indem du ihnen deine vollständige XMPP-Adresse gibst. Deine vollständige XMPP-Adresse lautet: %s Konto erstellen Nutze eigenen Provider From 20fb420a247a252511e9998b10db96bb9fcbaa34 Mon Sep 17 00:00:00 2001 From: Grzegorz Szymaszek Date: Fri, 6 Jan 2023 20:04:48 +0000 Subject: [PATCH 336/394] Translated using Weblate (Polish) Currently translated at 99.8% (961 of 962 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/pl/ --- src/main/res/values-pl/strings.xml | 32 +++++++++++++++--------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index b4e03f700..6ecbdbc3e 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -31,16 +31,9 @@ %d minut temu %d nieprzeczytana konwersacja - - %d nieprzeczytane konwersacje - - %d nieprzeczytanych konwersacji - - %d nieprzeczytanych konwersacji - wysyłanie... Odszyfrowywanie wiadomości. To zajmie tylko chwilę... @@ -95,7 +88,9 @@ Wyczyść historię konwersacji Czy chcesz usunąć wszystkie wiadomości w tej rozmowie?\n\nOstrzeżenie: To nie ma wpływu na wiadomości składowane na innych urządzeniach lub serwerach. Usuń plik - Czy na pewno usunąć ten plik?\n\nUwaga: Działanie nie wpływa na kopie pliku przechowywane na innych urządzeniach lub serwerach. + Czy na pewno usunąć ten plik\? +\n +\nUwaga: Działanie nie wpływa na kopie pliku przechowywane na innych urządzeniach lub serwerach. Zamknij konwersację po zakończeniu Wybierz urządzenie Wyślij wiadomość bez szyfrowania @@ -156,7 +151,7 @@ Wybrany plik nie jest obrazem Błąd konwersji obrazu Nie odnaleziono pliku - Ogólny błąd wejścia/wyjścia + Ogólny błąd wejścia/wyjścia. Być może skończyło się miejsce w pamięci\? Aplikacja użyta do wyboru obrazu nie zezwoliła na odczyt pliku.\n\nWybierz obraz przy użyciu innego menedżera plików Aplikacja której użyłeś do udostępnienia pliku nie dostarczyła odpowiednich uprawnień. Nieznany @@ -409,7 +404,7 @@ Spraw aby adres XMPP był widoczny dla wszystkich Włącz moderację na kanale Nie bierzesz udziału - Ustawienia konferencji zostały zmodyfikowane + Ustawienia konferencji zostały zmodyfikowane! Nie można zmodyfikować ustawień konferencji Nigdy Ręcznie @@ -468,7 +463,7 @@ Przeszukuj kontakty Przeszukaj zakładki Wyślij wiadomość prywatną - %1$s opuścił konferencję! + %1$s opuścił konferencję Nazwa użytkownika Nazwa użytkownika Błędna nazwa użytkownika @@ -503,8 +498,8 @@ Adres XMPP nie pasuje do certyfikatu Odnów certyfikat Błąd pobierania klucza OMEMO! - Zweryfikowano klucz OMEMO z certyfikatem - Twoje urządzenie nie wspiera wyboru certyfikatów klienckich + Zweryfikowano klucz OMEMO z certyfikatem! + Twoje urządzenie nie wspiera wyboru certyfikatów klienckich! Połączenie Połącz przez sieć TOR Tuneluj wszystkie połączenia przez sieć TOR. Wymaga zainstalowania aplikacji \"Orbot\" @@ -814,7 +809,7 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Wybierz kraj numer telefonu Zweryfikuj swój numer telefonu - Quicksy wyśle SMS (operator może naliczyć koszty) aby zweryfikować numer telefonu. Wpisz kod kraju i numer telefonu. + Quicksy wyśle wiadomość SMS (operator może naliczyć opłatę) aby zweryfikować numer telefonu. Wpisz kod kraju i numer telefonu:
%s

Czy wszystko się zgadza czy też chciałbyś zmienić numer?]]>
%s nie jest prawidłowym numerem telefonu Proszę wpisać swój numer telefonu. @@ -1032,5 +1027,10 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Dzwonienie jest wyłączone podczas używania Tora Przełącz na wideo Odrzuć prośbę przełączenia na wideo - - + Dystrybutor UnifiedPush + Konto XMPP + Konto, poprzez które będą odbierane powiadomienia push. + Serwer push + Dowolnie wybrany serwer push do przekazywania wiadomości push przez XMPP na Twoje urządzenie. + Brak (nieaktywne) + \ No newline at end of file From a6eb12588db192d31353a31838fe0b443ce9ff19 Mon Sep 17 00:00:00 2001 From: hamburger1024 Date: Sat, 7 Jan 2023 04:27:06 +0000 Subject: [PATCH 337/394] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (962 of 962 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/zh_Hans/ --- src/main/res/values-zh-rCN/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index f5d2d2e04..3152cd7f7 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -547,7 +547,8 @@ 分享链接……
您注册了电话号码,Quicksy就会根据您的通讯录中的电话号码自动为您建议可能的联系人

签署即表示您同意我们的隐私政策。]]>
同意并继续 - 此向导将为您在conversations.im¹上创建一个账户。\n您的联系人可以通过您的XMPP完整地址与您聊天。 + 此向导将为您在conversations.im 上创建一个账户。 +\n您的联系人可以通过您的XMPP完整地址与您聊天。 您的XMPP完整地址将是:%s 创建账户 使用我自己的服务器 From c9b1883c41364e6b5c3c74cbe6e3094c63e9bf8e Mon Sep 17 00:00:00 2001 From: Grzegorz Szymaszek Date: Fri, 6 Jan 2023 20:01:33 +0000 Subject: [PATCH 338/394] Translated using Weblate (Polish) Currently translated at 4.6% (2 of 43 strings) Translation: Conversations/App Store Metadata Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/pl/ --- fastlane/metadata/android/pl-PL/changelogs/42044.txt | 3 +++ fastlane/metadata/android/pl-PL/short_description.txt | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 fastlane/metadata/android/pl-PL/changelogs/42044.txt diff --git a/fastlane/metadata/android/pl-PL/changelogs/42044.txt b/fastlane/metadata/android/pl-PL/changelogs/42044.txt new file mode 100644 index 000000000..5098380e3 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/42044.txt @@ -0,0 +1,3 @@ +* Naprawiono ponowne wysyłanie wiadomości podczas używania SASL2 +* Naprawiono czarny obraz wideo pomiędzy niektórymi urządzeniami +* Naprawiono awarię przy użyciu pustych haseł diff --git a/fastlane/metadata/android/pl-PL/short_description.txt b/fastlane/metadata/android/pl-PL/short_description.txt index b5e958701..7869c1ba5 100644 --- a/fastlane/metadata/android/pl-PL/short_description.txt +++ b/fastlane/metadata/android/pl-PL/short_description.txt @@ -1 +1 @@ -Szyfrowany, prosty w użyciu klient XMPP przystosowany do urządzeń mobilnych +Szyfrowany, prosty w użyciu komunikator XMPP dla Twojego urządzenia mobilnego From 3f6ec7e7c111765f1903fe464b81189032a530d2 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Sat, 7 Jan 2023 12:34:46 +0000 Subject: [PATCH 339/394] Translated using Weblate (Spanish) Currently translated at 99.6% (959 of 962 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/es/ --- src/main/res/values-es/strings.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index 559051cc9..e6e9087c2 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -548,7 +548,7 @@ Compartir URI con… Quicksy es un derivado del popular cliente XMPP Conversations con detección automática de contactos.<br><br>El registro se realiza con tu número de teléfono y Quicksy automáticamente—basado en los teléfonos de tu agenda de contactos—te sugerirá posibles contactos.<br><br>Registrándote en Quicksy aceptas nuestra <a href=https://quicksy.im/#privacy>política de privacidad</a>. Aceptar y continuar - Una guía te ayudará en el proceso de creación de la cuenta en conversations.im.¹ + Una guía te ayudará en el proceso de creación de la cuenta en conversations.im. \nCuando selecciones conversations.im como proveedor podrás comunicarte con usuarios de otros servidores proporcionándoles tu dirección XMPP completa. Tu dirección XMPP completa será: %s Crear cuenta @@ -1015,4 +1015,7 @@ Las llamadas están deshabilitadas cuando se usa Tor Cambiar a vídeo Rechazar petición de cambiar a vídeo + Distribuidor de UnifiedPush + Cuenta XMPP + La cuenta a través de la cual se recibirán los mensajes push. \ No newline at end of file From 60308753bef7113c90eca1e848711040501b1283 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Sat, 7 Jan 2023 12:38:11 +0000 Subject: [PATCH 340/394] Translated using Weblate (Spanish) Currently translated at 100.0% (962 of 962 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/es/ --- src/main/res/values-es/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index e6e9087c2..234c5410e 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -1018,4 +1018,7 @@ Distribuidor de UnifiedPush Cuenta XMPP La cuenta a través de la cual se recibirán los mensajes push. + Servidor push + Un servidor push elegido por el usuario para transmitir mensajes push a través de XMPP a su dispositivo. + Ninguno (desactivado) \ No newline at end of file From 50f05cee1c043e61b2f6564609bb6a6c7da00f6e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 7 Jan 2023 15:17:11 +0100 Subject: [PATCH 341/394] version bump to 2.12.0-beta --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index cc35ecb56..87980397c 100644 --- a/build.gradle +++ b/build.gradle @@ -91,8 +91,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 32 - versionCode 42044 - versionName "2.11.3" + versionCode 42045 + versionName "2.12.0-beta" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId From 4670f643f3b086524d6cb282b1179597088c426e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 9 Jan 2023 12:31:06 +0100 Subject: [PATCH 342/394] add changelog ahead of release to allow translation --- CHANGELOG.md | 4 ++++ fastlane/metadata/android/en-US/changelogs/42046.txt | 1 + 2 files changed, 5 insertions(+) create mode 100644 fastlane/metadata/android/en-US/changelogs/42046.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 44f9829f8..36d2bd996 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### Version 2.12.0 + +* Integrate UnifiedPush Distributor to facilitate push messages to other UnifiedPush enabled apps like Tusky and Fedilab + ### Version 2.11.3 * Fix messages getting resend when using SASL2 diff --git a/fastlane/metadata/android/en-US/changelogs/42046.txt b/fastlane/metadata/android/en-US/changelogs/42046.txt new file mode 100644 index 000000000..2ca538cfb --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/42046.txt @@ -0,0 +1 @@ +* Integrate UnifiedPush Distributor to facilitate push messages to other UnifiedPush enabled apps like Tusky and Fedilab From 0ee82f6135d0c1836a93126f9157d30d70e2ffce Mon Sep 17 00:00:00 2001 From: licaon-kter Date: Sun, 8 Jan 2023 15:26:42 +0000 Subject: [PATCH 343/394] Translated using Weblate (Romanian) Currently translated at 100.0% (962 of 962 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ro/ --- src/main/res/values-ro-rRO/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index 42a918606..b8bb104ae 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -556,7 +556,8 @@ Partajează adresa cu…
Vă înscrieți cu numărul de telefon și Quicksy—pe baza numerelor de telefon din agenda dumneavoastră—vă va sugera automat posibile contacte.

Înscriindu-vă sunteți de acord cu politica noastră de confidențialitate.]]>
Sunt de acord și continuă - Ghidul va configura un cont pe conversations.im.¹\nCând alegeți conversations.im ca furnizor veți putea comunica cu utilizatorii altor furnizori oferindu-le adresa dumneavoastră completă XMPP. + Ghidul va configura un cont pe conversations.im. +\nCând alegeți conversations.im ca furnizor veți putea comunica cu utilizatorii altor furnizori oferindu-le adresa dumneavoastră completă XMPP. Adresa dumneavoastră XMPP completă va fi: %s Creează cont Folosește furnizorul meu From 528a73741cd97d1e06f3430bd8ae91fe0229b474 Mon Sep 17 00:00:00 2001 From: Grzegorz Szymaszek Date: Sat, 7 Jan 2023 19:33:34 +0000 Subject: [PATCH 344/394] Translated using Weblate (Polish) Currently translated at 100.0% (13 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/pl/ --- src/conversations/res/values-pl/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conversations/res/values-pl/strings.xml b/src/conversations/res/values-pl/strings.xml index ecc67da8c..f3771aed2 100644 --- a/src/conversations/res/values-pl/strings.xml +++ b/src/conversations/res/values-pl/strings.xml @@ -12,5 +12,5 @@ Użyj przycisku udostępniania aby wysłać swojemu kontaktowi zaproszenie do %1$s. Jeśli twój kontakt jest blisko może przeskanować kod poniżej aby zaakceptować twoje zaproszenie. Dołącz do %1$s aby porozmawiać ze mną: %2$s - Udostępnij zaproszenie... + Udostępnij zaproszenie… \ No newline at end of file From ffe0b9ff5056979b4f2c62229d44e4057e5ba125 Mon Sep 17 00:00:00 2001 From: esk0rner Date: Sat, 7 Jan 2023 15:16:18 +0000 Subject: [PATCH 345/394] Translated using Weblate (Russian) Currently translated at 100.0% (13 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/ru/ --- src/conversations/res/values-ru/strings.xml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/conversations/res/values-ru/strings.xml b/src/conversations/res/values-ru/strings.xml index a81945089..20b99a7b3 100644 --- a/src/conversations/res/values-ru/strings.xml +++ b/src/conversations/res/values-ru/strings.xml @@ -3,10 +3,13 @@ Выберите своего XMPP-провайдера Использовать conversations.im Создать новый аккаунт - У вас есть аккаунт XMPP? Если вы использовали Conversations или другой XMPP-клиент в прошлом, то скорее всего, он у вас есть. Если у вас нет аккаунта, вы можете создать его прямо сейчас.\nНекоторые провайдеры электронной почты также регистрируют аккаунты XMPP. + У вас есть аккаунт XMPP\? Если вы использовали Conversations или другой XMPP-клиент в прошлом, то скорее всего, он у вас есть. Если у вас нет аккаунта, вы можете создать его прямо сейчас. +\nПодсказка: Некоторые провайдеры электронной почты также регистрируют аккаунты XMPP. XMPP - это независимая сеть обмена сообщениями. Conversations позволяет вам подключиться к любому XMPP-серверу на ваш выбор.\nЕсли у вас нет сервера, предлагаем вам зарегистрировать аккаунт на conversations.im, сервере, специально предназначенном для работы с Conversations. - Вас пригласили на %1$s. Мы проведём вас через процесс создания аккаунта. Аккаунт на %1$s позволит вам общаться с пользователями и на этом, и на других серверах, используя ваш полный XMPP-адрес. - Вас пригласили на %1$s. Вам уже назначили имя пользователя. Мы проведём вас через процесс создания аккаунта. Этот аккаунт позволит вам общаться с пользователями и на этом, и на других серверах, используя ваш полный XMPP-адрес. + Вас пригласили на %1$s. Мы проведём вас через процесс создания аккаунта. +\nАккаунт на %1$s позволит вам общаться с пользователями и на этом, и на других серверах, используя ваш полный XMPP-адрес. + Вас пригласили на %1$s. Вам уже назначили имя пользователя. Мы проведём вас через процесс создания аккаунта. +\nЭтот аккаунт позволит вам общаться с пользователями и на этом, и на других серверах, используя ваш полный XMPP-адрес. Ваше приглашение Неправильный формат кода Нажмите кнопку «Поделиться», чтобы отправить вашему контакту приглашение в %1$s. From d6c693786de705d3b9e2a927298bc1670a57a19e Mon Sep 17 00:00:00 2001 From: Grzegorz Szymaszek Date: Sat, 7 Jan 2023 19:34:23 +0000 Subject: [PATCH 346/394] Translated using Weblate (Polish) Currently translated at 100.0% (9 of 9 strings) Translation: Conversations/Android App (Quicksy) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-quicksy/pl/ --- src/quicksy/res/values-pl/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/quicksy/res/values-pl/strings.xml b/src/quicksy/res/values-pl/strings.xml index 4794987b4..d94b0c775 100644 --- a/src/quicksy/res/values-pl/strings.xml +++ b/src/quicksy/res/values-pl/strings.xml @@ -1,12 +1,12 @@ - Ilość czasu kiedy Quicksy jest cicho po zobaczeniu aktywności na innym urządzeniu. + Czas, przez który Quicksy jest cicho po zobaczeniu aktywności na innym urządzeniu Wysyłając nam ślady stosu pomagasz w rozwoju Quicksy Powiadom kontakty o tym że używasz Quicksy Aby otrzymywać powiadomienia nawet kiedy ekran jest wyłączony musisz dodać Quicksy do listy chronionych aplikacji. Obrazek profilowy Quicksy - Quicksy nie jest dostępne w twoim kraju + Quicksy nie jest dostępne w Twoim kraju. Nie udało się sprawdzić tożsamości serwera. Nieznany błąd bezpieczeństwa. Błąd czasu oczekiwania na połączenie z serwerem. - + \ No newline at end of file From 5f4fd7f37d7b2912994f191ec82761fa1ed4cd9b Mon Sep 17 00:00:00 2001 From: licaon-kter Date: Sun, 8 Jan 2023 15:27:40 +0000 Subject: [PATCH 347/394] Translated using Weblate (Romanian) Currently translated at 2.3% (1 of 43 strings) Translation: Conversations/App Store Metadata Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/ro/ --- fastlane/metadata/android/ro/short_description.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 fastlane/metadata/android/ro/short_description.txt diff --git a/fastlane/metadata/android/ro/short_description.txt b/fastlane/metadata/android/ro/short_description.txt new file mode 100644 index 000000000..143f7cb55 --- /dev/null +++ b/fastlane/metadata/android/ro/short_description.txt @@ -0,0 +1 @@ +Client de mesagerie XMPP ușor de folosit, criptat, și optimizat pentru mobile From a128f49472c7bb07e56d9cf4eab6325e6a760e21 Mon Sep 17 00:00:00 2001 From: Grzegorz Szymaszek Date: Sat, 7 Jan 2023 20:23:52 +0000 Subject: [PATCH 348/394] Translated using Weblate (Polish) Currently translated at 6.9% (3 of 43 strings) Translation: Conversations/App Store Metadata Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/pl/ --- .../android/pl-PL/changelogs/42044.txt | 6 +-- .../android/pl-PL/full_description.txt | 39 +++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 fastlane/metadata/android/pl-PL/full_description.txt diff --git a/fastlane/metadata/android/pl-PL/changelogs/42044.txt b/fastlane/metadata/android/pl-PL/changelogs/42044.txt index 5098380e3..9afce4574 100644 --- a/fastlane/metadata/android/pl-PL/changelogs/42044.txt +++ b/fastlane/metadata/android/pl-PL/changelogs/42044.txt @@ -1,3 +1,3 @@ -* Naprawiono ponowne wysyłanie wiadomości podczas używania SASL2 -* Naprawiono czarny obraz wideo pomiędzy niektórymi urządzeniami -* Naprawiono awarię przy użyciu pustych haseł +* Naprawiono ponowne wysyłanie wiadomości podczas używania SASL2. +* Naprawiono czarny obraz wideo pomiędzy niektórymi urządzeniami. +* Naprawiono awarię przy użyciu pustych haseł. diff --git a/fastlane/metadata/android/pl-PL/full_description.txt b/fastlane/metadata/android/pl-PL/full_description.txt new file mode 100644 index 000000000..bb1ca3919 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/full_description.txt @@ -0,0 +1,39 @@ +Łatwy w użyciu, godny zaufania, przyjazny dla baterii. Wbudowane wsparcie dla obrazków, rozmów grupowych i szyfrowania od nadawcy do odbiorcy. + +Zasady projektu: + +* ma być tak ładny i prosty w użyciu jak to możliwe bez uszczerbku na bezpieczeństwie lub prywatności; +* używa istniejących, dobrze znanych protokołów; +* nie wymaga Konta Google ani, w szczególności, Google Cloud Messaging (GCM); +* wymaga tylko naprawdę koniecznych uprawnień. + +Funkcjonalność: + +* szyfrowanie od nadawcy do odbiorcy (E2EE) z użyciem OMEMO lub OpenPGP; +* wysyłanie i odbieranie obrazków; +* szyfrowane rozmowy głosowe i wideo; +* intuicyjny interfejs użytkownika, zgodny z wytycznymi Android Design; +* obrazki/awatary dla Twoich kontaktów; +* synchronizacja z klientem desktopowym; +* konferencje (z obsługą zakładek); +* integracja z książką adresową; +* wiele kont, zintegrowana skrzynka odbiorcza; +* bardzo ograniczony wpływ na zużycie baterii. + +Conversations bardzo ułatwia rejestrację konta na darmowym serwerze conversations.im, jednak będzie działać również z każdym innym serwerem XMPP. Wiele serwerów jest uruchamianych przez wolontariuszy i są dostępne za bez opłat. + +Funkcjonalność XMPP: + +Conversations działa z każdym dostępnym serwerem XMPP, jednak XMPP to rozszerzalny protokół. Rozszerzenia są ustandaryzowane w tak zwanych XEP. Conversations obsługuje sporo z nich, dzięki czemu można go przyjemniej używać. Jest jednak możliwość, że Twój obecny serwer nie obsługuje tych rozszerzeń. Aby wyciągnąć jak najwięcej z Conversations rozważ przeniesienie się na taki serwer, który je obsługuje, lub — jeszcze lepiej — uruchom własny serwer dla Ciebie i Twoich przyjaciół. + +Obecnie są obsługiwane następujące rozszerzenia: + +* XEP-0065: SOCKS5 Bytestreams (lub mod_proxy65). Będzie używany do przesyłania plików jeżeli obie strony znajdują się za zaporą (NAT); +* XEP-0163: Personal Eventing Protocol dla awatarów; +* XEP-0191: Blocking Command umożliwia ochronę przed spamerami lub blokowanie bez usuwanie ich z rostera; +* XEP-0198: Stream Management pozwala na przetrwanie krótkich braków połączenia z siecią oraz zmian używanego połączenia TCP; +* XEP-0280: Message Carbons automatycznie synchronizuje wysyłane wiadomości z klientem desktopowym i w ten sposób pozwala na proste używanie zarówno klienta mobilnego, jak i desktopowego, w jednej konwersacji; +* XEP-0237: Roster Versioning, dzięki któremu można ograniczyć używanie sieci na słabych połączeniach komórkowych; +* XEP-0313: Message Archive Management synchronizuje historię wiadomości z serwerem. Bądź na bieżąco z wiadomości wysłanymi gdy Conversations był rozłączony; +* XEP-0352: Client State Indication informuje serwer o tym, czy Conversations działa w tle. Pozwala to na oszczędzanie łącza przez wstrzymywanie mniej ważnych komunikatów; +* XEP-0363: HTTP File Upload umożliwia udostępnianie plików w konferencjach oraz rozłączonym kontaktom. Wymaga dodatkowego komponentu na Twoim serwerze. From caa5c519f12f6e1b5baa1635981b176c144120b4 Mon Sep 17 00:00:00 2001 From: ewm Date: Sun, 8 Jan 2023 19:54:53 +0000 Subject: [PATCH 349/394] Translated using Weblate (Polish) Currently translated at 100.0% (962 of 962 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/pl/ --- src/main/res/values-pl/strings.xml | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 6ecbdbc3e..fc021fdc6 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -35,8 +35,8 @@ %d nieprzeczytanych konwersacji %d nieprzeczytanych konwersacji - wysyłanie... - Odszyfrowywanie wiadomości. To zajmie tylko chwilę... + wysyłanie… + Odszyfrowywanie wiadomości. To zajmie tylko chwilę… Wiadomość zaszyfrowana OpenPGP Nazwa jest już w użyciu NIeprawidłowy pseudonim @@ -55,7 +55,7 @@ Czy chcesz usunąć zakładkę %s? Rozmowy z tą zakładką nie zostaną usunięte. Zarejestruj nowe konto na serwerze Zmień hasło na serwerze - Udostępnij... + Udostępnij… Rozpocznij rozmowę Zaproś kontakt Zaproś @@ -83,7 +83,7 @@ wysyłanie nie powiodło się Przygotowanie do wysłania obrazka Przygotowanie do wysłania obrazków - Udostępnianie plików. Proszę czekać... + Udostępnianie plików. Proszę czekać… Wyczyść historię Wyczyść historię konwersacji Czy chcesz usunąć wszystkie wiadomości w tej rozmowie?\n\nOstrzeżenie: To nie ma wpływu na wiadomości składowane na innych urządzeniach lub serwerach. @@ -107,8 +107,8 @@ Zrestartuj Zainstaluj Proszę zainstalować OpenKeychain - oferowanie... - oczekiwanie... + oferowanie… + oczekiwanie… Nie znaleziono klucza OpenPGP Nie można zaszyfrować twojej wiadomości bo ten kontakt nie ogłasza swojego publicznego klucza.\n\nPoproś kontakt aby ustawił OpenPGP. Nie znaleziono kluczy OpenPGP @@ -199,10 +199,10 @@ Informacje o serwerze XEP-0313: MAM XEP-0280: Kopie wiadomości - XEP-0352: Client State Indication - XEP-0191: Blocking Command + XEP-0352: Wskaźnik stanu klienta + XEP-0191: Polecenia Blokujące XEP-0237: Roster Versioning - XEP-0198: Stream Management + XEP-0198: Zarządzanie Strumieniem XEP-0215: Wykrywanie Zewnętrznych Usług XEP-0163: PEP (Awatary / OMEMO) XEP-0363: Przesyłanie plików przez HTTP @@ -549,7 +549,8 @@ Udostępnij URI za pomocą...
Zapisujesz się przy użyciu numeru telefonu i Quicksy automatycznie - na podstawie numerów telefonów w książce adresowej - zasugeruje potencjalne kontakty dla ciebie.

Zapisując się zgadzasz się na naszą politykę prywatności.]]>
Zgoda i kontynuuj - Poprowadzimy ciebie przez proces tworzenia konta na conversations.im.¹\nKiedy wybierzesz conversations.im jako dostawcę będziesz mógł komunikować się z innymi osobami jeśli podasz im swój pełen adres XMPP. + Poprowadzimy cię przez proces tworzenia konta na conversations.im. +\nKiedy wybierzesz conversations.im jako dostawcę będziesz mógł komunikować się z innymi osobami jeśli podasz im swój pełen adres XMPP. Twój pełen adres XMPP to: %s Utwórz konto Użyj innego serwera From 86394e3da73566ee086cd28589e3eb7288f0f4c5 Mon Sep 17 00:00:00 2001 From: nautilusx Date: Mon, 9 Jan 2023 12:05:23 +0000 Subject: [PATCH 350/394] Translated using Weblate (German) Currently translated at 100.0% (44 of 44 strings) Translation: Conversations/App Store Metadata Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/de/ --- fastlane/metadata/android/de-DE/changelogs/42046.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 fastlane/metadata/android/de-DE/changelogs/42046.txt diff --git a/fastlane/metadata/android/de-DE/changelogs/42046.txt b/fastlane/metadata/android/de-DE/changelogs/42046.txt new file mode 100644 index 000000000..ed6e4bb38 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/42046.txt @@ -0,0 +1 @@ +* Integration eines UnifiedPush-Verteilers, um Push-Nachrichten für andere UnifiedPush-fähige Apps wie Tusky und Fedilab zu ermöglichen From 0923440936155b771050ef97ead94a8dd48769f6 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 10 Jan 2023 09:02:32 +0100 Subject: [PATCH 351/394] version bump to 2.12.0 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 87980397c..652f20262 100644 --- a/build.gradle +++ b/build.gradle @@ -91,8 +91,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 32 - versionCode 42045 - versionName "2.12.0-beta" + versionCode 42046 + versionName "2.12.0" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId From 41cd96e37b9beeb7a4239ea4ea1ffac49cfc2b72 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 10 Jan 2023 17:22:48 +0100 Subject: [PATCH 352/394] UP: null check transport verification --- .../services/UnifiedPushBroker.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java index 8bfd018a7..7d2d90dd5 100644 --- a/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java +++ b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java @@ -6,19 +6,10 @@ import android.content.pm.PackageManager; import android.preference.PreferenceManager; import android.util.Log; - import com.google.common.base.Optional; import com.google.common.base.Strings; import com.google.common.collect.Iterables; import com.google.common.io.BaseEncoding; - -import java.nio.charset.StandardCharsets; -import java.text.ParseException; -import java.util.List; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; @@ -29,6 +20,12 @@ import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.PresencePacket; +import java.nio.charset.StandardCharsets; +import java.text.ParseException; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; public class UnifiedPushBroker { @@ -278,6 +275,9 @@ private Optional getPushTarget( final Jid transport, final String application, final String instance) { + if (transport == null || application == null || instance == null) { + return Optional.absent(); + } final String uuid = account.getUuid(); final List pushTargets = UnifiedPushDatabase.getInstance(service) From a5e380a1af5534f1d68ef7d9819e8180a6ad141a Mon Sep 17 00:00:00 2001 From: Besnik_b Date: Tue, 10 Jan 2023 10:26:07 +0000 Subject: [PATCH 353/394] Added translation using Weblate (Albanian) --- src/conversations/res/values-sq/strings.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 src/conversations/res/values-sq/strings.xml diff --git a/src/conversations/res/values-sq/strings.xml b/src/conversations/res/values-sq/strings.xml new file mode 100644 index 000000000..a6b3daec9 --- /dev/null +++ b/src/conversations/res/values-sq/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From d5e2b6ff6dd80061b9e388556d4997df4f219f47 Mon Sep 17 00:00:00 2001 From: Besnik_b Date: Tue, 10 Jan 2023 10:27:58 +0000 Subject: [PATCH 354/394] Added translation using Weblate (Albanian) --- src/quicksy/res/values-sq/strings.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 src/quicksy/res/values-sq/strings.xml diff --git a/src/quicksy/res/values-sq/strings.xml b/src/quicksy/res/values-sq/strings.xml new file mode 100644 index 000000000..a6b3daec9 --- /dev/null +++ b/src/quicksy/res/values-sq/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From 670fb0c2c013ba34675a864a5e822b4fbc5e53ec Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Tue, 10 Jan 2023 19:40:57 +0000 Subject: [PATCH 355/394] Translated using Weblate (Spanish) Currently translated at 100.0% (9 of 9 strings) Translation: Conversations/Android App (Quicksy) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-quicksy/es/ --- src/quicksy/res/values-es/strings.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/quicksy/res/values-es/strings.xml b/src/quicksy/res/values-es/strings.xml index 153e61b43..9b9f07ad1 100644 --- a/src/quicksy/res/values-es/strings.xml +++ b/src/quicksy/res/values-es/strings.xml @@ -1,10 +1,10 @@ - El tiempo que Quicksy permanece en silencio después de ver actividad en otro dispositivo - Al enviar seguimientos del registro, está ayudando al desarrollo de Quicksy + Cuánto tiempo Quicksy permanece en silencio después de ver actividad en otros dispositivos + Al enviar los seguimientos del registro, está ayudando al desarrollo de Quicksy Informar a tus contactos cuando usas Quicksy - Para seguir recibiendo notificaciones, aunque la pantalla esté apagada, tienes que añadir Quicksy a la lista de aplicaciones protegidas. - Imagen de perfil Quicksy + Para continuar recibiendo notificaciones incluso cuando la pantalla está apagada, debe agregar Quicksy a la lista de aplicaciones protegidas. + Imagen del perfil de Quicksy Quicksy no está disponible en tu país. No se ha podido verificar la identidad del servidor. Error de seguridad desconocido. From e4d79386c84be82bd46d832e7e7fdff894de4b92 Mon Sep 17 00:00:00 2001 From: ghose Date: Thu, 12 Jan 2023 05:04:01 +0000 Subject: [PATCH 356/394] Translated using Weblate (Galician) Currently translated at 100.0% (962 of 962 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/gl/ --- src/main/res/values-gl/strings.xml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 6a5016681..377b7da8b 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -547,7 +547,8 @@ Compartir URI con…
Podes rexistrarte co teu número de teléfono e Quicksy suxerirache automáticamente —tomando os números da túa libreta de enderezos como referencia— posibles contactos para ti.

Ao rexistrarte aceptas a nosa política de privacidade.]]>
Aceptar e continuar - Tes unha guía para crear unha conta en conversations.im¹\nAo escoller conversations.im como provedor poderás comunicarte con outras usuarias de outros provedores con só darlles o teu enderezo XMPP completo. + Tes unha guía para crear unha conta en conversations.im +\nAo escoller conversations.im como provedor poderás comunicarte con outras usuarias de outros provedores con só darlles o teu enderezo XMPP completo. O teu enderezo XMPP completo será: %s Crear conta Utilizar o meu propio proveedor @@ -1003,4 +1004,10 @@ As chamadas están desactivadas cando usas Tor Cambiar a vídeo Rexeitar a solicitude para cambiar a vídeo + Distribuidor UnifiedPush + Conta XMPP + A conta a través da cal se recibirán as mensaxes push. + Servidor Push + O servidor elexido pola usuaria para obter as mensaxes push a través de XMPP. + Ningún (desactivado)
\ No newline at end of file From e91b6ce37744bf508a1e0fe37cee88cdea339566 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 12 Jan 2023 09:41:10 +0100 Subject: [PATCH 357/394] version bump to 2.12.1 --- CHANGELOG.md | 4 ++++ build.gradle | 4 ++-- fastlane/metadata/android/en-US/changelogs/42047.txt | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/42047.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 36d2bd996..b5ef2e389 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### Version 2.12.1 + +* Fix crash in UnifiedPush Distributor + ### Version 2.12.0 * Integrate UnifiedPush Distributor to facilitate push messages to other UnifiedPush enabled apps like Tusky and Fedilab diff --git a/build.gradle b/build.gradle index 652f20262..37c0d856f 100644 --- a/build.gradle +++ b/build.gradle @@ -91,8 +91,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 32 - versionCode 42046 - versionName "2.12.0" + versionCode 42047 + versionName "2.12.1" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId diff --git a/fastlane/metadata/android/en-US/changelogs/42047.txt b/fastlane/metadata/android/en-US/changelogs/42047.txt new file mode 100644 index 000000000..c44467f52 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/42047.txt @@ -0,0 +1 @@ +* Fix crash in UnifiedPush Distributor From b7ceccb1a1d407b47fbd76b9f0455fdd863f9e63 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 12 Jan 2023 11:01:20 +0100 Subject: [PATCH 358/394] fix typo in older changelog --- fastlane/metadata/android/en-US/changelogs/382.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/metadata/android/en-US/changelogs/382.txt b/fastlane/metadata/android/en-US/changelogs/382.txt index 89fc89b78..64e23e14d 100644 --- a/fastlane/metadata/android/en-US/changelogs/382.txt +++ b/fastlane/metadata/android/en-US/changelogs/382.txt @@ -1,2 +1,2 @@ -* Add button to switch camea during video call +* Add button to switch camera during video call * Fixed voice calls on tablets From ed9318feacaedf062232811a7bb2704394b5c7b0 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 13 Jan 2023 14:33:21 +0100 Subject: [PATCH 359/394] remove support for Google Auto MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Google Play Store review is extra 'thorough' with that flag enabled You shouldn’t text and drive anyway Trains! --- src/main/AndroidManifest.xml | 4 ---- src/main/res/xml/automotive_app_desc.xml | 4 ---- 2 files changed, 8 deletions(-) delete mode 100644 src/main/res/xml/automotive_app_desc.xml diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index c18addf27..f6a4f4f84 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -88,10 +88,6 @@ tools:replace="android:label" tools:targetApi="q"> - - - - - \ No newline at end of file From 9cb65a039887f8252f176e5bbc0f1cd9eb73ec73 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 13 Jan 2023 14:35:54 +0100 Subject: [PATCH 360/394] bump version code for new google play release --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 37c0d856f..efbd51277 100644 --- a/build.gradle +++ b/build.gradle @@ -91,7 +91,7 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 32 - versionCode 42047 + versionCode 42048 versionName "2.12.1" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" From c1fe03ef55c9f2d7af47c1871cda048cc6c3981d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 14 Jan 2023 08:11:42 +0100 Subject: [PATCH 361/394] update gradle and AGP --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index efbd51277..b8478b443 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.3.1' + classpath 'com.android.tools.build:gradle:7.4.0' } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d33b7f161..9d41003b2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip From 42b5cab7a474278a9744af5de722de3e80835c9d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 20 Jan 2023 08:07:45 +0100 Subject: [PATCH 362/394] Revert "remove support for Google Auto" This reverts commit ed9318feacaedf062232811a7bb2704394b5c7b0. --- src/main/AndroidManifest.xml | 4 ++++ src/main/res/xml/automotive_app_desc.xml | 4 ++++ 2 files changed, 8 insertions(+) create mode 100644 src/main/res/xml/automotive_app_desc.xml diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index f6a4f4f84..c18addf27 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -88,6 +88,10 @@ tools:replace="android:label" tools:targetApi="q"> + + + + + \ No newline at end of file From 340ad1143de68a1b3dfa684de3041448605498de Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 20 Jan 2023 08:08:35 +0100 Subject: [PATCH 363/394] another version bump for google play MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We are trying to bring Google Auto back. With this release being relatively stable long review times hopefully don’t matter. --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b8478b443..6da78fcae 100644 --- a/build.gradle +++ b/build.gradle @@ -91,7 +91,7 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 32 - versionCode 42048 + versionCode 42049 versionName "2.12.1" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" From e98aa821f1579569fcf7bb2af7a3849baf05fa05 Mon Sep 17 00:00:00 2001 From: Besnik_b Date: Thu, 12 Jan 2023 09:25:49 +0000 Subject: [PATCH 364/394] Translated using Weblate (Albanian) Currently translated at 100.0% (13 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/sq/ --- src/conversations/res/values-sq/strings.xml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/conversations/res/values-sq/strings.xml b/src/conversations/res/values-sq/strings.xml index a6b3daec9..1e3f34b5b 100644 --- a/src/conversations/res/values-sq/strings.xml +++ b/src/conversations/res/values-sq/strings.xml @@ -1,2 +1,20 @@ - \ No newline at end of file + + XMPP është një rrjet shkëmbimi mesazhesh të atypëratyshëm i pavarur nga shërbimet. Këtë klient mund ta përdorni me cilindo shërbyes XMPP që zgjidhni. +\nMegjithatë, për lehtësi, e kemi bërë të kollajshme të krijohet një llogari te conversations.im, një shërbim posaçërisht i përshtatshëm për përdorim me Conversations. + Jeni ftuar te %1$s. Do t’ju udhëheqim përmes procesit të krijimit të një llogarie. +\nKur zgjidhet %1$s si shërbim, do të jeni në gjendje të komunikoni me përdorues nga shërbime të tjera duke u dhënë adresën tuaj të plotë XMPP. + Jeni ftuar te %1$s. Për ju është zgjedhur tashmë një emër përdoruesi. Do t’ju udhëheqim përmes procesit të krijimit të një llogarie. +\nDo të jeni në gjendje të komunikoni me përdorues nga shërbime të tjera duke u dhënë adresën tuaj të plotë XMPP. + Prekni butonin e ndarjes me të tjerë që t’i dërgoni kontaktit tuaj një ftesë për te %1$s. + Nëse kontakti juaj është atypari, mund të skanojë gjithashtu kodin më poshtë, që të pranojë ftesën tuaj. + Bëhuni pjesë e %1$s dhe bisedoni me: %2$s + Ndajeni ftesën me… + Krijoni llogari të re + Zgjidhni shërbimin tuaj XMPP + Përdor conversations.im + Keni tashmë një llogari XMPP\? Mund të jetë kështu nëse përdorni tashmë një klient tjetër XMPP, ose e keni përdorur Conversations më parë. Nëse jo, mund të krijoni një llogari të re XMPP që tani. +\nNdihmëz: Disa shërbime email-i ofrojnë gjithashtu llogari XMPP. + Ftesë nga shërbyesi juaj + Kod i formatuar jo saktësisht + \ No newline at end of file From 98ca0a70685d91c375a68ba95f47cb80dd96113b Mon Sep 17 00:00:00 2001 From: Besnik_b Date: Thu, 12 Jan 2023 09:31:20 +0000 Subject: [PATCH 365/394] Translated using Weblate (Albanian) Currently translated at 100.0% (9 of 9 strings) Translation: Conversations/Android App (Quicksy) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-quicksy/sq/ --- src/quicksy/res/values-sq/strings.xml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/quicksy/res/values-sq/strings.xml b/src/quicksy/res/values-sq/strings.xml index a6b3daec9..d3fbe6d7d 100644 --- a/src/quicksy/res/values-sq/strings.xml +++ b/src/quicksy/res/values-sq/strings.xml @@ -1,2 +1,12 @@ - \ No newline at end of file + + Sasia e kohës që Quicksy nuk ndihet, pasi të ketë parë veprimtari në pajisje tjetër + Duke dërguar “stack traces” ndihmoni në zhvillimin e pandërprerë të Quicksy-t + Bëjuani të ditur krejt kontakteve tuaja, kur përdorni Quicksy-n + Që të vazhdoni të merrni njoftime, edhe kur ekrani juaj është i fikur, duhet të shtoni Quicksy-n te lista e aplikacioneve të mbrojtur. + Foto profili Quicksy + Quicksy s’mund të kihet në vendin tuaj. + S’arrihet të verifikohet identitet shërbyesi. + Gabim i panjohur sigurie. + Mbarim kohe teksa lidhej me shërbyesin + \ No newline at end of file From 3cce3c0256a32873b774e5db064548d65f50cf72 Mon Sep 17 00:00:00 2001 From: Besnik_b Date: Thu, 12 Jan 2023 09:32:00 +0000 Subject: [PATCH 366/394] Translated using Weblate (Albanian) Currently translated at 100.0% (9 of 9 strings) Translation: Conversations/Android App (Quicksy) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-quicksy/sq/ --- src/quicksy/res/values-sq/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/quicksy/res/values-sq/strings.xml b/src/quicksy/res/values-sq/strings.xml index d3fbe6d7d..bac4948d3 100644 --- a/src/quicksy/res/values-sq/strings.xml +++ b/src/quicksy/res/values-sq/strings.xml @@ -8,5 +8,5 @@ Quicksy s’mund të kihet në vendin tuaj. S’arrihet të verifikohet identitet shërbyesi. Gabim i panjohur sigurie. - Mbarim kohe teksa lidhej me shërbyesin + Mbarim kohe teksa lidhej me shërbyesin. \ No newline at end of file From 78d81c704f8e0c3a8d6984192958662eea2696e2 Mon Sep 17 00:00:00 2001 From: Besnik_b Date: Thu, 12 Jan 2023 11:22:01 +0000 Subject: [PATCH 367/394] Translated using Weblate (Albanian) Currently translated at 4.4% (2 of 45 strings) Translation: Conversations/App Store Metadata Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/sq/ --- .../metadata/android/sq/full_description.txt | 39 +++++++++++++++++++ .../metadata/android/sq/short_description.txt | 1 + 2 files changed, 40 insertions(+) create mode 100644 fastlane/metadata/android/sq/full_description.txt create mode 100644 fastlane/metadata/android/sq/short_description.txt diff --git a/fastlane/metadata/android/sq/full_description.txt b/fastlane/metadata/android/sq/full_description.txt new file mode 100644 index 000000000..f6f03b151 --- /dev/null +++ b/fastlane/metadata/android/sq/full_description.txt @@ -0,0 +1,39 @@ +I kollajtë për t’u përdorur, i qëndrueshëm, dashamirës ndaj baterisë. Me mbulim së brendshmi për figura, fjalosje në grup dhe fshehtëzim e2e. + +Parime konceptuale: + +* Të qenët aq i bukur dhe i lehtë për përdorim sa mundet, pa sakrifikuar sigurinë ose privatësinë +* Bazim në protokolle ekzistues, të mirënjohur +* Mospasje nevojë për një Google Account, ose, posaçërisht Google Cloud Messaging (GCM) +* Kërkim i sa më pak lejesh që të jetë e mundur + +Veçori: + +* Fshehtëzim skaj-më-skaj me OMEMO, ose OpenPGP +* Dërgim dhe marrje mesazhesh +* Thirrje të fshehtëzuara audio dhe video (DTLS-SRTP) +* UI intuitive që ndjek udhëzimet Android Design +* Foto / Avatarë të Kontakteve tuaja +* Njëkohësim me klient desktop +* Konferenca (me mbulim për faqerojtës) +* Integrim libri adresash +* Llogari të shumta / kuti e unifikuar të marrësh +* Ndikim shumë i pakët në jetëgjatësinë e baterisë + +Conversations e bën shumë të lehtë krijimin e një llogarie te shërbyesi falas conversations.im. Megjithatë, Conversations do të funksionojë me çfarëdo shërbyesi tjetër XMPP. Plot shërbyes XMPP mbahen në punë nga vullnetarë dhe janë pa pagesë + +Veçori të XMPP-së: + +Conversations funksionon me çdo shërbyes XMPP në qarkullim. Megjithatë, XMPP është një protokoll i zgjerueshëm. Edhe këto zgjerime janë të standardizuara në të ashtuquajturit XEP-e. Conversations mbulon një a dy prej tyre, për ta bërë punën e përdoruesit më të mirë në përgjithësi. Ka një mundësi që shërbyesi juaj aktual XMPP të mos i mbulojë këto zgjerime. Ndaj, që të përfitoni maksimumin nga Conversations, duhet të shihni mundësi ose të kaloni te një shërbyes XMPP që i mbulon, ose - akoma më mirë - të vini në punë shërbyesin tuaj XMPP për ju dhe shokët tuaj. + +Këto XEP-e janë - deri sot: + +* XEP-0065: SOCKS5 Bytestreams (ose mod_proxy65). Do të përdoret për të shpërngulur kartela, nëse të dy palët gjenden pas një firewall-i (NAT). +* XEP-0163: Personal Eventing Protocol, për avatarë +* XEP-0191: Urdhri i bllokimeve ju lejon të kaloni në listë bllokimesh llogari që dërgojnë mesazhe të padëshiruar, ose të bllokoni kontakte pa i hequr nga lista juaj. +* XEP-0198: Stream Management i lejon XMPP-së të mbijetojë ndërprerje të vockla rrjeti dhe ndryshime te lidhja përkatëse TCP. +* XEP-0280: Message Carbons do të njëkohësojë automatikisht te klienti juaj desktop mesazhet që dërgoni dhe, pra, ju lejon të kaloni pa një cen nga klienti juaj për celular në atë për desktop dhe anasjelltas, brenda një bisede. +* XEP-0237: Roster Versioning kryesisht për të kursyer sasi trafiku në lidhje celulare të dobëta +* XEP-0313: Message Archive Management njëkohëson historik mesazhesh me shërbyesin. Ndiqni mesazhet që qenë dërguar ndërkohë që Conversations s’qe në linjë. +* XEP-0352: Client State Indication i lejon shërbyesit të dijë nëse është apo jo në prapaskenë Conversations. I lejon shërbyesit të kursejë sasi trafiku, duke mbajtur paketa pa rëndësi. +* XEP-0363: HTTP File Upload ju lejon të ndani me të tjerë kartela në konferenca dhe me kontakte jo në linjë. Lyp një përbërë shtesë në shërbyesin tuaj. diff --git a/fastlane/metadata/android/sq/short_description.txt b/fastlane/metadata/android/sq/short_description.txt new file mode 100644 index 000000000..6b4e97300 --- /dev/null +++ b/fastlane/metadata/android/sq/short_description.txt @@ -0,0 +1 @@ +Shkëmbyes XMPP mesazhesh të atypëratyshëm, i fshehtëzuar, i lehtë në përdorim, për pajisjen tuaj celulare From 037b4cc7869543c78bb745349a08db082e9da4ac Mon Sep 17 00:00:00 2001 From: Anonymous Date: Thu, 12 Jan 2023 20:03:35 +0000 Subject: [PATCH 368/394] Translated using Weblate (French) Currently translated at 96.7% (931 of 962 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/fr/ --- src/main/res/values-fr/strings.xml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml index 6be70fd0c..147721e5e 100644 --- a/src/main/res/values-fr/strings.xml +++ b/src/main/res/values-fr/strings.xml @@ -31,13 +31,8 @@ Il y a %d minutes %d conversation non lue - - %d conversations non lues - - %d conversations non lues - Envoi… Déchiffrement du message. Veuillez patienter… @@ -246,7 +241,7 @@ Détruire le groupe Détruire le canal Voulez-vous vraiment détruire ce groupe ?\n\nAvertissement : le groupe sera complètement supprimé du serveur. - Êtes-vous sûr de vouloir détruire ce canal public ? \ On \ Avertissement : le canal sera complètement supprimé du serveur. + Êtes-vous sûr de vouloir détruire ce canal public\? On Avertissement: le canal sera complètement supprimé du serveur. Impossible de détruire le groupe Impossible de détruire le canal Modifier le sujet du groupe @@ -979,4 +974,4 @@ Impossible d’activer la vidéo. La création de nouveaux comptes n’est pas prise en charge Aucune adresse XMPP trouvée - + \ No newline at end of file From 8f7452c7542e5dcced7af064b00039ae072a4e8e Mon Sep 17 00:00:00 2001 From: Anonymous Date: Thu, 12 Jan 2023 20:03:51 +0000 Subject: [PATCH 369/394] Translated using Weblate (Turkish) Currently translated at 98.7% (950 of 962 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/tr/ --- src/main/res/values-tr-rTR/strings.xml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/res/values-tr-rTR/strings.xml b/src/main/res/values-tr-rTR/strings.xml index 62d8966b5..922e8b836 100644 --- a/src/main/res/values-tr-rTR/strings.xml +++ b/src/main/res/values-tr-rTR/strings.xml @@ -31,10 +31,7 @@ %d dakika önc %d okunmamış konuşma - - %d okunmamış konuşmalar - gönderiyor… İleti deşifre ediliyor. Lütfen bekleyin… @@ -631,7 +628,7 @@ Aktif olmayanları göster Aktif olmayanları sakla Güvensiz aygıt - Bu cihazın doğrulamasını kaldırmak istediğinizden emin misiniz? \ Bu cihaz ve cihazdan gelen mesajlar güvenilmez olarak işaretlenecektir. + Bu cihazın doğrulamasını kaldırmak istediğinizden emin misiniz\? Bu cihaz ve cihazdan gelen mesajlar güvenilmez olarak işaretlenecektir. %d saniye %d saniye @@ -994,4 +991,4 @@ Geçici doğrulama hatası Avatar\'ı sil Tor kullanırken çağrılar devre dışı - + \ No newline at end of file From e05d6a8e5c256f793b4ff71ff5e50f2f8b3d890d Mon Sep 17 00:00:00 2001 From: Anonymous Date: Thu, 12 Jan 2023 20:03:10 +0000 Subject: [PATCH 370/394] Translated using Weblate (Indonesian) Currently translated at 100.0% (13 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/id/ --- src/conversations/res/values-id/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/conversations/res/values-id/strings.xml b/src/conversations/res/values-id/strings.xml index 16a58436c..a316ee848 100644 --- a/src/conversations/res/values-id/strings.xml +++ b/src/conversations/res/values-id/strings.xml @@ -3,8 +3,8 @@ Pilih XMPP server anda Gunakan conversations.im Buat akun baru - Anda sudah memiliki akun XMPP? Ini mungkin terjadi jika Anda sudah menggunakan aplikasi XMPP yang berbeda atau pernah menggunakan Conversations sebelumnya. Jika tidak, Anda dapat membuat akun XMPP baru. \ NPetunjuk: Beberapa penyedia layanan email juga menyediakan akun XMPP. - XMPP adalah jaringan penyedia pesan instan independen. Anda dapat menggunakan aplikasi ini dengan server XMPP pilihan Anda. \ NNamun demi kenyamanan Anda, kami permudah untuk membuat akun di Conversations.im; provider yang sangat cocok digunakan dengan Conversations. + Anda sudah memiliki akun XMPP\? Ini mungkin terjadi jika Anda sudah menggunakan aplikasi XMPP yang berbeda atau pernah menggunakan Conversations sebelumnya. Jika tidak, Anda dapat membuat akun XMPP baru. NPetunjuk: Beberapa penyedia layanan email juga menyediakan akun XMPP. + XMPP adalah jaringan penyedia pesan instan independen. Anda dapat menggunakan aplikasi ini dengan server XMPP pilihan Anda. NNamun demi kenyamanan Anda, kami permudah untuk membuat akun di Conversations.im; provider yang sangat cocok digunakan dengan Conversations. Anda telah diundang ke %1$s. Kami akan memandu Anda melalui proses pembuatan akun. \nSaat memilih %1$s sebagai penyedia, Anda akan dapat berkomunikasi dengan pengguna provider lain dengan memberikan alamat XMPP lengkap Anda kepada mereka. Anda telah diundang ke%1$s. Username telah dipilihkan untuk Anda. Kami akan memandu Anda melalui proses pembuatan akun. \nAnda dapat berkomunikasi dengan pengguna provider lain dengan memberi mereka alamat XMPP lengkap Anda. Undangan server Anda From 5c2e6f1c7f635947e206f3269c9d053f9bef9715 Mon Sep 17 00:00:00 2001 From: nautilusx Date: Thu, 12 Jan 2023 12:54:20 +0000 Subject: [PATCH 371/394] Translated using Weblate (German) Currently translated at 100.0% (45 of 45 strings) Translation: Conversations/App Store Metadata Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/de/ --- fastlane/metadata/android/de-DE/changelogs/42047.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 fastlane/metadata/android/de-DE/changelogs/42047.txt diff --git a/fastlane/metadata/android/de-DE/changelogs/42047.txt b/fastlane/metadata/android/de-DE/changelogs/42047.txt new file mode 100644 index 000000000..b1271c241 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/42047.txt @@ -0,0 +1 @@ +* Absturz im UnifiedPush-Verteiler behoben From 1fe1a4fabd3bf97562b7d78453beba28cc4037fa Mon Sep 17 00:00:00 2001 From: Besnik_b Date: Thu, 12 Jan 2023 11:22:41 +0000 Subject: [PATCH 372/394] Translated using Weblate (Albanian) Currently translated at 100.0% (45 of 45 strings) Translation: Conversations/App Store Metadata Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/sq/ --- fastlane/metadata/android/sq/changelogs/349.txt | 4 ++++ fastlane/metadata/android/sq/changelogs/351.txt | 3 +++ fastlane/metadata/android/sq/changelogs/353.txt | 4 ++++ fastlane/metadata/android/sq/changelogs/360.txt | 1 + fastlane/metadata/android/sq/changelogs/362.txt | 1 + fastlane/metadata/android/sq/changelogs/364.txt | 2 ++ fastlane/metadata/android/sq/changelogs/367.txt | 2 ++ fastlane/metadata/android/sq/changelogs/379.txt | 1 + fastlane/metadata/android/sq/changelogs/381.txt | 2 ++ fastlane/metadata/android/sq/changelogs/382.txt | 2 ++ fastlane/metadata/android/sq/changelogs/383.txt | 3 +++ fastlane/metadata/android/sq/changelogs/387.txt | 2 ++ fastlane/metadata/android/sq/changelogs/388.txt | 3 +++ fastlane/metadata/android/sq/changelogs/390.txt | 1 + fastlane/metadata/android/sq/changelogs/393.txt | 3 +++ fastlane/metadata/android/sq/changelogs/394.txt | 2 ++ fastlane/metadata/android/sq/changelogs/395.txt | 3 +++ fastlane/metadata/android/sq/changelogs/397.txt | 3 +++ fastlane/metadata/android/sq/changelogs/398.txt | 4 ++++ fastlane/metadata/android/sq/changelogs/401.txt | 2 ++ fastlane/metadata/android/sq/changelogs/402.txt | 3 +++ fastlane/metadata/android/sq/changelogs/403.txt | 3 +++ fastlane/metadata/android/sq/changelogs/404.txt | 1 + fastlane/metadata/android/sq/changelogs/405.txt | 1 + fastlane/metadata/android/sq/changelogs/407.txt | 3 +++ fastlane/metadata/android/sq/changelogs/42000.txt | 4 ++++ fastlane/metadata/android/sq/changelogs/42006.txt | 2 ++ fastlane/metadata/android/sq/changelogs/42010.txt | 2 ++ fastlane/metadata/android/sq/changelogs/42012.txt | 1 + fastlane/metadata/android/sq/changelogs/42013.txt | 1 + fastlane/metadata/android/sq/changelogs/42014.txt | 2 ++ fastlane/metadata/android/sq/changelogs/42015.txt | 1 + fastlane/metadata/android/sq/changelogs/42018.txt | 3 +++ fastlane/metadata/android/sq/changelogs/42022.txt | 2 ++ fastlane/metadata/android/sq/changelogs/42023.txt | 2 ++ fastlane/metadata/android/sq/changelogs/42037.txt | 8 ++++++++ fastlane/metadata/android/sq/changelogs/42038.txt | 2 ++ fastlane/metadata/android/sq/changelogs/42041.txt | 5 +++++ fastlane/metadata/android/sq/changelogs/42042.txt | 2 ++ fastlane/metadata/android/sq/changelogs/42043.txt | 1 + fastlane/metadata/android/sq/changelogs/42044.txt | 3 +++ fastlane/metadata/android/sq/changelogs/42046.txt | 1 + fastlane/metadata/android/sq/changelogs/42047.txt | 1 + fastlane/metadata/android/sq/short_description.txt | 2 +- 44 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 fastlane/metadata/android/sq/changelogs/349.txt create mode 100644 fastlane/metadata/android/sq/changelogs/351.txt create mode 100644 fastlane/metadata/android/sq/changelogs/353.txt create mode 100644 fastlane/metadata/android/sq/changelogs/360.txt create mode 100644 fastlane/metadata/android/sq/changelogs/362.txt create mode 100644 fastlane/metadata/android/sq/changelogs/364.txt create mode 100644 fastlane/metadata/android/sq/changelogs/367.txt create mode 100644 fastlane/metadata/android/sq/changelogs/379.txt create mode 100644 fastlane/metadata/android/sq/changelogs/381.txt create mode 100644 fastlane/metadata/android/sq/changelogs/382.txt create mode 100644 fastlane/metadata/android/sq/changelogs/383.txt create mode 100644 fastlane/metadata/android/sq/changelogs/387.txt create mode 100644 fastlane/metadata/android/sq/changelogs/388.txt create mode 100644 fastlane/metadata/android/sq/changelogs/390.txt create mode 100644 fastlane/metadata/android/sq/changelogs/393.txt create mode 100644 fastlane/metadata/android/sq/changelogs/394.txt create mode 100644 fastlane/metadata/android/sq/changelogs/395.txt create mode 100644 fastlane/metadata/android/sq/changelogs/397.txt create mode 100644 fastlane/metadata/android/sq/changelogs/398.txt create mode 100644 fastlane/metadata/android/sq/changelogs/401.txt create mode 100644 fastlane/metadata/android/sq/changelogs/402.txt create mode 100644 fastlane/metadata/android/sq/changelogs/403.txt create mode 100644 fastlane/metadata/android/sq/changelogs/404.txt create mode 100644 fastlane/metadata/android/sq/changelogs/405.txt create mode 100644 fastlane/metadata/android/sq/changelogs/407.txt create mode 100644 fastlane/metadata/android/sq/changelogs/42000.txt create mode 100644 fastlane/metadata/android/sq/changelogs/42006.txt create mode 100644 fastlane/metadata/android/sq/changelogs/42010.txt create mode 100644 fastlane/metadata/android/sq/changelogs/42012.txt create mode 100644 fastlane/metadata/android/sq/changelogs/42013.txt create mode 100644 fastlane/metadata/android/sq/changelogs/42014.txt create mode 100644 fastlane/metadata/android/sq/changelogs/42015.txt create mode 100644 fastlane/metadata/android/sq/changelogs/42018.txt create mode 100644 fastlane/metadata/android/sq/changelogs/42022.txt create mode 100644 fastlane/metadata/android/sq/changelogs/42023.txt create mode 100644 fastlane/metadata/android/sq/changelogs/42037.txt create mode 100644 fastlane/metadata/android/sq/changelogs/42038.txt create mode 100644 fastlane/metadata/android/sq/changelogs/42041.txt create mode 100644 fastlane/metadata/android/sq/changelogs/42042.txt create mode 100644 fastlane/metadata/android/sq/changelogs/42043.txt create mode 100644 fastlane/metadata/android/sq/changelogs/42044.txt create mode 100644 fastlane/metadata/android/sq/changelogs/42046.txt create mode 100644 fastlane/metadata/android/sq/changelogs/42047.txt diff --git a/fastlane/metadata/android/sq/changelogs/349.txt b/fastlane/metadata/android/sq/changelogs/349.txt new file mode 100644 index 000000000..b7f3efd3a --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/349.txt @@ -0,0 +1,4 @@ +* Sjellje për herë të parë rregullimi ekspertësh për të kryer pikasje kanalesh në shërbyes vendor, në vend se në search.jabber.network +* Aktivizim, si parazgjedhje, i shenjave të për dërgim dhe heqje e rregullimit +* Aktivizim, si parazgjedhje, i “Send button indicates status” dhe heqje e rregullimit +* Kalim i rregullimit “Shërbim Move Backup and Foreground Service” te skena kryesore diff --git a/fastlane/metadata/android/sq/changelogs/351.txt b/fastlane/metadata/android/sq/changelogs/351.txt new file mode 100644 index 000000000..9885dc4dd --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/351.txt @@ -0,0 +1,3 @@ +* ndreqje për shpërngulje kartelash Jingle IBB +* ndreqje për saktësime të përsëritura që zënë vend te baza e të dhënave +* u kalua te Last Message Correction v1.1 diff --git a/fastlane/metadata/android/sq/changelogs/353.txt b/fastlane/metadata/android/sq/changelogs/353.txt new file mode 100644 index 000000000..fcf64418a --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/353.txt @@ -0,0 +1,4 @@ +* lejim i përdoruesve të caktojnë nofkën e vet +* rimarrje shkarkimi kartela të fshehtëzuara me OMEMO +* Kanali tani përdor '#' si simbol në një avatar +* Quicksy përdor “përherë”, si parazgjedhje për fshehtëzim OMEMO (e fsheh ikonën e kyçjes) diff --git a/fastlane/metadata/android/sq/changelogs/360.txt b/fastlane/metadata/android/sq/changelogs/360.txt new file mode 100644 index 000000000..7c5bcb13e --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/360.txt @@ -0,0 +1 @@ +* Mbulim për parametra URI XMPP ?register dher ?register;preauth diff --git a/fastlane/metadata/android/sq/changelogs/362.txt b/fastlane/metadata/android/sq/changelogs/362.txt new file mode 100644 index 000000000..c56753172 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/362.txt @@ -0,0 +1 @@ +* Mbulim për ndërrim të automatizuar teme në Android 10 diff --git a/fastlane/metadata/android/sq/changelogs/364.txt b/fastlane/metadata/android/sq/changelogs/364.txt new file mode 100644 index 000000000..675ba9b39 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/364.txt @@ -0,0 +1,2 @@ +* Sjellje e paraparjes për PDF në Android 5+ +* Përdor 12 byte IV për OMEMO diff --git a/fastlane/metadata/android/sq/changelogs/367.txt b/fastlane/metadata/android/sq/changelogs/367.txt new file mode 100644 index 000000000..9527158a7 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/367.txt @@ -0,0 +1,2 @@ +* Ndreqje përzgjedhjesh avatari në disa pajisje Android 10 +* Ndreqje shpërnguljesh kartelash për kartela të mëdha diff --git a/fastlane/metadata/android/sq/changelogs/379.txt b/fastlane/metadata/android/sq/changelogs/379.txt new file mode 100644 index 000000000..c39112cdb --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/379.txt @@ -0,0 +1 @@ +* Thirrje audio/video (Lyp mbulim nga shërbyesi në formë shërbyesish STUN dhe TURN të zbulueshëm përmes XEP-0215) diff --git a/fastlane/metadata/android/sq/changelogs/381.txt b/fastlane/metadata/android/sq/changelogs/381.txt new file mode 100644 index 000000000..d9bbe41fd --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/381.txt @@ -0,0 +1,2 @@ +* Sinjalizim i dëgjueshëm (rënie të zilesh, për fillim thirrjeje, për mbarim thirrjeje) për thirrje zanore. +* U ndreq problem me dështim riprovimi thirrjeje video diff --git a/fastlane/metadata/android/sq/changelogs/382.txt b/fastlane/metadata/android/sq/changelogs/382.txt new file mode 100644 index 000000000..30f6a0275 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/382.txt @@ -0,0 +1,2 @@ +* Shtim butoni për të ndërruar kamerën gjatë thirrjeje video +* U ndreqën thirrje me zë në tablete diff --git a/fastlane/metadata/android/sq/changelogs/383.txt b/fastlane/metadata/android/sq/changelogs/383.txt new file mode 100644 index 000000000..657191d83 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/383.txt @@ -0,0 +1,3 @@ +* Kalim i ikonës së thirrjes në të majtë, për të mbajtur ikonat e paneleve të tjerë në një vend të qëndrueshëm +* Shfaq kohëzgjatje thirrjeje gjatë thirrjesh audio +* Ndërprerje komunikimi për thirrje A/V (të njëjtët dy persona që i bëjnë thirrje njëri-tjetrit në të njëjtën kohë) diff --git a/fastlane/metadata/android/sq/changelogs/387.txt b/fastlane/metadata/android/sq/changelogs/387.txt new file mode 100644 index 000000000..23bb694eb --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/387.txt @@ -0,0 +1,2 @@ +* Ripunim i ndërfaqes për “Hyni me dëshmi” +* Shtim aftësie për të fiksuar fjalosje në krye (shtoje te të parapëlqyer) diff --git a/fastlane/metadata/android/sq/changelogs/388.txt b/fastlane/metadata/android/sq/changelogs/388.txt new file mode 100644 index 000000000..86ffab7a6 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/388.txt @@ -0,0 +1,3 @@ +* Zvogëlim jehone gjatë thirrjesh në disa pajisje +* Ndreqje hyrjeje, kur fjalëkalimet përmbajnë shenja speciale +* Luajtje tonesh për “rënie numri” dhe “i zënë” në altoparlant, gjatë thirrjesh video diff --git a/fastlane/metadata/android/sq/changelogs/390.txt b/fastlane/metadata/android/sq/changelogs/390.txt new file mode 100644 index 000000000..9849d5da0 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/390.txt @@ -0,0 +1 @@ +* Ofrim regjistrimi mesazhi zanor, kur i thirruri është i zënë diff --git a/fastlane/metadata/android/sq/changelogs/393.txt b/fastlane/metadata/android/sq/changelogs/393.txt new file mode 100644 index 000000000..4f5a7d3c3 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/393.txt @@ -0,0 +1,3 @@ +* Shfaqje butoni ndihme, nëse thirrja A/V dështon +* U ndreqën disa vithisje të bezdisshme +* U ndreqën lidhje Jingle (shpërngulje kartelash + thirrje) me JID-ra të zhveshura diff --git a/fastlane/metadata/android/sq/changelogs/394.txt b/fastlane/metadata/android/sq/changelogs/394.txt new file mode 100644 index 000000000..d4abc7a77 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/394.txt @@ -0,0 +1,2 @@ +* U ndreq mosshfaqje njoftimesh nën disa kushte +* U ndreqën probleme përputhshmërie dhe vithisje të lidhura me thirrje A/V diff --git a/fastlane/metadata/android/sq/changelogs/395.txt b/fastlane/metadata/android/sq/changelogs/395.txt new file mode 100644 index 000000000..805779d98 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/395.txt @@ -0,0 +1,3 @@ +* shtim “Kthehuni te fjalosje”, te skena e thirrjeve audio +* Përmirësim shkurtoresh tastiere +* ndreqje të metash diff --git a/fastlane/metadata/android/sq/changelogs/397.txt b/fastlane/metadata/android/sq/changelogs/397.txt new file mode 100644 index 000000000..611099b0e --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/397.txt @@ -0,0 +1,3 @@ +* Shtim aftësie për trajtim kartelash GPX +* Përmirësim funksionimi për rikthim me kopjeruajtje +* ndreqje të metash diff --git a/fastlane/metadata/android/sq/changelogs/398.txt b/fastlane/metadata/android/sq/changelogs/398.txt new file mode 100644 index 000000000..4b1fccd3a --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/398.txt @@ -0,0 +1,4 @@ +* Kërkim te biseda individuale +* Njoftim përdoruesish, nëse dështon dërgimi i mesazhit +* Mbajtje mend i emrave në ekran (nofkave) prej përdoruesish të Quicksy-t nga një rinisje në tjetrën +* Shtim butoni për nisje të Orbot-it (Tor) që nga njoftim, në qoftë e nevojshme diff --git a/fastlane/metadata/android/sq/changelogs/401.txt b/fastlane/metadata/android/sq/changelogs/401.txt new file mode 100644 index 000000000..af9886b3b --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/401.txt @@ -0,0 +1,2 @@ +* u ndreq kërkim në Android <= 5 +* optimizim konsumi kujtese diff --git a/fastlane/metadata/android/sq/changelogs/402.txt b/fastlane/metadata/android/sq/changelogs/402.txt new file mode 100644 index 000000000..3a0763bc8 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/402.txt @@ -0,0 +1,3 @@ +* Ofrim prodhimi Ftese të Kollajtë, për shërbyes që e mbulojnë +* Shfaqje GIF-esh dërguar prej Movim +* depozitim avatarësh në fshehtinë diff --git a/fastlane/metadata/android/sq/changelogs/403.txt b/fastlane/metadata/android/sq/changelogs/403.txt new file mode 100644 index 000000000..5c048809a --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/403.txt @@ -0,0 +1,3 @@ +* U ndreqën probleme lidhjeje, kur llogari të ndryshme përdornin mekanizma të ndryshëm SCRAM +* Shtim mbulimi për SCRAM-SHA-512 +* Lejim shpërngulje kartelash P2P (Jingle) me kontakt veten diff --git a/fastlane/metadata/android/sq/changelogs/404.txt b/fastlane/metadata/android/sq/changelogs/404.txt new file mode 100644 index 000000000..ee060d51c --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/404.txt @@ -0,0 +1 @@ +* përmirësime të vockla qëndrueshmërie për thirrje A/V diff --git a/fastlane/metadata/android/sq/changelogs/405.txt b/fastlane/metadata/android/sq/changelogs/405.txt new file mode 100644 index 000000000..5d526d00b --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/405.txt @@ -0,0 +1 @@ +* Quicksy: Marrje e automatizuar SMS-je verifikimi diff --git a/fastlane/metadata/android/sq/changelogs/407.txt b/fastlane/metadata/android/sq/changelogs/407.txt new file mode 100644 index 000000000..7d06acadc --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/407.txt @@ -0,0 +1,3 @@ +* Shfaq buton thirrjeje, për kontakte jo në linjë, nëse kanë deklaruar më parë mbulim të kësaj +* Butoni “mbrapsht” nuk përfundon më thirrjen, kur thirrja është e lidhur +* ndreqje të metash diff --git a/fastlane/metadata/android/sq/changelogs/42000.txt b/fastlane/metadata/android/sq/changelogs/42000.txt new file mode 100644 index 000000000..12a868871 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/42000.txt @@ -0,0 +1,4 @@ +* Aftësi për të përzgjedhur zilen për thirrje ardhëse +* Ndreqje pikasjeje ID-je kyçi OpenPGP për OpenKeychain 5.6+ +* Verifikim si duhet dëshmish TLS punycode +* Përmirësim qëndrueshmërie vendosjeje sesioni RTP (thirrje) diff --git a/fastlane/metadata/android/sq/changelogs/42006.txt b/fastlane/metadata/android/sq/changelogs/42006.txt new file mode 100644 index 000000000..184483c28 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/42006.txt @@ -0,0 +1,2 @@ +* Verifikim thirrjesh A/V me sesioni OMEMO që ekzistojnë që më parë +* Përmirësim përputhshmërie me sendërtime WebRTC jo të bazuara libwebrt diff --git a/fastlane/metadata/android/sq/changelogs/42010.txt b/fastlane/metadata/android/sq/changelogs/42010.txt new file mode 100644 index 000000000..0b861e242 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/42010.txt @@ -0,0 +1,2 @@ +* Ndreqje të metash të ndryshme që lidhen me mbulim Tor-i +* Përmirësim përputhshmërie thirrjesh me Dino diff --git a/fastlane/metadata/android/sq/changelogs/42012.txt b/fastlane/metadata/android/sq/changelogs/42012.txt new file mode 100644 index 000000000..c7bc07f2b --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/42012.txt @@ -0,0 +1 @@ +* Ndreqje ngarkimesh/shkarkime HTTP për përdorues që nuk besojnë AD-ra sistemi diff --git a/fastlane/metadata/android/sq/changelogs/42013.txt b/fastlane/metadata/android/sq/changelogs/42013.txt new file mode 100644 index 000000000..8ee028730 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/42013.txt @@ -0,0 +1 @@ +* U ndreqën probleme “S’ka Lidhje”, për Android 7.1 diff --git a/fastlane/metadata/android/sq/changelogs/42014.txt b/fastlane/metadata/android/sq/changelogs/42014.txt new file mode 100644 index 000000000..7604d0749 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/42014.txt @@ -0,0 +1,2 @@ +* Verifikim përherë i emrit të përkatësisë. Pa mbishkrim përdoruesi +* Mbulim për mirëfilltësim paraprak liste përdoruesish diff --git a/fastlane/metadata/android/sq/changelogs/42015.txt b/fastlane/metadata/android/sq/changelogs/42015.txt new file mode 100644 index 000000000..fb51e49f7 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/42015.txt @@ -0,0 +1 @@ +* përmirësime të vockla A/V diff --git a/fastlane/metadata/android/sq/changelogs/42018.txt b/fastlane/metadata/android/sq/changelogs/42018.txt new file mode 100644 index 000000000..125dc4b61 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/42018.txt @@ -0,0 +1,3 @@ +* Shfaq shtylla të zeza, kur videoja nga larg nuk përputhet me përpjesëtimet e ekranit +* Përmirësim funksionimi kërkimi +* Shtim rregullimi për parandalim fotosh ekrani diff --git a/fastlane/metadata/android/sq/changelogs/42022.txt b/fastlane/metadata/android/sq/changelogs/42022.txt new file mode 100644 index 000000000..281589a64 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/42022.txt @@ -0,0 +1,2 @@ +* Ndreqje problemi moskryerje ngjeshjeje për disa nga videot +* Ndreqje vithisjeje të rrallë, kur hapet njoftim diff --git a/fastlane/metadata/android/sq/changelogs/42023.txt b/fastlane/metadata/android/sq/changelogs/42023.txt new file mode 100644 index 000000000..61b5672af --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/42023.txt @@ -0,0 +1,2 @@ +* Ndreqje vithisjeje, kur shfaqen disa lloje thonjëzash +* Ndreqje vithisjeje te skena e mirëseardhjes diff --git a/fastlane/metadata/android/sq/changelogs/42037.txt b/fastlane/metadata/android/sq/changelogs/42037.txt new file mode 100644 index 000000000..35e334aad --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/42037.txt @@ -0,0 +1,8 @@ +Version 2.10.9 +* Kërko leje Bluetooth, kur bëhen thirrje A/V (Mund ta hidhni tej këtë, nëse s’përdorni kufje Bluetooth me mikrofon) +* Ndreqje të mete, kur thirret dikush në Movim +* Ndreqje shfaqjeje avatari gabim për fjalosje në grup +* Pyet përherë për lënie jashtë optimizimesh për baterinë +* Vendosje flamurke “vetëm vendore” për njoftime “x llogari të lidhura” +* Ndreqje ndërveprimi me shtojcën Google Maps Share Location Plugin +* Heqje poshtëshënimi lidhur me tarifa shërbyesi diff --git a/fastlane/metadata/android/sq/changelogs/42038.txt b/fastlane/metadata/android/sq/changelogs/42038.txt new file mode 100644 index 000000000..32d84634b --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/42038.txt @@ -0,0 +1,2 @@ +* Ndreqje të metash të vockla +* Rikthim i aftësisë për të bërë thirrje përmes JMP-je dhe shërbimesh të tjera (versioni në Playstore) diff --git a/fastlane/metadata/android/sq/changelogs/42041.txt b/fastlane/metadata/android/sq/changelogs/42041.txt new file mode 100644 index 000000000..65f27b7e2 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/42041.txt @@ -0,0 +1,5 @@ +* Sendërtim profili Implement Extensible SASL, Bind 2.0 dhe Fast, për rilidhje më të shpejta +* Sendërtim i “channel binding” +* Shtim aftësie për të kaluar nga thirrje audio në thirrje video +* Shtim aftësie për të fshirë avatarin e vetes +* Shtim njoftimi për thirrje të humbura diff --git a/fastlane/metadata/android/sq/changelogs/42042.txt b/fastlane/metadata/android/sq/changelogs/42042.txt new file mode 100644 index 000000000..bbdd93772 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/42042.txt @@ -0,0 +1,2 @@ +* Ndreqje qerthulli ridërgimi në shërbyes që mbulojnë vetëm sm:2 +* “Kalo në video” shfaqe vetëm nëse pala tjetër mbulon video diff --git a/fastlane/metadata/android/sq/changelogs/42043.txt b/fastlane/metadata/android/sq/changelogs/42043.txt new file mode 100644 index 000000000..af4821614 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/42043.txt @@ -0,0 +1 @@ +* Ndreqje prapakthimi në shpërngulje P2P kartelash diff --git a/fastlane/metadata/android/sq/changelogs/42044.txt b/fastlane/metadata/android/sq/changelogs/42044.txt new file mode 100644 index 000000000..6fa07e806 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/42044.txt @@ -0,0 +1,3 @@ +* Ndreqje dërgim sërish i mesazheve, kur përdoret SASL2 +* Ndreqje nxirje të videos mes disa pajisjesh +* Ndreqje vithisje kur jepen fjalëkalime të zbrazët diff --git a/fastlane/metadata/android/sq/changelogs/42046.txt b/fastlane/metadata/android/sq/changelogs/42046.txt new file mode 100644 index 000000000..e4f28ef07 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/42046.txt @@ -0,0 +1 @@ +* Integrim “UnifiedPush Distributor” për të lehtësuar mesazhe push për te aplikacione të tjerë që munden të përdorin UnifiedPush, bie fjala, Tusky dhe Fedilab diff --git a/fastlane/metadata/android/sq/changelogs/42047.txt b/fastlane/metadata/android/sq/changelogs/42047.txt new file mode 100644 index 000000000..d26dd71bb --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/42047.txt @@ -0,0 +1 @@ +* Ndreqje vithisjeje te UnifiedPush Distributor diff --git a/fastlane/metadata/android/sq/short_description.txt b/fastlane/metadata/android/sq/short_description.txt index 6b4e97300..bec029626 100644 --- a/fastlane/metadata/android/sq/short_description.txt +++ b/fastlane/metadata/android/sq/short_description.txt @@ -1 +1 @@ -Shkëmbyes XMPP mesazhesh të atypëratyshëm, i fshehtëzuar, i lehtë në përdorim, për pajisjen tuaj celulare +Shkëmbyes XMPP mesazhesh aty për aty, i fshehtëzuar, i kollajtë, për celular From 8c99d84826a8da1c157eeb08baaba6778362557d Mon Sep 17 00:00:00 2001 From: random_r Date: Fri, 13 Jan 2023 14:46:08 +0000 Subject: [PATCH 373/394] Translated using Weblate (Italian) Currently translated at 100.0% (962 of 962 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/it/ --- src/main/res/values-it/strings.xml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index 58104e5e0..cc00e3562 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -548,7 +548,8 @@ Condividi URI con…
Ti registri con il tuo numero di telefono e Quicksy ti suggerirà—in base ai numeri di telefono nella tua rubrica—automaticamente i possibili contatti.

Registrandoti accetti la nostra politica sulla privacy.]]>
Accetta e continua - È disponibile una guida per la creazione di un profilo su conversations.im.¹\nQuando scegli conversations.im come fornitore potrai comunicare con utenti di altri fornitori dando il tuo indirizzo XMPP completo. + È disponibile una guida per la creazione di un profilo su conversations.im. +\nQuando scegli conversations.im come fornitore potrai comunicare con utenti di altri fornitori dando il tuo indirizzo XMPP completo. Il tuo indirizzo XMPP completo sarà: %s Crea profilo Usa un altro fornitore @@ -1014,4 +1015,10 @@ Le chiamate sono disattivate quando si usa Tor Passa al video Rifiuta richiesta di passare al video + Distributore di UnifiedPush + Profilo XMPP + Il profilo attraverso cui verranno ricevuti i messaggi push. + Server push + Un server scelto dall\'utente per inoltrare i messaggi push via XMPP al tuo dispositivo. + Nessuno (disattivato) \ No newline at end of file From 3350df753d3196f2f4e1a1c7a94353e25bc77b17 Mon Sep 17 00:00:00 2001 From: Besnik_b Date: Fri, 13 Jan 2023 12:30:09 +0000 Subject: [PATCH 374/394] Translated using Weblate (Albanian) Currently translated at 81.8% (787 of 962 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/sq/ --- src/main/res/values-sq-rAL/strings.xml | 829 ++++++++++++++++++++++++- 1 file changed, 827 insertions(+), 2 deletions(-) diff --git a/src/main/res/values-sq-rAL/strings.xml b/src/main/res/values-sq-rAL/strings.xml index c757504ac..e9de59f54 100644 --- a/src/main/res/values-sq-rAL/strings.xml +++ b/src/main/res/values-sq-rAL/strings.xml @@ -1,2 +1,827 @@ - - + + + Bisedë e re + Administroni llogari + Administroni llogari + Mbylle bisedën + Hollësi kontakti + Hollësi fjalosjeje grupi + Hollësi kanali + Shtoni llogari + Përpunoni emër + Shtoje te libër adresash + Bllokojeni kontaktin + Zhbllokoje kontaktin + Blloko përkatësin + Zhbllokoje përkatësinë + Blloko pjesëmarrësin + Zhbllokoje pjesëmarrësin + Administroni Llogari + Formësime + Jepe përmes Conversation + Nisni Bisedë + Zgjidhni Kontakt + Zgjidhni Kontakte + Jepe përmes llogarie + mu tani + 1 minutë më parë + %d minuta më parë + po dërgohet… + Po shfshehtëzohet mesazhi. Ju lutem prisni… + Mesazhi i fshehtëzuar me OpenPGP + Nofka është tashmë e regjistruar + Nofkë e pavlefshme + Përgjegjës + I zoti + Moderator + Pjesëmarrës + Vizitor + Të bllokohen krejt kontaktet nga %s\? + Të zhbllokohen krejt kontaktet prej %s\? + Kontakti u bllokua + Bllokuar + Regjistroni llogari të re në shërbyes + Ndryshoni fjalëkalim te shërbyesi + Ndajeni me… + Nisni bisedë + Ftoni kontakt + Ftoni + Kontakte + Kontakt + Anuloje + Shtoni + Përpunojeni + Fshije + Bllokoje + Zhbllokoje + Ruaje + OK + %1$s u vithis + Dërgoje tani + Mos ripyet më kurrë + S’u lidh dot te llogaria + S’u lidh dot te llogari të shumta + Prekeni, që të administroni llogaritë tuaja + Bashkëngjitni kartelë + Të shtohet te lista juaj e kontakteve ky kontakt që mungon\? + Shtoni kontakt + dërgimi dështoi + Po bëhet gati për dërgim figure + Po bëhet gati për dërgim figurash + Po jepen kartela. Ju lutemi, prisni… + Spastro historikun + Spastoni Historik Bisedash + Fshije kartelën + Mbylle këtë bisedë më pas + Zgjidhni pajisje + Dërgoni mesazh të pafshehtëzuar + Dërgoni mesazh + Dërgoje mesazhin për %s + Dërgoni një mesazh të fshehtëzuar me OMEMO + Dërgoni një mesazh të fshehtëzuar me v\\OMEMO + Dërgoni një mesazh të fshehtëzuar me OpenPGP + Nofkë e re në përdorim + Dërgoje të pafshehtëzuar + Instaloje + Ju lutemi, instaloni OpenKeychain + po ofrohet… + po pritet… + S’u gjet Kyç OpenPGP + S’u gjetën Ke OpenPGP + Të Përgjithshme + Prano kartela + Bashkëngjitje + Njoftim + Dridhu + Dridhu, kur mbërrin mesazh i ri + Njoftim LED + Zile + Tingull njoftimi + Tingull njoftimi për mesazhe të rinj + Zile për thirrje ardhëse + Të mëtejshme + Mos dërgo kurrë njoftime vithisjesh + Ripohoni Mesazhe + Pengo Foto Ekrani + UI + OpenKeychain prodhoi një gabim. + Kyç i pavlefshëm për fshehtëzim. + Pranoje + Ndodhi një gabim + Gabim + Llogaria juaj + Dërgo përditësime pranie + Merr përditësime pranie + Pyet për përditësime pranie + Zgjidhni foto + Bëni një foto + Kartela që përzgjodhët s’është figurë + S’u gjet kartelë + E panjohur + Përkohësisht i çaktivizuar + I lidhur + Po lidhet… + I shkëputur + E paautorizuar + S’u gjet shërbyes + Pa lidhje + Regjistrimi dështoi + Emër përdoruesi tashmë në përdorim + Regjistrimi u plotësua + Regjistrim i pambuluar nga shërbyesi + Token-i i pavlefshëm regjistrimi + Tratativa TLS dështoi + Përkatësi: e paverifikueshme + Dhunim rregullash + Shërbyes i papërputhshëm + Klient jo i përputhshëm + Gabim rrjedhe + Gabim në hapjen e rrjedhës + Të pafshehtëzuara + OTR + OpenPGP + OMEMO + Fshije llogarinë + Çaktivizoje përkohësisht + Publikoje avatarin + Publikoni kyçin publik OpenPGP + Hiq kyç publik OpenPGP + Kyçi publik OpenPGP u bë publik. + Aktivizoje llogarinë + Jeni i sigurt\? + Incizoni zë + Adresë XMPP + Bllokoj adresë XMPP + username@example.com + Fjalëkalim + Kjo s’është adresë XMPP e vlefshme + Mbaroi kujtesa. Figurë shumë e madhe + Doni të shtohet %s te libri juaj i adresave\? + Hollësi shërbyesi + XEP-0313: MAM + XEP-0198: Administrim Rrjedhe + XEP-0163: PEP (Avatarë / OMEMO) + XEP-0363: Ngarkim Kartelash HTTP + jo i passhëm + parë së fundi mu tani + parë së fundi një minutë më parë + parë së fundi %d minuta më parë + parë së fundi një orë më parë + parë së fundi %d orë më parë + parë së fundi një ditë më parë + parë së fundi %d ditë më parë + U gjetën mesazhe të rinj të fshehtëzuar me OpenPGP + ID Kyçi OpenPGP + Shenja gishtash OMEMO + Shenja gishtash v\\OMEMO + Pajisje të tjera + Beso Shenja Gishtash OMEMO + Po sillen kyçe… + U bë + Shfshehtëzoje + Faqerojtës + Kërko + Jepni Kontakt + Fshije kontaktin + Shihni hollësi kontakti + Bllokojeni kontaktin + Zhbllokoje kontaktin + Krijoje + Përzgjidhni + Kontakti ekziston tashmë + Hyni + kanal@konferencë.shembull.com/nofkë + Ruaje si faqerojtës + Fshije faqerojtësin + Asgjëso fjalosje grupi + Asgjësoje kanalin + S’u asgjësua dot fjalosje në grup + S’u asgjësua dot kanali + Përpunoni subjekt fjalosjeje në grup + Po hyhet në fjalosje grupi… + Dil + Kontakti u shtua te listë kontaktesh + Rishtoje + %s ka lexuar deri në këtë pikë + %s ka lexuar deri në këtë pikë + Po publikohet… + Shërbyesi hodhi poshtë urdhrin publikimin tuaj + S’u shndërrua dot fotoja juaj + S’u ruajt dot avatari në disk + Shërbyesi juaj nuk mbulon publikim avatarësh + pëshpëriti + për %s + Dërgo mesazh privat te %s + Lidhe + Ka tashmë një llogari të tillë + Pasuesi + U vendos sesion + Anashkaloje + Çaktivizo njoftimet + Aktivizoje + Fjalosja në grupi lyp fjalëkalim + Jepni fjalëkalim + Kërkoje tani + Shpërfille + Siguri + Lejo ndreqje mesazhi + Rregullime ekspertësh + Ju lutemi, hapni sytë me këto + Mbi %s + Orë të Qeta + Kohë fillimi + Kohë përfundimi + Aktivizoni orë të qeta + Tjetër + Njëkohëso faqerojtës + U kopjuan në të papastër shenja gishtash OMEMO + Jeni dëbuar nga kjo fjalosje në grup + Kjo fjalosje në grup është vetëm për anëtarë + Kufizim burimesh + Jepni përzënë nga kjo fjalosje në grup + Fjalosje në grup qe mbyllur + S’jeni më te kjo fjalosje grupi + duke përdorur llogari %s + strehuar në %s + Po kontrollohet %s te strehë HTTP + S’jeni i lidhur. Riprovoni më vonë + Kontrolloni madhësi %s + Kontrolloni madhësinë e %1$s në %2$s + Mundësi mesazhi + Ngjite si citim + Kopjoji URL-në origjinale + Ridërgoje + URL kartele + URL-ja u kopjua në të papastër + Adresa XMPP u kopjua në clipboard + Mesazhi i gabimit u kopjua në të papastër + adresë web + Skano Kod me vija 2D + Shfaq Kod me vija 2D + Shfaqe listë bllokimesh + Hollësi llogarie + Ripohojeni + Riprovoni + Shërbim në prapaskenë + Krijo kopjeruajtje + Kartelat kopjeruajtje do të depozitohen në %s + Po krijohen kartela kopjeruajtje + Kopjeruajtja juaj u krijua + Po rikthehet kopjeruajtje + Kopjeruajtja juaj u rikthye + Mos harroni të aktivizoni llogarinë. + Zgjidhni kartelë + Po merret %1$s (plotësuar %2$d%%) + Shkarkoni %s + Fshije %s + kartelë + Hap %s + po dërgohet (plotësuar %1$d%%) + Po bëhet gati për dhënie kartele + %s ofruar për shkarkim + s’u nda dot kartelë me të tjerë + shpërngulje kartelash e anuluar + Kartela u fshi + S’u gjet aplikacion për hapje të kartelës + S’u gjet aplikacion për hapje të lidhjes + S’u gjet aplikacion për të parë kontakte + Etiketa Dinamike + Aktivizo njoftimet + S’u gjet shërbyes fjalosjeje grupi + S’u krijua dot fjalosje grupi + Avatar llogarie + Kopjoje shenja gishtash OMEMO në të papastër + Riprodho kyç OMEMO + Spastro pajisje + Diç shkoi ters + Po sillet historik prej shërbyesi + S’ka historik tjetër në shërbyes + Po përditësohet… + Fjalëkalimi u ndryshua! + Fjalëkalimi s’u ndryshua dot + Ndryshoni fjalëkalimin + Fjalëkalimi i tanishëm + Fjalëkalim i ri + Fjalëkalimi s’mund të jetë i zbrazët + Aktivizo krejt llogaritë + Çaktivizo krejt llogaritë + Kryeje veprimin me + Pa përshoqërim + I shkëputur + Anëtar + Mënyrë e thelluar + Akordojini privilegje anëtari + Shfuqizoni privilegje anëtari + Akordoni privilegje përgjegjësi + Shfuqizoni privilegje përgjegjësi + Akordojini privilegje të zoti + Shfuqizoni privilegje të zoti + Hiqe prej fjalosjeje grupi + Hiqe prej kanali + S’u ndryshua dot përshoqërim i %s + Dëboje nga fjalosje në grup + Dëboje nga kanali + Dëboje tani + S’u ndryshua dot roli i %s + Formësim fjalosje private në grup + Formësim kanali publik + Private, vetëm për anëtarë + S’po merrni pjesë + U ndryshuan mundësi fjalosjeje në grup! + Kurrë + Deri sa të jepet njoftim tjetër + Përgjigjuni + Vëri shenjë si të lexuar + Tasti Enter bën dërgimin + Shfaq tastin Enter + Ndryshoje tastin e emotikoneve si tast Enter + audio + video + figurë + grafik vektorial + kartelë multimedia + Dokument PDF + Aplikacion + Kontakt + Avatari u bë publik! + Po ofrohet %s + %s po shkruan… + %s ka reshtur së shkruari + %s po shkruajnë… + %s ka reshtur së shkruari + Njoftime shtypjesh + Dërgo vendndodhjen + Shfaq vendndodhje + S’u gjet aplikacion për shfaqje të vendndodhjes + Vendndodhje + Biseda u mbyll + Braktisi fjalosje private në grup + Braktisi kanal majtas + Krejt dëshmitë duhet të miratohen dorazi + Hiqni dëshmi + Fshi dëshmi të miratuara dorazi + S’u dhanë dëshmi dorazi + Hiqi dëshmitë + Fshije përzgjedhjen + Anuloje + Veprim i Shpejtë + Asnjë + Më të përdorur së fundi + Zgjidhni veprim të shpejtë + Kërko te kontaktet + Kërko te faqerojtës + Dërgo mesazh privat + %1$s e braktisi fjalosjen në grup + Emër përdoruesi + Emër përdoruesi + Ky s’është emër i vlefshëm përdoruesi + Shkarkimi dështoi: S’u gjet shërbye + Shkarkimi dështoi: S’u gjet kartelë + Shkarkimi dështoi: S’u lidh dot te strehë + Shkarkimi dështoi: S’u arrit të shkruhej te kartelë + Shkarkimi dështoi: Kartelë e pavlefshme + Rrjet Tor jo në punë + Dështim lidhjeje + Shërbyesi s’është përgjegjës për këtë përkatësi + I dëmtuar + “I larguar”, kur pajisja është e kyçur + Shfaqmë si “I lraguar”, kur pajisja është e kyçur + Shfaqmë si “I zënë”, kur pajisja është në dridhje + Rregullime të zgjeruara lidhjeje + Hyni me dëshmi + S’u analizua dot dëshmi + Parapëlqime arkivimi + Parapëlqime arkivimi më anë të shërbyesit + S’u prunë dot parapëlqime arkivimi + Zgjidhja e CAPTCHA-s është e domosdoshme + Jepni tekstin prej figurës më sipër + Varg jo i besuar dëshmish + Adresa XMPP s’përputhet me dëshminë + Rinovoni dëshminë + Gabim në sjellje kyçi OMEMO! + U verifikua kyç OMEMO me dëshmi! + Lidhje + Lidhu përmes Tor-i + Strehëemër + Portë + Ky s’është numër i vlefshëm porte + Ky s’është një strehëemër i vlefshëm + Njëkohësoje me kontaktet + Njofto për krejt mesazhet + Njoftomë vetëm kur përmendem + Njoftime të çaktivizuara + Njoftime të ndalura + Ngjeshje Figure + Përherë + Vetëm figura të mëdha + Me optimizime baterie të aktivizuara + Çaktivizoje + Zona e përzgjedhur është shumë e madhe + (Pa llogari të aktivizuara) + Kjo fushë është e domosdoshme + Ndreqe mesazhin + Dërgo mesazhin e saktësuar + E keni çaktivizuar këtë llogari + Gabim sigurie: Hyrje e pavlefshme te kartelë! + S’u gjet aplikacion për të dhënë URI + Jepjani URI-n… + Pajtohuni dhe vazhdoni + Adresa juaj e plotë XMPP do të jetë: %s + Krijo Llogari + Përdorni shërbimin tuaj + Zgjidhni emrin tuaj si përdorues + Administrojeni dorazi praninë + Mesazh gjendjeje + I lirë për Fjalosje + Në linjë + I larguar + I zënë + U prodhua një fjalëkalim i siguruar + Regjistrimi dështoi: Riprovoni më vonë + Regjistrimi dështoi: Fjalëkalim shumë i dobët + Zgjidhni pjesëmarrës + Po krijohet fjalosje në Grup… + Riftojeni + Çaktivizoje + E shkurtër + Mesatare + E gjatë + Përdorim transmetimi + Privatësi + Temë + Përzgjidhni paletën e ngjyrave + Automatike + E çelët + E errët + Sfond i gjelbër + S’u lidh dot te OpenKeychain + Kjo pajisje s’është më në përdorim + Kompjuter + Telefon celular + Tablet + Shfletues + Konsolë + Lypset pagesë + Akordo leje për përdorim të Internetit + Unë + Kontakti kërkon pajtim pranie + Lejoje + S’ka leje për përdorim të %s + S’u gjet shërbyes i largët + Mbarim kohe shërbyesi të largët + S’u përditësua dot llogaria + Raportojeni këtë adresa për mesazhe të padëshiruar. + Fshini identitete OMEMO + Fshi kyçet e përzgjedhur + Kopjo shenja gishtash + Kodi me vija nuk përmban shenja gishtash për këtë bisedë. + Shenja gishtash të verifikuar + Përdorni kamerën e telefonit tuaj që të skanoni një kod me vija + Ju lutemi, pritni të sillen kyçet + Ndajeni si Kod me vija + Jepe si URI XMPP + Ndajeni me të tjerë si lidhje HTTP + Besim i Verbër Para Verifikimi + Jo i besuar + Kod 2D me vija i pavlefshëm + Spastro fshehtinën + Spastro depozitë private + Vazhdoni + Verifikoni kyçe OMEMO + Shfaq joaktive + Mos e beso pajisjen + + %d minutë + %d minuta + + + %d orë + %d orë + + + %d ditë + %d ditës + + + %d javë + %d javë + + Fshirje e automatizuar mesazhesh + Po fshehtëzohet meszhi + Po ngjishet video + Biseda përkatëse u mbyll. + Kontakti u bllokua. + Njoftime prej të panjohurish + U mor mesazh prej të panjohuri + Blloko të huaj + Bllokoje krejt përkatësinë + në linjë mu tani + Riprovo shfshehtëzimin + Dështim sesioni + Shërbyesi kërkon doemos regjistrim përmes një sajti + Hap sajtin + S’u gjetën aplikacione për hapjen e sajtit + Sot + Dje + Vlerësoj strehemër me DNSSEC + të pjesshme + Regjistroni video + Kopjoje në të papastër + Mesazhi u kopjua në të papastër + Mesazh + Mesazhet private janë të çaktivizuara + Aplikacione të Mbrojtur + Të pranohet Dëshmi e Panjohur\? + Dëshmia e shërbyesit s’është nënshkruar prej një Autoriteti të njohur Dëshmish. + Të Pranohet Emër Shërbyesi i Ngatërruar\? + Doni të lidheni, sido qoftë\? + Hollësi dëshmie: + Një herë + Që të skanojë kodin QR, skanerit i duhet leje përdorimi të kamerës + Rrëshqitni drejt fundit + Përpunoni Mesazh gjendjeje + Përpunoni mesazh gjendjeje + Çaktivizo fshehtëzimin + S’u soll dot listë pajisjesh + S’u sollën dot kyçe fshehtëzimi + Çaktivizoje tani + Skicë: + Fshehtëzim OMEMO + Krijoni Shkurtore + Madhësi Shkronjash + On, si parazgjedhje + Off, si parazgjedhje + Mesatare + E madhe + Mesazhi s’qe fshehtëzuar për këtë pajisje. + S’’u arrit të shfshehtëzohet mesazh fshehtëzuar me OMEMO. + zhbëje + Tregimi i vendndodhjes është i çaktivizuar + Ndreqe pozicionin + Shfiksoje pozicionin + Kopjo Vendndodhjen + Jepe Vendndodhjen + Drejtime + Jepe vendndodhjen + Shfaq vendndodhje + Ndajeni me të tjerë + S’u fillua dot regjistrim + Ju lutemi, prisni… + Akordoni hyrje %1$s për mikrofonin + Kërko te mesazhet + GIF + Shihni bisedë + Shtojcë Tregimi Vendndodhjeje + Kopjo adresë web + Kopjo adresë XMPP + Dhënie Kartelash HTTP për S3 + Kërkim i Drejtpërdrejtë + Avatar fjalosjeje në grup + Emër kontakti + Nofkë + Emër + Jepni një emër, nëse doni + Emër fjalosjeje në grup + Kjo fjalosje në grup është asgjësuar + S’u ruajt dot regjistrim + Shërbim në pjesën e dukshme + Hollësi Gjendjeje + Probleme Lidhjeje + Mesazhe + Thirrje + Mesazhe + Thirrje ardhëse + Thirrje në kryerje e sipër + Thirrje të humbura + Mesazhe heshtazi + Dërgime të dështuar + Rregullime njoftimesh mesazhesh + Rregullime njoftimesh për thirrje ardhëse + Rëndësi, Tingull, Dridhje + Ngjeshje videoje + Shihni media + Pjesëmarrës + Shfletues mediash + Cilësi Video + Mesatare (360p) + E lartë (720p) + anuluar + Po skiconi tashmë një mesazh. + Veçori e pasendërtuar + Kod i pavlefshëm vendi + Zgjidhni vend + numër telefoni + Verifikoni numrin tuaj të telefonit + %s s’është numër telefoni i vlefshëm. + Ju lutemi, jepni numrin e telefonit tuaj. + Kërko te vendet + Verifikoni %s + Ridërgo SMS + Ridërgo SMS (%s) + Ju lutemi, pritni (%s) + mbrapsht + Jeni i sigurt se doni të ndëpritet procedura e regjistrimit\? + Po + Po verifikohet… + PIN-i që dhatë është i pasaktë. + Gabim i panjohur rrjeti. + Përgjigje e panjohur nga shërbyesi. + S’u lidh dot te shërbyesi. + S’u vendos dot një lidhje të sigurt. + S’u gjet dot shërbyes. + Diç shkoi ters gjatë përpunimit të kërkesës tuaj. + Dhënie e pavlefshme nga përdoruesi + Përkohësisht i pakapshëm. Riprovoni më vonë. + S’ka lidhje rrjeti. + Ju lutemi, riprovoni pas %s + Shumë përpjekje + Përditësoje + Emri juaj + Jepni emrin tuaj + Hidhe poshtë kërkesën + Instaloni Orbot-in + Nisni Orbot-in + e-libër + Origjinalja (e pangjeshur) + Hape me… + Foto profili Conversations + Zgjidhni llogari + Riktheje kopjeruajtjen + Riktheje + S’u rikthye dot kopjeruajtje. + S’u shfehtëzua dot kopjeruajtje. A është i saktë fjalëkalimi\? + Kopjeruani & Riktheni + Jepni adresë XMPP + Krijoni fjalosje grupi + Hyni në kanal publik + Krijoni fjalosje private në grup + Krijoni kanal publik + Emër kanali + Adresë XMPP + Ju lutemi, jepni një emër për kanalin + Ju lutemi, jepni një adresë XMPP + Kjo është një adresë XMPP. Ju lutemi, jepni një emër. + Po krijohet kanal publik… + Ky kanal ekziston tashmë + Hytë në një kanal ekzistues + S’u ruajt dot formësim kanali + Lejo këdo të përpunojë temën + Lejo këdo të ftojë të tjerë + Cilido mund të përpunojë temën. + Të zotët mund të përpunoni subjektin. + Përgjegjësit mund të përpunojnë temën. + Të zotët mund të ftojnë të tjerë. + Cilido mund të ftojë të tjerë. + Adresat XMPP janë të dukshme për përgjegjës. + Adresat XMPP janë të dukshme për këdo. + Kjo fjalosje private në grup s’ka anëtarë. + Administroni privilegje + Kërko te pjesmarrës + Kartelë shumë e madhe + Bashkëngjite + Zbuloni kanale + Kërko te kanale + Cenim potencial privatësie! + Kam tashmë një llogari + Shtoni llogari ekzistuese + Regjistroni llogari të re + Kjo duket si adresë përkatësie + Shtoje sido qoftë + Ndani me të tjerë kartela kopjeruajtjesh + Kopjeruajtje bisede + Akt + Hap kopjeruajtje + Kartela që përzgjodhët s’është kartelë kopjeruajtjeje Conversations + Kjo llogari është ujdisur tashmë + Ju lutemi, jepni fjalëkalimin për këtë llogari + S’u krye dot ky veprim + Fjalosje në Grup & Kanale + Shërbyes vendor + Metodë zbulimi kanalesh + Kopjeruajtje + Mbi + Ju lutemi, aktivizoni një llogari + Bëni thirrje + Thirrje ardhëse + Thirrje video ardhëse + Të kalohet në thirrje video\? + Po lidhet + I lidhur + Po rilidhet + Po pranohet thirrja + Po përfundohet thirrja + Përgjigje + Hidhe tej + Po pikasen pajisje + Po i bihet ziles + I zënë + S’u bë dot lidhje e thirrjes + Humbi lidhja + Thirrje e tërhequr + Dështim aplikacioni + Problem verifikimi + Mbylle + Thirrje në kryerje e sipër + Thirrje video në kryerje e sipër + Po rilidhet thirrja + Po rilidhet thirrje video + Që të bëni thirrje, çaktivizoni Tor-in + Thirrje ardhëse + Thirrje ardhëse · %s + Thirrje e humbur · %s + Thirrje për + Thirrje për · %s + Thirrje e humbur + + %1$d thirrje e humbur, prej %2$s + %1$d thirrje të humbura, prej %2$s + + + %d thirrje e humbur + %d thirrje të humbura + + Mikrofoni juaj s’është i përdorshëm + Mund të kryeni vetëm një thirrje në herë. + S’u ndërrua dot kahu i kamerës + Fiksoje në krye + Shfiksoje prej kreut + Gjurmë GPX + S’u ndreq dot mesazhi + Krejt bisedat + Këtë bisedë + Avatari juaj + Avatar për %s + Fshehtëzuar me OMEMO + Fshehtëzuar me OpenPGP + Jo e fshehtëzuar + Dalje + Incizo postë zanore + Luaje audion + Ndale videon + + Shihni %1$d Pjesëmarrës + Shihni %1$d Pjesëmarrës + + + S’u dha dot një mesazh + S’u dhanë dot disa mesazhe + + Dërgime të dështuara + Më tepër mundësi + S’u gjet aplikacion + Ftojeni te Conversations + S’arrihet të analizohet ftesë + Shërbyesi s’mbulon prodhim ftesash + Këtë veçori s’e mbulon ndonjë llogari aktive + S’arrihet të aktivizohet video. + Dokument tekst i thjeshtë + Nuk mbulohen regjistrime llogarish + S’u gjet adresë XMPP + Dështim i përkohshëm mirëfilltësimi + Fshije avatarin + Kalo në thirrje video + Hidh poshtë kalim në thirrje video + Hyni në kanal publik… + + %d bisedë e palexuar + %d biseda të palexuara + + + U fshi %d dëshmi + U fshi %d dëshmi + + + %d muaj + %d muaj + + S’u shndërrua dot kartelë figure + Temë + Rregullime + Rinise + Duke dërguar “stack traces” ndihmoni në zhvillimin + Bëje kanalin të moderuar + U lidhën %1$d nga %2$d e llogarive + Ngarko më tepër mesazhe + Akordoni hyrje %1$s për kamerën + Shfaq mesazh gabimi + Mesazh Gabimi + S’u krijua dot kartelë e përkohshme + Kjo pajisje u verifikua + Që të vazhdoni të merrni njoftime, edhe kur ekrani juaj është i fikur, duhet të shtoni Conversations te lista e aplikacioneve të mbrojtur. + Kjo duket si adresë kanali + Thirrje audio + Thirrje video + Ndihmë + Kalo te bisedë + Rikthehu te thirrja që po bëhej + + %d mesazh + %d mesazhe + + Llogari XMPP + Asnjë (e çaktivizuar) + + %d sekondë + %d sekonda + + \ No newline at end of file From af59a028a6810302bab0f78387f8a151511e4726 Mon Sep 17 00:00:00 2001 From: random_r Date: Fri, 13 Jan 2023 14:49:29 +0000 Subject: [PATCH 375/394] Translated using Weblate (Italian) Currently translated at 4.4% (2 of 45 strings) Translation: Conversations/App Store Metadata Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/it/ --- fastlane/metadata/android/it-IT/short_description.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/metadata/android/it-IT/short_description.txt b/fastlane/metadata/android/it-IT/short_description.txt index 66e51b2d5..fd4dfa96d 100644 --- a/fastlane/metadata/android/it-IT/short_description.txt +++ b/fastlane/metadata/android/it-IT/short_description.txt @@ -1 +1 @@ -Un client di messaggistica XMPP facile e criptato, ottimizzato per il mobile +Client di messaggistica XMPP facile e criptato, per il tuo dispositivo mobile From ffc98dd997110db473e8d9a7ec29c35709935c77 Mon Sep 17 00:00:00 2001 From: Grzegorz Szymaszek Date: Fri, 13 Jan 2023 17:36:41 +0000 Subject: [PATCH 376/394] Translated using Weblate (Polish) Currently translated at 6.6% (3 of 45 strings) Translation: Conversations/App Store Metadata Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/pl/ --- fastlane/metadata/android/pl-PL/full_description.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/metadata/android/pl-PL/full_description.txt b/fastlane/metadata/android/pl-PL/full_description.txt index bb1ca3919..442e1e826 100644 --- a/fastlane/metadata/android/pl-PL/full_description.txt +++ b/fastlane/metadata/android/pl-PL/full_description.txt @@ -20,7 +20,7 @@ Funkcjonalność: * wiele kont, zintegrowana skrzynka odbiorcza; * bardzo ograniczony wpływ na zużycie baterii. -Conversations bardzo ułatwia rejestrację konta na darmowym serwerze conversations.im, jednak będzie działać również z każdym innym serwerem XMPP. Wiele serwerów jest uruchamianych przez wolontariuszy i są dostępne za bez opłat. +Conversations bardzo ułatwia rejestrację konta na darmowym serwerze conversations.im, jednak będzie działać również z każdym innym serwerem XMPP. Wiele serwerów jest uruchamianych przez wolontariuszy i są dostępne bez opłat. Funkcjonalność XMPP: From 0092aeaa5832531e854a0c0e2f0a124d0c17b2c2 Mon Sep 17 00:00:00 2001 From: Besnik_b Date: Sun, 15 Jan 2023 13:37:36 +0000 Subject: [PATCH 377/394] Translated using Weblate (Albanian) Currently translated at 96.6% (930 of 962 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/sq/ --- src/main/res/values-sq-rAL/strings.xml | 179 +++++++++++++++++++++++++ 1 file changed, 179 insertions(+) diff --git a/src/main/res/values-sq-rAL/strings.xml b/src/main/res/values-sq-rAL/strings.xml index e9de59f54..9ffcd8727 100644 --- a/src/main/res/values-sq-rAL/strings.xml +++ b/src/main/res/values-sq-rAL/strings.xml @@ -824,4 +824,183 @@ %d sekondë %d sekonda
+ Jo + PIN-i që keni dërguar, ka skaduar. + Po përdorni një version të vjetruar të këtij aplikacioni. + Prej këtij numri telefoni është bërë hyrja me një tjetër pajisje. + Ju lutemi, jepni emrin tuaj, për t’u bërë të ditur se cili jeni, personave që s’ju kanë në librat e tyre të adresave. + Që të caktoni emrin tuaj, përdorni butonin e përpunimeve. + Ky kanal do ta bëjë publike adresën tuaj XMPP + Po kërkohet SMS… + Pajisja juaj përdor optimizime shumë të thella baterie për %1$s, çka mund të shpjerë në vonesa njoftimesh, ose madje edhe humbje mesazhesh. +\n +\nTani do t’ju kërkohet t’i çaktivizoni ato. + I keni vlerësuar në mënyrë të parrezik shenjat e gishtave të këtij personi, për të ripohuar besimin. Duke përzgjedhur “U bë”, thjesht po ripohoni se %s është pjesë e kësaj fjalosjeje në grup. + Quicksy është një pjellë e klientit popullor XMPP, Conversations, me zbulim të automatizuar kontaktesh.<br><br>Regjistroheni me numrin tuaj të telefonit dhe Quicksy do të sugjerojë në mënyrë të automatizuar—bazuar në numrat e telefonave në librin tuaj të adresave—kontakte të mundshëm për ju.<br><br>Duke u regjistruar, pajtoheni me <a href=https://quicksy.im/#privacy>rregullat tona të privatësisë</a>. + Ju ndan një hap nga verifikimi i kyçeve OMEMO të llogarisë tuaj. Kjo është e sigurt vetëm nëse e ndoqët këtë lidhje prej një burimi të besuar, ku vetëm ju do të mund ta kishit publikuar këtë lidhje. + Jeni i sigurt se doni të hiqet verifikimi i kësaj pajisjeje\? +\nKësaj pajisjeje dhe mesazheve prej saj do t’u vihet shenjë si “Jo i besuar”. + Fshi vetvetiu nga kjo pajisje mesazhe që janë më të vjetër se intervali kohor i formësuar. + S’po sillen mesazhe, për shkak të një periudhe lokale mbajtjeje. + Fshije prej liste + Do të donit të hiqet %s prej listës tuaj të kontakteve\? Bisedat me këtë kontakt s’do të hiqen. + Do të donit t’i bllokohet %s dërgimi i mesazheve për ju\? + Do të donit të zhbllokohet %s dhe të lejohet t’ju dërgojë mesazhe\? + Do të donit të hiqet %s si faqerojtës\? Bisedat me këtë faqerojtës s’do të hiqen. + Përdorimi i llogarisë tuaj XMPP për të dërguar “stack traces” ndihmon zhvillimin e pandërprerë të %1$s. + Doni të fshihen krejt mesazhet te kjo bisedë\? +\n +\nKujdes: Kjo s’do të ndikojë mesazhet e depozituar në pajisje apo shërbyes të tjerë. + Jeni i sigurt se doni të fshihet kjo kartelë\? +\n +\nKujdes: Kjo s’do të fshijë kopje të kësaj kartele që janë depozituar në pajisje apo shërbyes të tjerë. + Shfshehtëzimi dështoi. Ndoshta s’keni kyçin e duhur privat. + OpenKeychain + %1$s përdor <b>OpenKeychain</b> që të fshehtëzojë dhe shfshehtëzojë mesazhe dhe të administrojë kyçet tuaj publikë.<br><br>Licensohet sipas kushteve të GPLv3+ dhe mund të merret në F-Droid dhe Google Play.<br><br><small>(Ju lutemi, riniseni %1$s më pas.)</small> + Mesazhi juaj s’u fshehtëzua dot, ngaqë kontakti juaj s’po deklaron kyçin e vet publik. +\n +\nJu lutemi, kërkojini kontaktit tuaj të ujdisë OpenPGP-në. + Mesazhi juaj s’u fshehtëzua dot, ngaqë kontaktet tuaj s’po deklarojnë kyçet e tyre publikë. +\n +\nJu lutemi, kërkojuni të ujdissin OpenPGP-në. + Pranoni vetvetiu kartela më të vogla se… + Xixëllo dritëz njoftimesh, kur mbërrin një mesazh i ri + Kohëzgjatje heshtimi njoftimesh, pas pikasjeje veprimtarie në një nga pajisjet tuaja të tjera. + Bëjuni të ditur kontakteve tuaja kur keni marrë dhe lexuar mesazhet e tyre + Fshih lëndë aplikacioni te këmbyesi i aplikacioneve dhe blloko fotografim ekrani + Gabim i përgjithshëm I/O. Ndoshta ju është mbaruar hapësirë depozitimi\? + Aplikacioni që përdorët për të përzgjedhur këtë figurë nuk dha leje të mjaftueshme për leximin e kartelës. +\n +\nPërdorni një tjetër përgjegjës kartelash për të zgjedhur një figurë. + Aplikacioni që përdorët për të dhënë këtë kartelë, nuk jep leje të mjaftueshme. + Jeni i sigurt se doni të hiqet kyçi juaj publik OpenPGP nga njoftimi juaj për prani\? +\nKontaktet tuaj s’do të jenë më në gjendje t’ju dërgojnë mesazhe të fshehtëzuar me OpenPGP. + Fshirja e llogarisë tuaj fshin krejt historikun e bisedave tuaja + Mesazh i fshehtëzuar. Që ta shfshehtëzoni, ju lutemi, instaloni OpenKeychain. + Shenja gishtash OMEMO (origjinë mesazhi) + Shenja gishtash v\\OMEMO (origjinë mesazhi) + kanal@konferencë.example.com + Jeni i sigurt se doni të asgjësohet kjo fjalosje në grup\? +\n +\nKujdes: Fjalosja në grup do të hiqet plotësisht te shërbyesi. + Jeni i sigurt se doni të asgjësohet ky kanal publik\? +\n +\nKujdes: Kanali do të hiqet plotësisht te shërbyesi. + %1$s +%2$d të tjerë kanë lexuar deri në këtë pikë + Gjithkush ka lexuar deri në këtë pikë + Prekni avatarin që të përzgjidhni një foto nga galeri + (Ose shtypeni gjatë, për të ri kthyer parazgjedhjet) + Ju lutemi, së pari kërkoni përditësime pranie nga kontakti juaj. +\n +\nKjo do të përdoret për të përcaktuar cilin aplikacion fjalosjeje po përdor kontakti juaj. + Lejojuni kontakteve tuaja të përpunojnë mesazhet e tyre edhe më pas + Gjatë orëve të qetësisë, njoftimet do të heshtohen + Kalo në “autojoin”, kur hyhet ose dilet nga një MUC dhe reago te ndryshime të bëra nga klientë të tjerë. + Kujdes: Dërgimi i kësaj, pa përditësime të dyanshme pranie, mund të shkaktojë probleme të papritura. +\n +\nKaloni te “Hollësi kontakti”, që të verifikoni pajtime tuajat pranie. + E braktisët këtë fjalosje grupi për arsye teknike + I pengon sistemit operativ të asgjësojë lidhjen tuaj + Kartelat e kopjeruajtjes janë depozituar në %s + Anuloje transmetimin + Shfaq etiketa vetëm-lexim nën kontakte + Jeni i sigurt se doni të spastrohen krejt pajisjet e tjera nga njoftimi OMEMO\? Herës tjetër që pajisjet tuaja lidhen, do të rinjoftojnë veten, por ndërkohë mund të mos marrin mesazhet e dërguar. + S’ka kyçe të përdorshëm për këtë kontakt. +\nS’u sollën dot kyçe të rinj nga shërbyesi. Ndoshta ka diçka gabim me shërbyesin tuaj të kontakteve\? + S’ka kyçe të përdorshëm për këtë kontakt. +\nSigurohuni se keni që të dy pajtim pranie. + Po rrekeni të hiqni %s nga një kanal publik. Rruga e vetme për ta bërë këtë është ta dëboni përgjithmonë atë përdorues. + Bëji adresat XMPP të dukshme për këdo + S’u ndryshuan dot mundësi fjalosjeje në grup + Përdorni tastin Enter për të dërguar mesazhin. Mundeni përherë të përdorni Ctrl+Enter për të dërguar mesazhin, edhe nëse kjo mundësi është e çaktivizuar. + Po dërgohet %s + Fshih të shkëputurat + Bëjuni të ditur kontakteve tuaj, kur shkruani mesazhe për ta + Mos beso DA sistemi + Zëvendëso butonin “Dërgoje” me veprim të shpejtë + “I zënë” nën mënyrën e heshtur + Shfaqe si “I zënë”, kur pajisja gjendet nën mënyrën e heshtur + Trajtoje dridhjen si mënyrë heshturazi + Kur ujdiset një llogari, shfaq rregullime strehëemri dhe porte + xmpp.example.com + Po sillen parapëlqime arkivimi. Ju lutemi, pritni… + Pajisja juaj nuk mbulon përzgjedhjen e dëshmive të klientëve! + Kaloji krejt lidhjet përmes rrjetit Tor. Lyp Orbot + Kartela iu dha %s + Figura iu dha %s + Figurat iu dhanë %s + Teksti iu dha %s + Akordoji %1$s hyrje te depozitë e jashtme + %1$s dëshiron leje të përdorë librin tuaj të adresave, për përkim me listën tuaj të kontakteve XMPP. +\nKjo do të sjellë shfaqjen e emrave të plotë dhe avatarëve të kontakteve tuaj. +\n +\n%1$s vetëm sa do të lexojë librin tuaj të adresave dhe bëjë lokalisht përkimin, pa ngarkuar gjë në shërbyesin tuaj. + Që të bëjë sugjerime rreth kontaktesh të mundshëm që përdorin tashëm Quicksy-n, i duhet hyrje në numrat e telefonave të kontakteve.<br><br>S’do të depozitojmë kopje të këtyre numrave të telefonave. +\n +\nPër më tepër hollësi, lexoni <a href=https://quicksy.im/#privacy>rregullat tona të privatësisë</a>.<br><br>Tani do t’ju kërkohet të akordoni leje hyrjeje te kontaktet tuaja. + Ndihmëz: Përdorni “Zgjidhni kartelë”, në vend se “Zgjidhni foto”, për të dërguar figura të pangjeshura, pavarësisht nga ky rregullim. + Pajisja juaj përdor optimizime shumë të thella baterie për %1$s, çka mund të shpjerë në vonesa njoftimesh, ose madje edhe humbje mesazhesh. +\nRekomandohet të çaktivizohen ato. + Pajisja juaj nuk mbulon zgjedhjen e lënies jashtë nga optimizim baterie + Bëjuni të ditur kontakteve tuaj, kur përdorni Conversations + Për mesazhe të marrë përdor sfond të gjelbër + Riprodhoni kyçet tuaj OMEMO. Krejt kontakteve tuaj do t’ju duhet t’ju verifikojnë sërish. Këtë përdoreni si zgjidhjen e fundit. + Që të bëni publik avatarin tuaj, duhet të jeni i lidhur. + Kursyesi i të dhënave u aktivizua + Sistemi juaj operativ po e kufizon hyrjen e %1$s në Internet, kur gjendet në sfond. Që të merrni njoftime për mesazhe të rinj, duhet t’i lejoni %1$s hyrje të pakufizuar , kur “Ruajtësi i të dhënave” është aktiv. +\n%1$s do të bëjë prapë një përpjekje të kursejë të dhëna, kur është e mundur. + Pajisja juaj nuk mbulon çaktivizim Kursyesi të dhënash për %1$s. + Verifikuat krejt kyçet OMEMO që zotëroni + Beso pajisje të reja prej kontaktesh të paverifikuar, por kërko ripohim dorazi për pajisje të reja për kontakte të verifikuarbut prompt manual confirmation of new devices for verified contacts. + Spastro dosje fshehtinë (përdorur nga aplikacioni kamerë) + Spastro depozitë private ku mbahen kartelat (Ato mund të rishkarkohen prej shërbyesit) + E ndoqa këtë lidhje prej një burimi të besuar + Ju ndan një hap nga verifikimi i kyçeve OMEMO të %1$s, pas klikimit të një lidhjeje. Kjo është e sigurt vetëm nëse e ndoqët këtë lidhje prej një burimi të besuar, ku vetëm %2$s do të mund ta kishte publikuar këtë lidhje. + Fshih jo aktivet + Njofto për mesazhe dhe thirrje të mara prej të huajish. + Dëshmi shërbyesi që përmbajnë strehëemrin e vlerësuar, konsiderohen të verifikuara + Dëshmia s’përmban adresë XMPP + Shërbyesi s’bëri dot mirëfilltësimin si “%s”. Dëshmia është e vlefshme vetëm për: + Pas dërgimit të një mesazhi, rrëshqit poshtë + %1$s s’është në gjendje të fshehtëzojë mesazhe te %2$s. Kjo mund të vijë për shkak se kontakti juaj përdor një shërbyes, ose klient të vjetruar, që s’mund të përdorë OMEMO. + Ndihmëz: Në disa raste, kjo mund të ndreqet duke shtuar listat e kontakteve të njëri-tjetrit. + Jeni i sigurt se doni të çaktivizohet fshehtëzimi OMEMO për këtë bisedë\? +\nKjo do t’i lejojë përgjegjësit të shërbyesit tuaj të lexojë mesazhet tuaj, por mund të jetë e vetmja rrugë për të komunikuar me persona që përdorin klientë të vjetruar. + OMEMO do të përdoret përherë për fjalosje tek-për-tek dhe në grup. + OMEMO do të përdoret për biseda të reja, si parazgjedhje. + OMEMO do të duhet të aktivizohet shprehimisht për biseda të reja. + Madhësia relative e shkronjave të përdorura brenda aplikacionit. + Te skena “Nisni Bisedë” hapni tastierën dhe vendoseni kursorin te fusha e kërkimeve + Streha nuk mbulon avatarë fjalosjeje në grup + Vetëm i zoti mund të ndryshyjë avatarin e një fjalosje në grup + Përdor Shtojcën Për Tregim Vendndodhjeje, në vend se hartën e brendshme + Kjo kategori njoftimesh përdoret për të shfaqur një njoftim të përhershëm që tregon se %1$s është në funksionim. + Kjo kategori njoftimesh përdoret për të shfaqur njoftim në rast se ka problem me lidhjen me një llogari. + Ky grup njoftimesh përdoret për të shfaqur njoftime që s’duhet të shkaktojnë ndonjë tingull. Për shembull, kur është aktiv në një tjetër pajisje (Grace Period). + Cilësi më e ulët do të thotë kartela më të vogla + Quicksy do të dërgojë një mesazh SMS (mund të aplikohen tarifa shërbimi) për të verifikuar numrin tuaj të telefonit. Jepni kodin e vendit tuaj dhe numrin e telefonit: + Ju kemi dërguar një tjetër SMS me një kod prej 6 shifrash. + Ju lutemi, jepni më poshtë PIN-in tuaj prej 6 shifrash. + Ju lutemi, jepni PIN-in tuaj prej 6 shifrash. + Kartelë e shpërfillur, për shkak cenimi sigurie. + Do të verifikojmë numrin e telefonit

%s

Dakord, apo do të donit të përpunonit numrin\?
+ Ju kemi dërguar një SMS te %s. + Që të rikthehet kopjeruajtja, jepni fjalëkalimin tuaj për llogarinë %s. + Mos përdorni veçorinë e rikthimit të një kopjeruajtje në një përpjekje për të klonuar (xhiruar në të njëjtën kohë) një instalim. Rikthimi i një kopjeruajtje është menduar vetëm për migrime, ose në rast se humbët pajisjen origjinale. + Ky kanal publik s’ka pjesëmarrës. Ftoni kontaktet tuaj, ose përdorni butonin e ndarjes me të tjerët për të dhënë adresën XMPP të tij. + Zbulimi i kanaleve përdor një shërbim prej pale të tretë, të quajtur <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>Përdorimi i kësaj veçorie do t’i transmetojë atij shërbimi adresën tuaj IP dhe termat tuaj të kërkimeve. Për më tepër hollësi, shihni <a href=https://search.jabber.network/privacy>Rregulla Privatësie</a> prej tyre. + Aplikacioni dhënës nuk akordoi leje për hyrje në këtë kartelë. + Shumica e përdoruesve duhet të zgjedhin ‘jabber.network’ për sugjerime më të mira nga krejt ekosistemi publik XMPP. + Shtoni kontakt, krijoni ose hyni në një fjalosje në grup, ose zbuloni kanale + Kopjeruajtja u nis. Do të merrni një njoftim, sapo të jetë plotësuar. + Thirrjet janë të çaktivizuara, kur përdoret Tor-i + Llogaria përmes së cilës do të merren mesazhet push. + Shërbyes Push + Një shërbyes push i zgjedhur nga përdoruesi, përmes të cilit të kalohen te pajisja juaj mesazhet push përmes XMPP-je. + Ka të hartuar një udhërrëfyes mbi krijim llogarish te conversations.im. +\nKu zgjidhet conversations.im si shërbim, do të jeni në gjendje të komunikoni me përdorues prej shërbimesh të tjera duke u dhënë atyre adresën tuaj të plotë XMPP. + + %1$d thirrje të humbur prej %2$d kontakti + %1$d thirrje të humbur prej %2$d kontaktesh + \ No newline at end of file From d77f6944a380083d2e0c98ecd01f60dc9ab2455e Mon Sep 17 00:00:00 2001 From: Xstatic Date: Mon, 16 Jan 2023 19:16:00 +0000 Subject: [PATCH 378/394] Added translation using Weblate (Portuguese) --- src/conversations/res/values-pt/strings.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 src/conversations/res/values-pt/strings.xml diff --git a/src/conversations/res/values-pt/strings.xml b/src/conversations/res/values-pt/strings.xml new file mode 100644 index 000000000..a6b3daec9 --- /dev/null +++ b/src/conversations/res/values-pt/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From 05ac54817012f993f4c1218f8ebfa1d6e42fa569 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Wed, 18 Jan 2023 16:36:21 +0000 Subject: [PATCH 379/394] Translated using Weblate (Spanish) Currently translated at 100.0% (962 of 962 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/es/ --- src/main/res/values-es/strings.xml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index 234c5410e..4d7e982de 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -126,10 +126,10 @@ Sonido de notificación para nuevos mensajes Tono para las nuevas llamadas Periodo de gracia - Después de que se detecte actividad en otros dispositivos, las notificaciones se silenciarán durante este período de tiempo. + El tiempo que se silencian las notificaciones tras detectar actividad en otro de tus dispositivos. Avanzado Nunca informar de errores - Estará ayudando al desarrollo si elige enviar un informe de error + Al enviar informes de fallas, ayudará a un mayor desarrollo Confirmar mensajes Permitir a tus contactos saber cuando has recibido y leído sus mensajes Impedir capturas de pantalla @@ -199,15 +199,15 @@ ¿Quieres añadir a %s a tus contactos? Información de servidor XEP-0313: MAM - XEP-0280: Message Carbons - XEP-0352: Client State Indication - XEP-0191: Blocking Command - XEP-0237: Roster Versioning - XEP-0198: Stream Management - XEP-0215: External Service Discovery - XEP-0163: PEP (Avatars / OMEMO) - XEP-0363: HTTP File Upload - XEP-0357: Push + XEP-0280: Copias de los mensajes + XEP-0352: Visualización del estado del cliente + XEP-0191: Comando de bloqueo + XEP-0237: Control de las versiones de la lista de contactos + XEP-0198: Gestión del flujo de datos + XEP-0215: Detectando servicios externos + XEP-0163: PEP (Avatares / OMEMO) + XEP-0363: Carga de archivo HTTP + XEP-0357: Notificaciones automáticas No Se han perdido las claves de anuncio públicas @@ -701,7 +701,7 @@ Mensaje Los mensajes privados están deshabilitados Aplicaciones protegidas - Para recibir notificaciones de mensajes incluso cuando la pantalla está apagada, debe agregar Conversations a la lista de aplicaciones protegidas. + Para continuar recibiendo notificaciones, incluso cuando la pantalla está apagada, debe agregar Conversaciones a la lista de aplicaciones protegidas. ¿Aceptar certificado desconocido? El certificado del servidor no está firmado por una Autoridad Certificadora conocida. ¿Aceptar nombre del servidor no coincidente? @@ -851,7 +851,7 @@ e-book Original (sin comprimir) Abrir con… - Establecer la foto del perfil + Foto de perfil de las conversaciones Elige una cuenta Restaurar copia de respaldo Restaurar From c49fe4c97d58ac19ba761dcf6bbe61c54b163b72 Mon Sep 17 00:00:00 2001 From: Grzegorz Szymaszek Date: Tue, 17 Jan 2023 19:41:49 +0000 Subject: [PATCH 380/394] Translated using Weblate (Polish) Currently translated at 100.0% (962 of 962 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/pl/ --- src/main/res/values-pl/strings.xml | 74 ++++++++++++++++-------------- 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index fc021fdc6..e7d739a5f 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -115,7 +115,7 @@ Nie można zaszyfrować twojej wiadomości bo twoje kontakty nie ogłaszają swoich kluczy publicznych.\n\nPoproś aby ustawili OpenPGP. Główne Akceptuj pliki - Automatycznie akceptuj pliki mniejsze niż... + Automatycznie akceptuj pliki mniejsze niż… Załączniki Powiadomienie Wibracje @@ -130,7 +130,7 @@ Długość czasu kiedy powiadomienia są uśpione po wykryciu aktywności na jednym z twoich innych urządzeń. Zaawansowane Nie wysyłaj raportów awarii - Wysyłając nam ślady stosu pomagasz w rozwoju + Wysyłając nam ślady stosu pomagasz w rozwoju Potwierdzenia wiadomości Zezwól na wysyłanie do osób z twojej listy kontaktów informacji o tym, kiedy otrzymałeś i przeczytałeś wiadomość od nich Zapobiegaj zrzutom ekranu @@ -152,12 +152,14 @@ Błąd konwersji obrazu Nie odnaleziono pliku Ogólny błąd wejścia/wyjścia. Być może skończyło się miejsce w pamięci\? - Aplikacja użyta do wyboru obrazu nie zezwoliła na odczyt pliku.\n\nWybierz obraz przy użyciu innego menedżera plików + Aplikacja użyta do wyboru obrazu nie zezwoliła na odczyt pliku. +\n +\nWybierz obraz przy użyciu innego menedżera plików. Aplikacja której użyłeś do udostępnienia pliku nie dostarczyła odpowiednich uprawnień. Nieznany Tymczasowo wyłączono Połączono - Łączenie... + Łączenie… Rozłączono Błąd uwierzytelnienia Nie odnaleziono serwera @@ -226,7 +228,7 @@ v\\Odcisk OMEMO (pochodzenie wiadomości) Pozostałe urządzenia Zaufane odciski OMEMO - Pobieranie kluczy... + Pobieranie kluczy… Ukończono Odszyfruj Zakładki @@ -252,7 +254,7 @@ Nie można usunąć kanału Edytuj tytuł konferencji Temat - Dołączanie do konferencji + Dołączanie do konferencji… Opuść pokój Kontakt dodał ciebie do swojej listy kontaktów Również dodaj @@ -262,7 +264,7 @@ Wszyscy przeczytali do tego miejsca Publikuj Dotknij awatar, żeby wybrać obraz z galerii - Publikowanie... + Publikowanie… Serwer odrzucił żądanie publikacji Nie można skonwertować obrazu Nie udało się zapisać obrazu w pamięci urządzenia @@ -280,7 +282,9 @@ Włącz Konferencja wymaga hasła Wprowadź hasło - Poproś kontakt o udostępnienie powiadomień o obecności.\n\nPozwoli to na ustalenie klienta, z którego korzysta rozmówca. + Poproś kontakt o udostępnianie powiadomień o obecności. +\n +\nPozwoli to na ustalenie klienta, z którego korzysta rozmówca. Zażądaj teraz Ignoruj Uwaga: Wysyłanie bez obustronnych powiadomień o obecności może powodować nieoczekiwane problemy.\n\nSprawdź subskrypcję powiadomień w szczegółach kontaktu. @@ -297,7 +301,7 @@ Powiadomienia będą wyciszone w wybranym przedziale czasu Inne Synchronizuj zakładki - Ustaw flagę automatycznego dołączania przy wchodzeniu lub opuszczaniu pokoju i reaguj na zmiany innych klientów + Ustaw flagę automatycznego dołączania przy wchodzeniu lub opuszczaniu pokoju i reaguj na zmiany innych klientów. Odcisk klucza OMEMO został skopiowany do schowka Zbanowany Konferencja tylko dla użytkowników @@ -369,7 +373,7 @@ Coś poszło źle Pobieranie historii z serwera Koniec historii na serwerze - Aktualizowanie... + Aktualizowanie… Hasło zostało zmienione! Nie udało się zmienić hasła Zmień hasło @@ -400,7 +404,7 @@ Nie udało się zmienić funkcji %s Konfiguracja prywatnej rozmowy grupowej Konfiguracja publicznego kanału - Prywatne, tylko dla członków. + Prywatne, tylko dla członków Spraw aby adres XMPP był widoczny dla wszystkich Włącz moderację na kanale Nie bierzesz udziału @@ -428,9 +432,9 @@ Wysyłanie %s Oferowanie %s Ukryj niedostępnych - %s pisze... + %s pisze… %s już nie pisze - %s piszą... + %s piszą… %s przestali pisać Powiadomienia pisania Powiadamiaj rozmówcę, kiedy rozpoczynasz nową wiadomość @@ -490,7 +494,7 @@ Nie mogę odczytać certyfikatu Preferencje archiwizacji Preferencje archiwizacji po stronie serwera - Pobieranie preferencji archiwizacji. Proszę czekać... + Pobieranie preferencji archiwizacji. Proszę czekać… Nie można pobrać preferencji archiwizacji CAPTCHA wymagana Wprowadź tekst z powyższego obrazka @@ -535,7 +539,9 @@ Tylko duże obrazki Optymalizacje zużycia baterii włączone Twoje urządzenie ma włączone agresywne oszczędzanie baterii przez co %1$s może odbierać wiadomości z opóźnieniem.\nZalecamy wyłączenie tych optymalizacji. - Twoje urządzenie ma włączone agresywne oszczędzanie baterii przez co %1$s może odbierać wiadomości z opóźnieniem.\nZostaniesz poproszony o wyłączenie ich. + Twoje urządzenie stosuje agresywne oszczędzanie baterii, przez co %1$s może odbierać wiadomości z opóźnieniem lub je tracić. +\n +\nZostaniesz poproszony o jego wyłączenie. Wyłącz Zaznaczony obszar jest zbyt duży (Brak aktywynych kont) @@ -546,7 +552,7 @@ Wyłączyłeś to konto Błąd bezpieczeństwa: nieprawidłowy dostęp do pliku! Nie odnaleziono aplikacji do udostępnienia URI - Udostępnij URI za pomocą... + Udostępnij URI za pomocą…
Zapisujesz się przy użyciu numeru telefonu i Quicksy automatycznie - na podstawie numerów telefonów w książce adresowej - zasugeruje potencjalne kontakty dla ciebie.

Zapisując się zgadzasz się na naszą politykę prywatności.]]>
Zgoda i kontynuuj Poprowadzimy cię przez proces tworzenia konta na conversations.im. @@ -568,7 +574,7 @@ Rejestracja nie powiodła się: spróbuj później Rejestracja nie powiodła się: hasło zbyt słabe Wybierz członków - Tworzenie konferencji + Tworzenie konferencji… Zaproś ponownie Wyłącz Krótki @@ -600,7 +606,7 @@ Nie znaleziono serwera Brak odpowiedzi od zdalnego serwera Nie można zaktualizować konta - Zgłoś spam z tego adresu XMPP + Zgłoś spam z tego adresu XMPP. Usuń tożsamości OMEMO Wygeneruj jeszcze raz klucze OMEMO. Wszystkie twoje kontakty będą musiały zweryfikować cię ponownie. Użyj tego tylko w ostateczności. Usuń zaznaczone klucze @@ -616,7 +622,7 @@ Zweryfikowałeś wszystkie klucze OMEMO które posiadasz Kod kreskowy nie zawiera odcisków dla tej rozmowy. Zaufane odciski - Użyj aparatu, aby zeskanować kod kreskowy kontaktu. + Użyj aparatu, aby zeskanować kod kreskowy kontaktu Proszę czekać na ściągnięcie kluczy Udostępnij przez kod QR Udostępnij przez URI XMPP @@ -678,10 +684,10 @@ Automatyczne usuwanie wiadomości Automatycznie usuwaj z tego urządzenia wiadomości starsze niż skonfigurowany okres czasu. Szyfrowanie wiadomości - Nie pobieram wiadomości przez lokalny okres retencji + Nie pobieram wiadomości przez lokalny okres retencji. Kompresuję film Odpowiadające rozmowy zostały zamknięte. - Kontakt zablokowany + Kontakt zablokowany. Powiadomienia od nieznajomych Powiadamiaj przy wiadomościach i połączeniach od nieznajomych. Odebrano wiadomość od nieznajomego @@ -708,9 +714,9 @@ Wiadomość Prywatne wiadomości są wyłączone Aplikacje chronione - Aby otrzymywać wiadomości kiedy ekran jest wyłączony musisz dodać Conversations do listy aplikacji chronionych. + Aby otrzymywać powiadomienia nawet kiedy ekran jest wyłączony musisz dodać Conversations do listy chronionych aplikacji. Zaakceptować nieznany certyfikat? - Certyfikat serwera nie jest podpisany przez znany Urząd Certyfikacji + Certyfikat serwera nie jest podpisany przez znany Urząd Certyfikacji. Czy zaakceptować niepasującą nazwę serwera? Nie można potwierdzić serwera jako \"%s\". Certyfikat jest ważny tylko dla: Czy chcesz kontynuować połączenie? @@ -726,8 +732,8 @@ Nie powiodło się pobranie listy urządzeń Nie powiodło się pobranie kluczy szyfrowania Podpowiedź: W niektórych przypadkach może pomóc wzajemne dodanie się do listy kontaktów. - Czy na pewno chcesz wyłączyć szyfrowanie OMEMO dla tej konwersacji? -Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale może to być jedyny sposób aby komunikować się z ludźmi korzystającymi ze starych klientów. + Czy na pewno chcesz wyłączyć szyfrowanie OMEMO dla tej konwersacji\? +\nAdministrator twojego serwera będzie mógł czytać twoje wiadomości, ale może to być jedyny sposób aby komunikować się z ludźmi korzystającymi z przestarzałych klientów. Wyłącz teraz Szkic: Szyfrowanie OMEMO @@ -755,7 +761,7 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Pokaż lokalizację Udostępnij Nie można rozpocząć nagrywania - Proszę czekać... + Proszę czekać… Pozwól %1$s na dostęp do mikrofonu Wyszukaj wiadomości GIF @@ -778,7 +784,7 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Ta konferencja została usunięta Nie można rozpocząć nagrywania Usługa na pierwszym planie - Ta kategoria powiadomień jest używana aby wyświetlać stałe powiadomienie oznaczające, że %1$s działa + Ta kategoria powiadomień jest używana aby wyświetlać stałe powiadomienie oznaczające, że %1$s działa. Wiadomość Statusu Problemy z połączeniem Ta kategoria powiadomień jest używana aby wyświetlać powiadomienia oznaczające, że Conversations ma problemy z połączeniem. @@ -812,7 +818,7 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Zweryfikuj swój numer telefonu Quicksy wyśle wiadomość SMS (operator może naliczyć opłatę) aby zweryfikować numer telefonu. Wpisz kod kraju i numer telefonu:
%s

Czy wszystko się zgadza czy też chciałbyś zmienić numer?]]>
- %s nie jest prawidłowym numerem telefonu + %s nie jest prawidłowym numerem telefonu. Proszę wpisać swój numer telefonu. Przeszukaj kraje Zweryfikuj %s @@ -828,8 +834,8 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Czy na pewno chcesz przerwać procedurę rejestracji? Tak Nie - Weryfikowanie... - Prośba o SMS... + Weryfikowanie… + Prośba o SMS… PIN który wpisałeś jest nieprawidłowy. PIN który wysłaliśmy stracił ważność. Nieznany błąd sieci. @@ -858,12 +864,12 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Ten kanał sprawi, że twój adres XMPP będzie publiczny e-book Oryginalne (nieskompresowane) - Otwórz za pomocą... + Otwórz za pomocą… Obrazek profilowy Conversations Wybierz konto Przywróć kopię zapasową Przywróć - Wpisz swoje hasło do konta %s aby przywrócić kopię zapasową + Wpisz swoje hasło do konta %s aby przywrócić kopię zapasową. Nie używaj kopii zapasowej aby klonować (uruchamiać równolegle) instalację. Przywracanie kopii jest przeznaczone tylko do migracji albo kiedy urządzenie zostało zgubione. Nie można przywrócić kopii zapasowej. Nie można odszyfrować kopii zapasowej. Czy hasło jest poprawne? @@ -878,7 +884,7 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Podaj nazwę kanału Podaj adres XMPP To jest adres XMPP. Podaj nazwę. - Tworzenie kanału publicznego... + Tworzenie kanału publicznego… Ten kanał już istnieje Dołączono do istniejącego kanału Nie można ustawić konfiguracji kanału @@ -915,7 +921,7 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż To konto zostało już ustawione Proszę podać hasło dla tego konta Nie można wykonać tej akcji - Dołącz do publicznego kanału... + Dołącz do publicznego kanału… Aplikacja udostępniająca nie udzieliła pozwolenia na dostęp do tego pliku. jabber.network From 626596d0e25b668a70c5bebf817b0d7a632bbcbf Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Tue, 17 Jan 2023 16:54:45 +0000 Subject: [PATCH 381/394] Translated using Weblate (Spanish) Currently translated at 100.0% (9 of 9 strings) Translation: Conversations/Android App (Quicksy) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-quicksy/es/ --- src/quicksy/res/values-es/strings.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/quicksy/res/values-es/strings.xml b/src/quicksy/res/values-es/strings.xml index 9b9f07ad1..473a5f3e1 100644 --- a/src/quicksy/res/values-es/strings.xml +++ b/src/quicksy/res/values-es/strings.xml @@ -1,10 +1,10 @@ - Cuánto tiempo Quicksy permanece en silencio después de ver actividad en otros dispositivos - Al enviar los seguimientos del registro, está ayudando al desarrollo de Quicksy + El tiempo que Quicksy permanece en silencio después de ver actividad en otro dispositivo + Al enviar los informes de fallos, ayudará al desarrollo continuo de Quicksy Informar a tus contactos cuando usas Quicksy - Para continuar recibiendo notificaciones incluso cuando la pantalla está apagada, debe agregar Quicksy a la lista de aplicaciones protegidas. - Imagen del perfil de Quicksy + Para seguir recibiendo notificaciones, aunque la pantalla esté apagada, tienes que añadir Quicksy a la lista de aplicaciones protegidas. + Foto de perfil de Quicksy Quicksy no está disponible en tu país. No se ha podido verificar la identidad del servidor. Error de seguridad desconocido. From b28bdb1d9ff3309b6690cdcf8e9009503f97568a Mon Sep 17 00:00:00 2001 From: Grzegorz Szymaszek Date: Tue, 17 Jan 2023 19:53:54 +0000 Subject: [PATCH 382/394] Translated using Weblate (Polish) Currently translated at 13.3% (6 of 45 strings) Translation: Conversations/App Store Metadata Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/pl/ --- fastlane/metadata/android/pl-PL/changelogs/42043.txt | 1 + fastlane/metadata/android/pl-PL/changelogs/42044.txt | 6 +++--- fastlane/metadata/android/pl-PL/changelogs/42046.txt | 1 + fastlane/metadata/android/pl-PL/changelogs/42047.txt | 1 + 4 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 fastlane/metadata/android/pl-PL/changelogs/42043.txt create mode 100644 fastlane/metadata/android/pl-PL/changelogs/42046.txt create mode 100644 fastlane/metadata/android/pl-PL/changelogs/42047.txt diff --git a/fastlane/metadata/android/pl-PL/changelogs/42043.txt b/fastlane/metadata/android/pl-PL/changelogs/42043.txt new file mode 100644 index 000000000..2b6bd98c9 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/42043.txt @@ -0,0 +1 @@ +* Naprawiono regresję w przesyłaniu plików P2P diff --git a/fastlane/metadata/android/pl-PL/changelogs/42044.txt b/fastlane/metadata/android/pl-PL/changelogs/42044.txt index 9afce4574..5098380e3 100644 --- a/fastlane/metadata/android/pl-PL/changelogs/42044.txt +++ b/fastlane/metadata/android/pl-PL/changelogs/42044.txt @@ -1,3 +1,3 @@ -* Naprawiono ponowne wysyłanie wiadomości podczas używania SASL2. -* Naprawiono czarny obraz wideo pomiędzy niektórymi urządzeniami. -* Naprawiono awarię przy użyciu pustych haseł. +* Naprawiono ponowne wysyłanie wiadomości podczas używania SASL2 +* Naprawiono czarny obraz wideo pomiędzy niektórymi urządzeniami +* Naprawiono awarię przy użyciu pustych haseł diff --git a/fastlane/metadata/android/pl-PL/changelogs/42046.txt b/fastlane/metadata/android/pl-PL/changelogs/42046.txt new file mode 100644 index 000000000..65648f27c --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/42046.txt @@ -0,0 +1 @@ +* Zintegrowano dystrybutora UnifiedPush aby ułatwić przesyłanie wiadomości push do innych aplikacji obsługujących UnifiedPush, takich jak Tusky czy Fedilab diff --git a/fastlane/metadata/android/pl-PL/changelogs/42047.txt b/fastlane/metadata/android/pl-PL/changelogs/42047.txt new file mode 100644 index 000000000..62038683d --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/42047.txt @@ -0,0 +1 @@ +* Naprawiono awarię w dystrybutorze UnifiedPush From 30fdebc924f6ea9e45e7ff8d5f91db10df2d6c9f Mon Sep 17 00:00:00 2001 From: rex07 Date: Thu, 26 Jan 2023 05:17:20 +0000 Subject: [PATCH 383/394] Translated using Weblate (Arabic) Currently translated at 100.0% (13 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/ar/ --- src/conversations/res/values-ar/strings.xml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/conversations/res/values-ar/strings.xml b/src/conversations/res/values-ar/strings.xml index 7d44818a7..6483bc9df 100644 --- a/src/conversations/res/values-ar/strings.xml +++ b/src/conversations/res/values-ar/strings.xml @@ -3,18 +3,18 @@ اختر مزود خدمة XMPP الخاص بك استخدِم conversations.im أنشئ حسابًا جديدًا - هل تملك حساب XMPP؟؟ قد يكون ذلك ممكنا لو كنت تستعمل خدمة XMPP أخرى أو إستعملت تطبيق كونفرسايشنز سابقا. أو يمكنك صنع حساب XMPP جديد الآن. -ملاحظة: بعض خدمات البريد الإلكتروني تقدم حسابات XMPP. - XMPP هي خدمة مستقلة للتواصل بشبكة الرسائل المباشرة. يمكنك إستعمال هذه الخدمة مع أي خادم XMPP تختاره. -سعيا لراحتك جعلنا خلق حساب في كونفيرسايشنز سهلا مع مقدم خدمة خاص بالإستعمال مع كونفيرسايشنز. - لقد تمت دعوتك لـ %1$s. سيتم دلّك على طريقة صنع حساب. -عندما تختار %1$sكمقدّم خدمة سيصبح من الممكن لك التواصل مع مستعملين من أي خادم آخر عن طريق إعطائهم عنوانك الكامل على XMPP. - تمّت دعوتك إلى %1$s. تم إختيار إسم مستخدم خاص بك. سيتم قيادتك عبر طريقة صنع حساب. -سيمكنك التواصل مع مستخدمين من مزودين آخرين عبر إعطائهم كامل عنوانك XMPP. + هل تملك حساب XMPP؟؟ قد يكون ذلك ممكنا لو كنت تستعمل خدمة XMPP أخرى أو إستعملت تطبيق Conversations سابقا. أو يمكنك صنع حساب XMPP جديد الآن. +\nملاحظة: بعض خدمات البريد الإلكتروني تقدم حسابات XMPP. + XMPP هو مزود مستقل لشبكة المراسلة الفورية. يمكنك استخدام هذا العميل مع أي خادم XMPP تختاره. +\nولكن من أجل راحتك ، فقد جعلنا من السهل إنشاء حساب على موقع chat. مزود مناسب بشكل خاص للاستخدام مع المحادثات. + لقد تمت دعوتك إلى%1$s. سنوجهك خلال عملية إنشاء حساب. +\nعند اختيار%1$s كموفر ، ستتمكن من التواصل مع مستخدمي مقدمي الخدمات الآخرين من خلال منحهم عنوان XMPP الكامل الخاص بك. + لقد تمت دعوتك إلى%1$s. تم بالفعل اختيار اسم مستخدم لك. سنوجهك خلال عملية إنشاء حساب. +\nستتمكن من التواصل مع مستخدمي مقدمي الخدمات الآخرين من خلال منحهم عنوان XMPP الكامل الخاص بك. سيرفر دعوتك لم يتم التقاط الكود بطريقة جيّدة إضغط على زر مشاركة لترسل إلى المتصل بك دعوة إلى %1$s. إذا كان المتصل بك قريبا منك، يمكنه فحص الكود بالأسفل ليقبل دعوتك. إنظم %1$s وتحدّث معي: %2$s - شارك إستدعاء مع... + شارك الدعوة مع… \ No newline at end of file From 17bf39f8e8a801a147d70ced82f2cd21f974a4ae Mon Sep 17 00:00:00 2001 From: rex07 Date: Thu, 26 Jan 2023 05:24:36 +0000 Subject: [PATCH 384/394] Translated using Weblate (Arabic) Currently translated at 100.0% (9 of 9 strings) Translation: Conversations/Android App (Quicksy) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-quicksy/ar/ --- src/quicksy/res/values-ar/strings.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/quicksy/res/values-ar/strings.xml b/src/quicksy/res/values-ar/strings.xml index d0af2d68c..ddf1d8788 100644 --- a/src/quicksy/res/values-ar/strings.xml +++ b/src/quicksy/res/values-ar/strings.xml @@ -1,12 +1,12 @@ - طول الوقت الذي يبقى فيه كويكسي صامتا بعد رؤية النشاط في جهاز آخر - عبر إرسال أثار الأخطاء تقوم بالمساعدة في تطوير برمجة كويكسي + مدى الوقت الذي يظل فيه Quicksy هادئًا بعد رؤية نشاط على جهاز آخر + عبر إرسال الأخطاء انت تقوم بالمساعدة في تطوير برمجة Quicksy إجعل كلّ جهات إتصالك تعلم أنك تستعمل كويكسي - للمواصلة في إستقبال التنبيهات، حتى والشاشة مغلقة، يجب عليك أن تضيف كويكسي إلى قائمة التطبيقات المحميّة. - صورة حساب كويكسي + للمواصلة في إستقبال التنبيهات، حتى والشاشة مغلقة، يجب عليك أن تضيف Quicksy إلى قائمة التطبيقات المحميّة. + صورة حساب Quicksy إن كويكسي Quicksy غير متوفر في بلدكم. لا يمكن التأكد من خادم الهويّة. خطأ أمني مجهول. تجاوز الوقت أثناء الإتصال بالخادم. - + \ No newline at end of file From ec8225112a0c8658ea9e4e9e110d3345598b7f19 Mon Sep 17 00:00:00 2001 From: tygyh Date: Thu, 26 Jan 2023 11:07:19 +0000 Subject: [PATCH 385/394] Translated using Weblate (Swedish) Currently translated at 91.5% (881 of 962 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/sv/ --- src/main/res/values-sv/strings.xml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/main/res/values-sv/strings.xml b/src/main/res/values-sv/strings.xml index 0257dca18..b606cdb84 100644 --- a/src/main/res/values-sv/strings.xml +++ b/src/main/res/values-sv/strings.xml @@ -31,10 +31,7 @@ %d min sedan %d oläst konversation - - %d olästa konversationer - skickar… Avkrypterar meddelande. Vänta… @@ -84,7 +81,7 @@ sändning misslyckades Förbereder att skicka bild Förbereder att skicka bilder - Delar filer. Vänta... + Delar filer. Vänta… Rensa historik Rensa konversationshistorik Vill du radera alla meddelanden i den här konversationen?\n\nVarning: Det här påverkar inte meddelanden som finns lagrade på andra enheter eller servrar. @@ -689,7 +686,7 @@ Godkänn okänt certifikat? Servercertifikatet är inte signerat av en känd certifikatutfärdare. Acceptera servernamn som inte matchar? - Servern kunde inte autentisera som \"%s\". Certifikatet är endast giltigt för: + Servern kunde inte autentisera som \"%s\". Certifikatet är endast giltigt för: Vill du ansluta ändå? Certifikatdetaljer: En gång @@ -913,4 +910,4 @@ Ingen applikation hittades Bjud in till Conversations Ingen XMPP-adress hittades - + \ No newline at end of file From c3102e2bc2122577fb2631817b5952e1d7ba7943 Mon Sep 17 00:00:00 2001 From: tygyh Date: Thu, 26 Jan 2023 10:04:19 +0000 Subject: [PATCH 386/394] Translated using Weblate (Swedish) Currently translated at 100.0% (13 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/sv/ --- src/conversations/res/values-sv/strings.xml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/conversations/res/values-sv/strings.xml b/src/conversations/res/values-sv/strings.xml index a6185650e..062a0c26f 100644 --- a/src/conversations/res/values-sv/strings.xml +++ b/src/conversations/res/values-sv/strings.xml @@ -9,5 +9,11 @@ Tryck på dela-knappen för att skicka en inbjudan till din kontakt till %1$s. Om din kontakt är i närheten, kan de också skanna koden nedan för att acceptera din inbjudan. Gå med %1$s och chatta med mig: %2$s - Dela inbjudan med... + Dela inbjudan med… + Du har blivit inbjuden till %1$s. Ett användarnamn har redan valts åt dig. Vi guidar dig genom processen för att skapa ett konto. +\nDu kommer att kunna kommunicera med användare av andra leverantörer genom att ge dem din fullständiga XMPP-adress. + XMPP är ett leverantörsoberoende snabbmeddelandenätverk. Du kan använda den här klienten med vilken XMPP-server du än väljer. +\nMen för din bekvämlighet har vi gjort det enkelt att skapa ett konto på conversations.im; en leverantör som är speciellt lämpad för användning med Conversations. + Du har blivit inbjuden till %1$s. Vi guidar dig genom processen för att skapa ett konto. +\nNär du väljer %1$s som leverantör kommer du att kunna kommunicera med användare av andra leverantörer genom att ge dem din fullständiga XMPP-adress. \ No newline at end of file From 9a561511d1fabe828746a9f4236887874eb780e5 Mon Sep 17 00:00:00 2001 From: tygyh Date: Thu, 26 Jan 2023 10:08:41 +0000 Subject: [PATCH 387/394] Translated using Weblate (Swedish) Currently translated at 100.0% (9 of 9 strings) Translation: Conversations/Android App (Quicksy) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-quicksy/sv/ --- src/quicksy/res/values-sv/strings.xml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/quicksy/res/values-sv/strings.xml b/src/quicksy/res/values-sv/strings.xml index 0bdc6a4d9..cd32d7395 100644 --- a/src/quicksy/res/values-sv/strings.xml +++ b/src/quicksy/res/values-sv/strings.xml @@ -3,4 +3,10 @@ Berätta för alla dina kontakter när du använder Quicksy Quicksy är inte tillgängligt i ditt land. Okänt säkerhetsfel. - + Quicksy-profilbild + Det gick inte att verifiera serveridentiteten. + Timeout under anslutning till servern. + Genom att skicka in stack traces hjälper du den pågående utvecklingen av Quicksy + För att fortsätta ta emot aviseringar, även när skärmen är avstängd, måste du lägga till Quicksy i listan över skyddade appar. + Hur lång tid Quicksy håller tyst efter att ha sett aktivitet på en annan enhet + \ No newline at end of file From 93cb17834a815994226cd1968b4c8fc08241ea92 Mon Sep 17 00:00:00 2001 From: random_r Date: Wed, 1 Feb 2023 08:53:07 +0000 Subject: [PATCH 388/394] Translated using Weblate (Italian) Currently translated at 100.0% (962 of 962 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/it/ --- src/main/res/values-it/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index cc00e3562..4a69b6129 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -83,8 +83,8 @@ Preparazione per l\'invio dell\'immagine Preparazione per l\'invio delle immagini Condivisione file. Attendere prego… - Pulisci la cronologia - Pulisci la cronologia della conversazione + Svuota la cronologia + Svuota la cronologia della conversazione Vuoi eliminare tutti i messaggi in questa conversazione?\n\nAttenzione: ciò non influenzerà i messaggi salvati su altri dispositivi o server. Elimina file Sei sicuro di voler eliminare questo file?\n\nAttenzione: non verranno eliminate copie di questo file memorizzate in altri dispositivi o server. @@ -363,7 +363,7 @@ Avatar del profilo Copia impronta OMEMO negli appunti Rigenera chiave OMEMO - Pulisci dispositivi + Elimina dispositivi Sei sicuro di voler rimuovere tutti gli altri dispositivi dall\'annuncio OMEMO? La prossima volta che si connetteranno si riannunceranno, ma potrebbero non ricevere i messaggi inviati nel frattempo. Non ci sono chiavi utilizzabili per questo contatto.\nRicezione di nuove chiavi dal server non riuscita. Forse qualcosa non va con il server del tuo contatto? Non ci sono chiavi utilizzabili per questo contatto.\nAssicurati di avere reciprocamente la sottoscrizione sulla presenza. From 84fa529256cbd92da007e96c43167d4285981a75 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 1 Feb 2023 17:51:32 +0100 Subject: [PATCH 389/394] use setText instead of append() --- .../eu/siacs/conversations/ui/ConversationFragment.java | 6 ++++-- .../java/eu/siacs/conversations/utils/MessageUtils.java | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index f6626c3a1..0f7e9073c 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -2452,12 +2452,14 @@ private boolean reInit(final Conversation conversation, final boolean hasExtras) this.binding.textSendButton.setContentDescription( activity.getString(R.string.send_message_to_x, conversation.getName())); this.binding.textinput.setKeyboardListener(null); - this.binding.textinput.setText(""); final boolean participating = conversation.getMode() == Conversational.MODE_SINGLE || conversation.getMucOptions().participating(); if (participating) { - this.binding.textinput.append(this.conversation.getNextMessage()); + this.binding.textinput.setText(this.conversation.getNextMessage()); + this.binding.textinput.setSelection(this.binding.textinput.length()); + } else { + this.binding.textinput.setText(MessageUtils.EMPTY_STRING); } this.binding.textinput.setKeyboardListener(this); messageListAdapter.updatePreferences(); diff --git a/src/main/java/eu/siacs/conversations/utils/MessageUtils.java b/src/main/java/eu/siacs/conversations/utils/MessageUtils.java index 9687a7b14..1ac9d2c7d 100644 --- a/src/main/java/eu/siacs/conversations/utils/MessageUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/MessageUtils.java @@ -45,7 +45,7 @@ public class MessageUtils { private static final Pattern LTR_RTL = Pattern.compile("(\\u200E[^\\u200F]*\\u200F){3,}"); - private static final String EMPTY_STRING = ""; + public static final String EMPTY_STRING = ""; public static String prepareQuote(Message message) { final StringBuilder builder = new StringBuilder(); From bcfc70d2c09dc4274bdaf71ecc06f46e5738bef8 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 5 Feb 2023 11:13:59 +0100 Subject: [PATCH 390/394] increase corner radius for profile pics --- src/main/res/layout/account_row.xml | 2 +- src/main/res/layout/activity_contact_details.xml | 3 ++- src/main/res/layout/activity_edit_account.xml | 2 +- src/main/res/layout/activity_muc_details.xml | 2 +- src/main/res/layout/contact.xml | 2 +- src/main/res/layout/conversation_list_row.xml | 2 +- src/main/res/layout/message_received.xml | 2 +- src/main/res/layout/message_sent.xml | 2 +- src/main/res/layout/message_status.xml | 2 +- src/main/res/layout/search_result_item.xml | 2 +- src/main/res/layout/user_preview.xml | 6 ++++-- 11 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/main/res/layout/account_row.xml b/src/main/res/layout/account_row.xml index 914ee1950..5bf98a8bc 100644 --- a/src/main/res/layout/account_row.xml +++ b/src/main/res/layout/account_row.xml @@ -16,7 +16,7 @@ android:layout_height="48dp" android:layout_alignParentLeft="true" android:contentDescription="@string/account_image_description" - app:riv_corner_radius="2dp" /> + app:riv_corner_radius="6dp" /> - + app:riv_corner_radius="8dp"/> diff --git a/src/main/res/layout/contact.xml b/src/main/res/layout/contact.xml index 088f3243d..d8fb128a7 100644 --- a/src/main/res/layout/contact.xml +++ b/src/main/res/layout/contact.xml @@ -14,7 +14,7 @@ android:layout_height="48dp" android:layout_alignParentLeft="true" android:scaleType="centerCrop" - app:riv_corner_radius="2dp"/> + app:riv_corner_radius="6dp"/> + app:riv_corner_radius="8dp" /> + app:riv_corner_radius="6dp" /> + app:riv_corner_radius="6dp" /> + app:riv_corner_radius="4dp"/> + app:riv_corner_radius="6dp" /> - + - From 93e4bca0371a98657153dd2b6c1099cf357dac6a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 5 Feb 2023 17:15:33 +0100 Subject: [PATCH 391/394] version bump to 2.12.2 + changelog --- CHANGELOG.md | 4 ++++ build.gradle | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5ef2e389..a93d1f736 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### Version 2.12.2 + +* Increase corner radius on profile pictures + ### Version 2.12.1 * Fix crash in UnifiedPush Distributor diff --git a/build.gradle b/build.gradle index 6da78fcae..13e320a08 100644 --- a/build.gradle +++ b/build.gradle @@ -91,8 +91,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 32 - versionCode 42049 - versionName "2.12.1" + versionCode 42050 + versionName "2.12.2" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId From d9131473db6ac8434d238b23435383d75e0762e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Sun, 26 Mar 2023 10:47:40 +0200 Subject: [PATCH 392/394] Do not build emoji flavors in Snikket CI This is mirroring the commit 882e7319edfcf848a3a622de99d2ae799d2da32c from upstream, which was made following ad493938a03cadb4747613040bad9c28cf244789, which dropped support for the emoji flavor and migrated to emoji2. --- .github/workflows/android.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 778c6315d..f44369019 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -22,10 +22,8 @@ jobs: run: mkdir libs && wget -O libs/libwebrtc-m92.aar https://gultsch.de/files/libwebrtc-m92.aar - name: Grant execute permission for gradlew run: chmod +x gradlew - - name: Build Snikket (Compat) - run: ./gradlew assembleConversationsFreeCompatDebug - - name: Build Snikket (System) - run: ./gradlew assembleConversationsFreeSystemDebug + - name: Build Snikket + run: ./gradlew assembleConversationsFreeDebug - uses: actions/upload-artifact@v2 with: name: Snikket all-flavors (debug) From 906d5b11bcf3d15a5bb004d6e6e579c0f18c04b0 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Thu, 21 Apr 2022 21:19:28 -0500 Subject: [PATCH 393/394] Fix NPE --- .../java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 0d4dc9f2a..02bbb3500 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -670,7 +670,7 @@ public DtmfSender getDtmfSender() { } public boolean applyDtmfTone(String tone) { - if (toneManager == null || peerConnection.getSenders().isEmpty()) { + if (toneManager == null || peerConnection == null || peerConnection.getSenders().isEmpty()) { return false; } peerConnection.getSenders().get(0).dtmf().insertDtmf(tone, TONE_DURATION, 100); From 3e03f11acc5bdf013b580a458778fac1bbd44257 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 27 Mar 2023 13:23:28 -0500 Subject: [PATCH 394/394] Fix DTMF causes track to become disposed --- .../eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 02bbb3500..8852b63a1 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -670,10 +670,10 @@ public DtmfSender getDtmfSender() { } public boolean applyDtmfTone(String tone) { - if (toneManager == null || peerConnection == null || peerConnection.getSenders().isEmpty()) { + if (toneManager == null || peerConnection == null || localAudioTrack == null) { return false; } - peerConnection.getSenders().get(0).dtmf().insertDtmf(tone, TONE_DURATION, 100); + localAudioTrack.rtpSender.dtmf().insertDtmf(tone, TONE_DURATION, 100); toneManager.startTone(TONE_CODES.get(tone), TONE_DURATION); return true; }