From 4dab5156e1fabf42c597260bea921355ddbd90f0 Mon Sep 17 00:00:00 2001 From: kosyak Date: Sun, 1 Sep 2024 22:57:59 +0200 Subject: [PATCH 01/56] small muc subject improvements --- .../eu/siacs/conversations/ui/ConversationFragment.java | 2 +- src/main/res/drawable/ic_subject_black.xml | 9 +++++++++ src/main/res/drawable/ic_subject_white.xml | 9 +++++++++ src/main/res/layout/fragment_conversation.xml | 2 +- src/main/res/values/attrs.xml | 1 + src/main/res/values/themes.xml | 2 ++ 6 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 src/main/res/drawable/ic_subject_black.xml create mode 100644 src/main/res/drawable/ic_subject_white.xml diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 457ff8807..ebea2bc31 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -3610,7 +3610,7 @@ public class ConversationFragment extends XmppFragment conversation.refreshSessions(); - if (conversation != null && conversation.getMode() == Conversational.MODE_MULTI) { + if (conversation != null && conversation.getMode() == Conversational.MODE_MULTI && conversation.getNextCounterpart() == null) { String subject = conversation.getMucOptions().getSubject(); Boolean hidden = conversation.getMucOptions().subjectHidden(); diff --git a/src/main/res/drawable/ic_subject_black.xml b/src/main/res/drawable/ic_subject_black.xml new file mode 100644 index 000000000..6edbae1ae --- /dev/null +++ b/src/main/res/drawable/ic_subject_black.xml @@ -0,0 +1,9 @@ + + + diff --git a/src/main/res/drawable/ic_subject_white.xml b/src/main/res/drawable/ic_subject_white.xml new file mode 100644 index 000000000..8c6262226 --- /dev/null +++ b/src/main/res/drawable/ic_subject_white.xml @@ -0,0 +1,9 @@ + + + diff --git a/src/main/res/layout/fragment_conversation.xml b/src/main/res/layout/fragment_conversation.xml index 870d2fb85..0da157c37 100644 --- a/src/main/res/layout/fragment_conversation.xml +++ b/src/main/res/layout/fragment_conversation.xml @@ -54,7 +54,7 @@ diff --git a/src/main/res/values/attrs.xml b/src/main/res/values/attrs.xml index 19d4166c8..6f9eccd42 100644 --- a/src/main/res/values/attrs.xml +++ b/src/main/res/values/attrs.xml @@ -40,6 +40,7 @@ + diff --git a/src/main/res/values/themes.xml b/src/main/res/values/themes.xml index 8eb113795..74462aa2f 100644 --- a/src/main/res/values/themes.xml +++ b/src/main/res/values/themes.xml @@ -59,6 +59,7 @@ @drawable/ic_attach_videocam @drawable/ic_attach_document @drawable/ic_pin_black + @drawable/ic_subject_black @drawable/ic_close_24dp_black @drawable/ic_attach_location @drawable/ic_attach_photo @@ -239,6 +240,7 @@ @drawable/ic_attach_videocam_white @drawable/ic_attach_document_white @drawable/ic_pin_white + @drawable/ic_subject_white @drawable/ic_close_24dp @drawable/ic_attach_location_white @drawable/ic_attach_photo_white -- 2.39.2 From 00817b79bea00f872b9621d8dbe7bcd053a25f1e Mon Sep 17 00:00:00 2001 From: kosyak Date: Mon, 2 Sep 2024 00:12:42 +0200 Subject: [PATCH 02/56] account color selector --- build.gradle | 1 + .../ui/ManageAccountActivity.java | 36 +++++++++++++++++- .../ui/adapter/AccountAdapter.java | 32 ++++++++++++++-- .../ui/adapter/ConversationAdapter.java | 2 +- .../ui/adapter/ListItemAdapter.java | 2 +- .../siacs/conversations/utils/UIHelper.java | 18 +++++++++ src/main/res/layout/account_row.xml | 38 ++++++++++++------- 7 files changed, 107 insertions(+), 22 deletions(-) diff --git a/build.gradle b/build.gradle index c6e5f252d..c9c8eabb7 100644 --- a/build.gradle +++ b/build.gradle @@ -83,6 +83,7 @@ dependencies { implementation 'com.github.singpolyma:TokenAutoComplete:bfa93780e0' + implementation 'com.github.kizitonwose.colorpreference:core:1.1.0' implementation 'com.github.kizitonwose.colorpreference:support:1.1.0' implementation 'com.caverock:androidsvg-aar:1.4' implementation 'com.github.singpolyma:Better-Link-Movement-Method:4df081e1e4' diff --git a/src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java index aab32220d..67803b387 100644 --- a/src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java +++ b/src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java @@ -36,6 +36,7 @@ import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; import eu.siacs.conversations.ui.adapter.AccountAdapter; import eu.siacs.conversations.ui.util.MenuDoubleTabUtil; +import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; @@ -44,8 +45,14 @@ import static eu.siacs.conversations.utils.PermissionUtils.writeGranted; import com.google.android.material.bottomnavigation.BottomNavigationView; import com.google.android.material.navigation.NavigationBarView; +import com.kizitonwose.colorpreference.ColorDialog; +import com.kizitonwose.colorpreference.ColorShape; -public class ManageAccountActivity extends XmppActivity implements OnAccountUpdate, KeyChainAliasCallback, XmppConnectionService.OnAccountCreated, AccountAdapter.OnTglAccountState { +public class ManageAccountActivity extends XmppActivity implements OnAccountUpdate, + KeyChainAliasCallback, + XmppConnectionService.OnAccountCreated, + AccountAdapter.OnTglAccountState, + ColorDialog.OnColorSelectedListener { private final String STATE_SELECTED_ACCOUNT = "selected_account"; @@ -61,6 +68,18 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda protected Pair mPostponedActivityResult = null; + private AccountAdapter.ColorSelectorListener colorSelectorListener = new AccountAdapter.ColorSelectorListener() { + @Override + public void onColorPickerRequested(Jid accountJid, int currentColor) { + new ColorDialog.Builder(ManageAccountActivity.this) + .setColorShape(ColorShape.CIRCLE) + .setColorChoices(R.array.themeColorsOverride) + .setSelectedColor(currentColor) + .setTag(accountJid.asBareJid().toEscapedString()) + .show(); + } + }; + @Override public void onAccountUpdate() { refreshUi(); @@ -102,7 +121,7 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda } accountListView = findViewById(R.id.account_list); - this.mAccountAdapter = new AccountAdapter(this, accountList); + this.mAccountAdapter = new AccountAdapter(this, accountList, colorSelectorListener); accountListView.setAdapter(this.mAccountAdapter); accountListView.setOnItemClickListener((arg0, view, position, arg3) -> switchToAccount(accountList.get(position))); registerForContextMenu(accountListView); @@ -158,6 +177,13 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda super.onSaveInstanceState(savedInstanceState); } + @Override + protected void onDestroy() { + super.onDestroy(); + colorSelectorListener = null; + mAccountAdapter.colorSelectorListener = null; + } + @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); @@ -349,6 +375,12 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda } } + @Override + public void onColorSelected(int newColor, String tag) { + UIHelper.overrideAccountColor(this, tag, newColor); + refreshUiReal(); + } + private void addAccountFromKey() { try { KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java index a074fd6c8..6a23b2cf8 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java @@ -9,6 +9,11 @@ import android.widget.ArrayAdapter; import androidx.annotation.NonNull; import androidx.databinding.DataBindingUtil; +import com.kizitonwose.colorpreference.ColorDialog; +import com.kizitonwose.colorpreference.ColorPreference; +import com.kizitonwose.colorpreference.ColorShape; +import com.kizitonwose.colorpreference.ColorUtils; + import java.util.List; import eu.siacs.conversations.Config; @@ -19,22 +24,29 @@ 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.UIHelper; +import eu.siacs.conversations.xmpp.Jid; public class AccountAdapter extends ArrayAdapter { private final XmppActivity activity; private final boolean showStateButton; + private final boolean showColorSelector; + + public ColorSelectorListener colorSelectorListener = null; public AccountAdapter(XmppActivity activity, List objects, boolean showStateButton) { super(activity, 0, objects); this.activity = activity; this.showStateButton = showStateButton; + this.showColorSelector = false; } - public AccountAdapter(XmppActivity activity, List objects) { + public AccountAdapter(XmppActivity activity, List objects, ColorSelectorListener listener) { super(activity, 0, objects); this.activity = activity; this.showStateButton = true; + this.showColorSelector = true; + colorSelectorListener = listener; } @Override @@ -77,18 +89,27 @@ public class AccountAdapter extends ArrayAdapter { } else { viewHolder.binding.tglAccountStatus.setVisibility(View.GONE); } + viewHolder.binding.tglAccountStatus.setOnCheckedChangeListener((compoundButton, b) -> { if (b == isDisabled && activity instanceof OnTglAccountState) { ((OnTglAccountState) activity).onClickTglAccountState(account, b); } }); - if (activity.xmppConnectionService.getAccounts().size() > 1) { - viewHolder.binding.accountIndicator.setBackgroundColor(UIHelper.getColorForName(account.getJid().asBareJid().getEscapedLocal())); + if (this.showColorSelector && activity.xmppConnectionService.getAccounts().size() > 1) { + int color = UIHelper.getAccountColor(activity, account.getJid()); + viewHolder.binding.colorView.setVisibility(View.VISIBLE); + ColorUtils.setColorViewValue(viewHolder.binding.colorView, color, false, ColorShape.CIRCLE); + viewHolder.binding.colorView.setOnClickListener(v -> { + if (colorSelectorListener != null) { + colorSelectorListener.onColorPickerRequested(account.getJid(), color); + } + }); } else { - viewHolder.binding.accountIndicator.setBackgroundColor(Color.TRANSPARENT); + viewHolder.binding.colorView.setVisibility(View.GONE); } + return view; } @@ -106,4 +127,7 @@ public class AccountAdapter extends ArrayAdapter { void onClickTglAccountState(Account account, boolean state); } + public interface ColorSelectorListener { + void onColorPickerRequested(Jid accountJid, int currentColor); + } } 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 69d4c7df4..22a41b59b 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java @@ -401,7 +401,7 @@ public class ConversationAdapter Account account = conversation.getAccount(); if (account != null && activity.xmppConnectionService.getAccounts().size() > 1) { - viewHolder.binding.accountIndicator.setBackgroundColor(UIHelper.getColorForName(account.getJid().asBareJid().getEscapedLocal())); + viewHolder.binding.accountIndicator.setBackgroundColor(UIHelper.getAccountColor(activity, account.getJid())); } else { viewHolder.binding.accountIndicator.setBackgroundColor(Color.TRANSPARENT); } 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 b342ccf0d..940d5cb2b 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java @@ -108,7 +108,7 @@ public class ListItemAdapter extends ArrayAdapter { } if (account != null && activity.xmppConnectionService.getAccounts().size() > 1) { - viewHolder.accountIndicator.setBackgroundColor(UIHelper.getColorForName(account.getJid().asBareJid().getEscapedLocal())); + viewHolder.accountIndicator.setBackgroundColor(UIHelper.getAccountColor(activity, account.getJid())); } else { viewHolder.accountIndicator.setBackgroundColor(Color.TRANSPARENT); } diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java index 5680a5e60..ea674c893 100644 --- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java @@ -1,6 +1,7 @@ package eu.siacs.conversations.utils; import android.content.Context; +import android.content.SharedPreferences; import android.text.SpannableStringBuilder; import android.text.format.DateFormat; import android.text.format.DateUtils; @@ -231,6 +232,23 @@ public class UIHelper { return getColorForName(name, false); } + public static int getAccountColor(Context context, Jid accountJid) { + SharedPreferences prefs = context.getSharedPreferences("accountColorsPrefs", Context.MODE_PRIVATE); + String name = accountJid.asBareJid().toEscapedString(); + int overrideColor = prefs.getInt(name, -1); + + if (overrideColor != -1) { + return overrideColor; + } else { + return getColorForName(name); + } + } + + public static void overrideAccountColor(Context context, String accountName, int color) { + SharedPreferences prefs = context.getSharedPreferences("accountColorsPrefs", Context.MODE_PRIVATE); + prefs.edit().putInt(accountName, color).apply(); + } + public static int getColorForName(String name, boolean safe) { if (Config.XEP_0392) { return XEP0392Helper.rgbFromNick(name); diff --git a/src/main/res/layout/account_row.xml b/src/main/res/layout/account_row.xml index ee3f7c138..93d6bd8d7 100644 --- a/src/main/res/layout/account_row.xml +++ b/src/main/res/layout/account_row.xml @@ -6,17 +6,10 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?selectableItemBackground" - android:clipToPadding="false" android:paddingLeft="8dp" android:paddingBottom="8dp" android:paddingTop="8dp"> - - + android:layout_toLeftOf="@+id/controls" + android:layout_toStartOf="@+id/controls"> - + android:layout_alignParentRight="true" + android:layout_centerVertical="true"> + + + + + + \ No newline at end of file -- 2.39.2 From ffbdad7503e5d70d461adb65d7841856ea60c2fd Mon Sep 17 00:00:00 2001 From: kosyak Date: Mon, 2 Sep 2024 01:00:39 +0200 Subject: [PATCH 03/56] show participants count/last seen in toolbar indicator --- .../ui/ContactDetailsActivity.java | 2 +- .../ui/ConversationsActivity.java | 41 ++++++++++++++ .../siacs/conversations/utils/UIHelper.java | 56 +++++++++++++++---- src/main/res/values/strings.xml | 13 +++++ 4 files changed, 100 insertions(+), 12 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index cc7773ad2..8daffdbbc 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -504,7 +504,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp && contact.getLastseen() > 0 && contact.getPresences().allOrNonSupport(Namespace.IDLE)) { binding.detailsLastseen.setVisibility(View.VISIBLE); - binding.detailsLastseen.setText(UIHelper.lastseen(getApplicationContext(), contact.isActive(), contact.getLastseen())); + binding.detailsLastseen.setText(UIHelper.lastseen(getApplicationContext(), contact.isActive(), contact.getLastseen(), false)); } else { binding.detailsLastseen.setVisibility(View.GONE); } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java index b225d8404..5d7762488 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java @@ -42,10 +42,14 @@ import android.app.FragmentTransaction; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.preference.PreferenceManager; import android.provider.Settings; import android.util.Log; import android.view.KeyEvent; @@ -95,7 +99,9 @@ import eu.siacs.conversations.ui.util.PendingItem; import eu.siacs.conversations.utils.ExceptionHelper; import eu.siacs.conversations.utils.PhoneNumberUtilWrapper; import eu.siacs.conversations.utils.SignupUtils; +import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.utils.XmppUri; +import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import io.michaelrocks.libphonenumber.android.NumberParseException; @@ -135,6 +141,10 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio private boolean mActivityPaused = true; private final AtomicBoolean mRedirectInProcess = new AtomicBoolean(false); + private final Handler handler = new Handler(Looper.getMainLooper()); + private final Runnable refreshTitleRunnable = this::invalidateActionBarTitle; + private boolean showLastSeen = false; + private static boolean isViewOrShareIntent(Intent i) { Log.d(Config.LOGTAG, "action: " + (i == null ? null : i.getAction())); return i != null && VIEW_AND_SHARE_ACTIONS.contains(i.getAction()) && i.hasExtra(EXTRA_CONVERSATION); @@ -660,6 +670,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio this.mSkipBackgroundBinding = false; } mRedirectInProcess.set(false); + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + this.showLastSeen = preferences.getBoolean("last_activity", false); BottomNavigationView bottomNavigationView = findViewById(R.id.bottom_navigation); bottomNavigationView.setSelectedItemId(R.id.chats); @@ -735,6 +747,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio if (actionBar == null) { return; } + final FragmentManager fragmentManager = getFragmentManager(); final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment); if (mainFragment instanceof ConversationFragment) { @@ -750,10 +763,38 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio binding.toolbar, (v) -> openConversationDetails(conversation) ); + + if (conversation.getMode() == Conversation.MODE_MULTI && conversation.getNextCounterpart() == null) { + int usersCount = conversation.getMucOptions().getUserCount(); + if (usersCount > 0) { + actionBar.setSubtitle(getResources().getQuantityString(R.plurals.x_participants, conversation.getMucOptions().getUserCount(), conversation.getMucOptions().getUserCount())); + } else { + actionBar.setSubtitle(""); + } + + handler.postDelayed(refreshTitleRunnable, 5000L); + } else if (conversation.getMode() == Conversation.MODE_SINGLE) { + Contact contact = conversation.getContact(); + if (showLastSeen + && contact.getLastseen() > 0 + && contact.getPresences().allOrNonSupport(Namespace.IDLE)) { + actionBar.setSubtitle(UIHelper.lastseen(getApplicationContext(), contact.isActive(), contact.getLastseen(), true)); + } else { + actionBar.setSubtitle(""); + handler.removeCallbacks(refreshTitleRunnable); + } + } else { + actionBar.setSubtitle(""); + handler.removeCallbacks(refreshTitleRunnable); + } + return; } } + + handler.removeCallbacks(refreshTitleRunnable); actionBar.setTitle(R.string.app_name); + actionBar.setSubtitle(""); actionBar.setDisplayHomeAsUpEnabled(false); ActionBarUtil.resetActionBarOnClickListeners(binding.toolbar); } diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java index ea674c893..f684c5f46 100644 --- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java @@ -205,26 +205,60 @@ public class UIHelper { .get(Calendar.DAY_OF_YEAR); } - public static String lastseen(Context context, boolean active, long time) { + public static String lastseen(Context context, boolean active, long time, boolean shortText) { long difference = (System.currentTimeMillis() - time) / 1000; if (active) { - return context.getString(R.string.online_right_now); + if (shortText) { + return context.getString(R.string.online_right_now_short); + } else { + return context.getString(R.string.online_right_now); + } } else if (difference < 60) { - return context.getString(R.string.last_seen_now); + if (shortText) { + return context.getString(R.string.last_seen_now_short); + } else { + return context.getString(R.string.last_seen_now); + } } else if (difference < 60 * 2) { - return context.getString(R.string.last_seen_min); + if (shortText) { + return context.getString(R.string.last_seen_min_short); + } else { + return context.getString(R.string.last_seen_min); + } } else if (difference < 60 * 60) { - return context.getString(R.string.last_seen_mins, Math.round(difference / 60.0)); + if (shortText) { + return context.getString(R.string.last_seen_mins_short, Math.round(difference / 60.0)); + } else { + return context.getString(R.string.last_seen_mins, Math.round(difference / 60.0)); + } } else if (difference < 60 * 60 * 2) { - return context.getString(R.string.last_seen_hour); + if (shortText) { + return context.getString(R.string.last_seen_hour_short); + } else { + return context.getString(R.string.last_seen_hour); + } } else if (difference < 60 * 60 * 24) { - return context.getString(R.string.last_seen_hours, - Math.round(difference / (60.0 * 60.0))); + if (shortText) { + return context.getString(R.string.last_seen_hours_short, + Math.round(difference / (60.0 * 60.0))); + } else { + return context.getString(R.string.last_seen_hours, + Math.round(difference / (60.0 * 60.0))); + } } else if (difference < 60 * 60 * 48) { - return context.getString(R.string.last_seen_day); + if (shortText) { + return context.getString(R.string.last_seen_day_short); + } else { + return context.getString(R.string.last_seen_day); + } } else { - return context.getString(R.string.last_seen_days, - Math.round(difference / (60.0 * 60.0 * 24.0))); + if (shortText) { + return context.getString(R.string.last_seen_days_short, + Math.round(difference / (60.0 * 60.0 * 24.0))); + } else { + return context.getString(R.string.last_seen_days, + Math.round(difference / (60.0 * 60.0 * 24.0))); + } } } diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index a6c021455..92ce2eac0 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -1108,6 +1108,19 @@ Rounded Square Square + + %1$d participant + %1$d participants + + + online + just now + one minute ago + %d minutes ago + one hour ago + %d hours ago + one day ago + %d days ago Clarendon OldMan -- 2.39.2 From e3542ccf723b43ab260c1923c2633f0efd30b4c5 Mon Sep 17 00:00:00 2001 From: kosyak Date: Mon, 2 Sep 2024 01:08:27 +0200 Subject: [PATCH 04/56] take last seen into account during status indicator calculation --- .../ui/ContactDetailsActivity.java | 2 +- .../ui/adapter/ConversationAdapter.java | 2 +- .../ui/adapter/ListItemAdapter.java | 2 +- .../conversations/ui/adapter/UserAdapter.java | 2 +- .../ui/adapter/UserPreviewAdapter.java | 2 +- .../ui/widget/PresenceIndicator.kt | 22 +++++++++++++------ 6 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index 8daffdbbc..37dd2e88d 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -523,7 +523,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp AvatarWorkerTask.loadAvatar(contact, binding.detailsContactBadge, R.dimen.avatar_on_details_screen_size); binding.detailsContactBadge.setOnClickListener(this::onBadgeClick); - binding.presenceIndicator.setStatus(contact.getShownStatus()); + binding.presenceIndicator.setStatus(contact); binding.detailsContactKeys.removeAllViews(); boolean hasKeys = false; 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 22a41b59b..bac12514b 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java @@ -393,7 +393,7 @@ public class ConversationAdapter Contact contact = conversation.getContact(); if (contact != null) { - viewHolder.binding.presenceIndicator.setStatus(contact.getShownStatus()); + viewHolder.binding.presenceIndicator.setStatus(contact); } else { viewHolder.binding.presenceIndicator.setStatus(null); } 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 940d5cb2b..f9444a06c 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java @@ -94,7 +94,7 @@ public class ListItemAdapter extends ArrayAdapter { AvatarWorkerTask.loadAvatar(item, viewHolder.avatar, R.dimen.avatar); if (item instanceof Contact) { - viewHolder.presenceIndicator.setStatus(((Contact) item).getShownStatus()); + viewHolder.presenceIndicator.setStatus(((Contact) item)); } else { viewHolder.presenceIndicator.setStatus(null); } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java index 525fec6f6..e1fbfa283 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java @@ -91,7 +91,7 @@ public class UserAdapter extends ListAdapter 60)) { + this.status = null + } else { + this.status = status + } + + invalidate() + } + override fun onDraw(canvas: Canvas) { super.onDraw(canvas) -- 2.39.2 From 021552b1d479b9b39714087324c9580e1d5cda9d Mon Sep 17 00:00:00 2001 From: kosyak Date: Mon, 2 Sep 2024 01:10:32 +0200 Subject: [PATCH 05/56] try to fix wired jump to bottom button behavior --- .../java/eu/siacs/conversations/ui/ConversationFragment.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index ebea2bc31..1b6eb44a4 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -590,7 +590,7 @@ public class ConversationFragment extends XmppFragment public void onClick(View v) { stopScrolling(); - if (previousClickedReply != null) { + /*if (previousClickedReply != null) { int lastVisiblePosition = binding.messagesView.getLastVisiblePosition(); Message lastVisibleMessage = messageListAdapter.getItem(lastVisiblePosition); Message jump = previousClickedReply; @@ -602,7 +602,7 @@ public class ConversationFragment extends XmppFragment return; } } - } + }*/ if (conversation.isInHistoryPart()) { conversation.jumpToLatest(); -- 2.39.2 From 95ee8459b8d6b9a6d62576c0f439c070999ab72c Mon Sep 17 00:00:00 2001 From: kosyak Date: Mon, 2 Sep 2024 22:16:05 +0200 Subject: [PATCH 06/56] don't use jabber last seen in status calculations --- .../eu/siacs/conversations/ui/ConversationsActivity.java | 8 ++++---- .../eu/siacs/conversations/ui/widget/PresenceIndicator.kt | 8 ++------ 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java index 5d7762488..79c70d46b 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java @@ -775,10 +775,10 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio handler.postDelayed(refreshTitleRunnable, 5000L); } else if (conversation.getMode() == Conversation.MODE_SINGLE) { Contact contact = conversation.getContact(); - if (showLastSeen - && contact.getLastseen() > 0 - && contact.getPresences().allOrNonSupport(Namespace.IDLE)) { - actionBar.setSubtitle(UIHelper.lastseen(getApplicationContext(), contact.isActive(), contact.getLastseen(), true)); + List statuses = contact.getPresences().getStatusMessages(); + if (!statuses.isEmpty() && !statuses.get(0).isBlank()) { + actionBar.setSubtitle(statuses.get(0)); + handler.postDelayed(refreshTitleRunnable, 5000L); } else { actionBar.setSubtitle(""); handler.removeCallbacks(refreshTitleRunnable); diff --git a/src/main/java/eu/siacs/conversations/ui/widget/PresenceIndicator.kt b/src/main/java/eu/siacs/conversations/ui/widget/PresenceIndicator.kt index 55ab27809..f4ecaf21e 100644 --- a/src/main/java/eu/siacs/conversations/ui/widget/PresenceIndicator.kt +++ b/src/main/java/eu/siacs/conversations/ui/widget/PresenceIndicator.kt @@ -49,14 +49,10 @@ class PresenceIndicator : View { fun setStatus(contact: Contact?) { val status = contact?.shownStatus - if ((status == Presence.Status.ONLINE || status == Presence.Status.CHAT) && - (contact.lastseen <= 0 || !contact.presences.allOrNonSupport(Namespace.IDLE) || ((System.currentTimeMillis() - contact.lastseen) / 1000) > 60)) { - this.status = null - } else { + if (status != this.status) { this.status = status + invalidate() } - - invalidate() } override fun onDraw(canvas: Canvas) { -- 2.39.2 From cf9d0e13b150f5d23235a9536555c4cf25a2b73b Mon Sep 17 00:00:00 2001 From: Sergei Poljanski Date: Wed, 4 Sep 2024 03:32:18 +0300 Subject: [PATCH 07/56] changelogs and f-droid download icon --- CHANGELOG.md | 1210 +---------------- README.md | 12 +- .../android/en-US/changelogs/4211504.txt | 6 + .../android/ru/changelogs/4211504.txt | 6 + .../android/uk/changelogs/4211504.txt | 6 + 5 files changed, 92 insertions(+), 1148 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/4211504.txt create mode 100644 fastlane/metadata/android/ru/changelogs/4211504.txt create mode 100644 fastlane/metadata/android/uk/changelogs/4211504.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e9eb28bd..d141b9720 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,1147 +1,67 @@ # Changelog -### Version 2.13.0 - -* Easier access to 'Show QR code' -* Support PEP Native Bookmarks -* Add support for SDP Offer / Answer Model (Used by SIP gateways) -* Raise target API to Android 14 - -### Version 2.12.12 - -* Support Private DNS (DNS over TLS) -* Support themed launcher icon -* Fix rare permission issue when sharing files on Android 11+ - -### Version 2.12.11 - -* Bump libwebrtc dependency to M117 and bump libvpx -* Go back to AAC for voice messages -* Support per app language settings - -### Version 2.12.10 - -* support per conversation notification settings -* use opus for voice messages on Android 10 - -### Version 2.12.9 - -* Introduce new backup file format - -### Version 2.12.8 - -* Disable opening backup files (.ceb) from file manager - -### Version 2.12.7 - -* Remove channel discovery feature from Google Play version - -### Version 2.12.6 - -* Fix 'q' falsely being recognized as cyrillic - -### Version 2.12.5 - -* Bump Target SDK to 33 again -* Fix issues on servers supporting SASL2 w/o inline Stream Management - -### Version 2.12.4 - -* Revert Target SDK bump (back to 32) to fix various issues on Android 13 - -### Version 2.12.3 - -* Improve support for new emojis -* Add ability to remove account from server -* Show timestamp for calls - -### Version 2.12.2 - -* Increase corner radius on profile pictures - -### 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 - -### 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 - -### 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 -* Implement Channel Binding -* Add ability to switch from audio call to video call -* Add ability to delete own avatar -* Add notification for missed calls - -### 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) -* Fix bug when calling Movim - -### Version 2.10.8 - -* Fix wrong avatar being shown for group chats - -### 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 - -### 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 -* Remove footnote with regards to server fee - -### Version 2.10.3 - -* 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 - -* Fix crash when rendering some quotes -* Fix crash in welcome screen - -### Version 2.10.1 - -* Fix issue with some videos not being compressed -* Fix rare crash when opening notification - -### Version 2.10.0 - -* Show black bars when remote video does not match aspect ratio of screen -* Improve search performance -* Add setting to prevent screenshots - -### Version 2.9.13 - -* minor A/V improvements - -### Version 2.9.12 - -* Always verify domain name. No user overwrite -* Support roster pre authentication - -### Version 2.9.11 - -* Fixed 'No Connectivity' issues on Android 7.1 - -### Version 2.9.10 -* fix HTTP up/download for users that don’t trust system CAs - -### Version 2.9.9 - -* Various bug fixes around Tor support -* Improve call compatibility with Dino - -### Version 2.9.8 - -* Verify A/V calls with preexisting OMEMO sessions -* Improve compatibility with non libwebrtc WebRTC implementations - -### Version 2.9.7 - -* Ability to select incoming call ringtone -* Fix OpenPGP key id discovery for OpenKeychain 5.6+ -* Properly verify punycode TLS certificates -* Improve stability of RTP session establishment (calling) - -### Version 2.9.6 - -* Show call button for offline contacts if they previously announced support -* Back button no longer ends call when call is connected -* bug fixes - -### Version 2.9.5 - -* Quicksy: Automatically receive verification SMS - -### Version 2.9.4 -* minor stability improvements for A/V calls -* Conversations releases from here on forward require Android 5 - -### Version 2.9.3 - -* Fixed connectivity issues when different accounts used different SCRAM mechanisms -* Add support for SCRAM-SHA-512 -* Allow P2P (Jingle) file transfer with self contact - -### Version 2.9.2 - -* Offer Easy Invite generation on supporting servers -* Display GIFs send from Movim -* store avatars in cache - -### Version 2.9.1 - -* fixed search on Android <= 5 -* optimize memory consumption - -### Version 2.9.0 - -* Search individual conversations -* Notify user if message delivery fails -* Remember display names (nicks) from Quicksy users across restarts -* Add button to start Orbot (Tor) from notification if necessary - -### Version 2.8.10 - -* Handle GPX files -* Improve performance for backup restore -* bug fixes - -### Version 2.8.9 - -* add 'Return to chat' to audio call screen -* Improve keyboard shortcuts -* bug fixes - -### Version 2.8.8 - -* Fixed notifications not showing up under certain conditions -* Fixed compatibility issues and crashes related to A/V calls - -### Version 2.8.7 - -* Show help button if A/V call fails -* Fixed some annoying crashes -* Fixed Jingle connections (file transfer + calls) with bare JIDs - -### Version 2.8.6 - -* Offer to record voice message when callee is busy - -### Version 2.8.5 - -* Reduce echo during calls on some devices -* Fix login when passwords contains special characters -* Play dial and busy tones on speaker during video calls - -### Version 2.8.4 - -* Rework Login with certificate UI -* Add ability to pin chats on top (add to favorites) - -### Version 2.8.3 - -* Move call icon to the left in order to keep other toolbar icons in a consistent place -* Show call duration during audio calls -* Tie breaking for A/V calls (the same two people calling each other at the same time) - -### Version 2.8.2 - -* Add button to switch camera during video call -* Fixed voice calls on tablets - -### Version 2.8.1 - -* Audible feedback (dialing, call started, call ended) for voice calls. -* Fixed issue with retrying failed video call - -### Version 2.8.0 - -* Audio/Video calls (Requires server support in form of STUN and TURN servers discoverable via XEP-0215) - - -### Version 2.7.1 - -* Fix avatar selection on some Android 10 devices -* Fix file transfer for larger files - -### Version 2.7.0 - -* Provide PDF preview on Android 5+ -* Use 12 byte IVs for OMEMO - -### Version 2.6.4 - -* Support automatic theme switching on Android 10 - -### Version 2.6.3 - -* Support for ?register and ?register;preauth XMPP uri parameters - -### Version 2.6.2 -* let users set their own nick name -* resume download of OMEMO encrypted files -* Channels now use '#' as symbol in avatar -* Quicksy uses 'always' as OMEMO encryption default (hides lock icon) - -### Version 2.6.1 -* fixes for Jingle IBB file transfer -* fixes for repeated corrections filling up the database -* switched to Last Message Correction v1.1 - -### Version 2.6.0 -* Introduce expert setting to perform channel discovery on local server instead of [search.jabber.network](https://search.jabber.network) -* Enable delivery check marks by default and remove setting -* Enable ‘Send button indicates status’ by default and remove setting -* Move Backup and Foreground Service settings to main screen - -### Version 2.5.12 -* Jingle file transfer fixes -* Fixed OMEMO self healing (after backup restore) on servers w/o MAM - -### Version 2.5.11 -* Fixed crash on Android <5.0 - -### Version 2.5.10 -* Fixed crash on Xiaomi devices running Android 8.0 + 8.1 - -### Version 2.5.9 -* fixed minor security issues -* Share XMPP uri from channel search by long pressing a result - -### Version 2.5.8 -* fixed connection issues over Tor -* P2P file transfer (Jingle) now offers direct candidates -* Support XEP-0396: Jingle Encrypted Transports - OMEMO - -### Version 2.5.7 -* fixed crash when scanning QR codes on Android 6 and lower -* when sharing a message from and to Conversations insert it as quote - -### Version 2.5.6 -* fixes for Jingle file transfer -* fixed some rare crashes - -### Version 2.5.5 -* allow backups to be restored from anywhere -* bug fixes - -### Version 2.5.4 -* stability improvements for group chats and channels - -### Version 2.5.3 -* bug fixes for peer to peer file transfer (Jingle) -* fixed server info for unlimited/unknown max file size - -### Version 2.5.2 -* bug fixes - -### Version 2.5.1 -* minor bug fixes -* Set own OMEMO devices to inactive after not seeing them for 14 days. (was 7 days) - -### Version 2.5.0 -* Added channel search via search.jabbercat.org -* Reworked onboarding screens -* Warn when trying to enter domain address or channel address in Add Contact dialog - -### Version 2.4.3 -* Fixed display of private messages sent from another client -* Fixed backup creation on long time installations - -### Version 2.4.2 -* Fix image preview on older Android version - -### Version 2.4.1 -* Fixed crash in message view - -### Version 2.4.0 -* New Backup / Restore feature -* Clearly distinguish between (private) group chats and (public) channels -* Redesigned participants view for group chats and channels -* Redesigned create new contact/group chat/channel flow in Start Conversation screen - - -### Version 2.3.12 -* Fixed rare crash on start up -* Fixed avatar not being refreshed in group chats - -### Version 2.3.11 -* Support for Android 9 'message style' notifications -* OMEMO stability improvements -* Added ability to destroy group chats -* Do not show deleted files in media browser -* Added 'Keep Original' as video quality choice - -### Version 2.3.10 -* lower minimum required Android version to 4.1 -* Synchronize group chat join/leaves across multiple clients -* Fixed sending PGP encrypted messages from quick reply - -### Version 2.3.9 -* OMEMO stability improvements -* Context menu when long pressing avatar in 1:1 chat - -### Version 2.3.8 -* make PEP avatars public to play nice with Prosody 0.11 -* Fixed re-sending failed files in group chats - -### Version 2.3.7 -* long press on 'allow' or 'add back' snackbar to bring up 'reject' -* bug fixes for Android 9 - -### Version 2.3.6 -* Improved handling of bookmark nicks -* Show send PM menu entry in anonymous MUCs - -### Version 2.3.5 -* Fixed group chat mentions when nick ends in . (dot) -* Fixed Conversations not asking for permissions after direct share -* Fixed CVE-2018-18467 - -### Version 2.3.4 -* Fixed sending OMEMO files to ChatSecure - -### Version 2.3.3 -* Fixed connection issues with user@ip type JIDs - -### Version 2.3.2 -* Fixed OMEMO on Android 5.1 & 6.0 -* Added setting for video quality -* bug fixes - -### Version 2.3.1 -* Stronger compression for video files -* Use SNI on STARTTLS to fix gtalk -* Fix Quiet Hours on Android 8+ -* Use Consistent Color Generation (XEP-0392) - -### Version 2.3.0 -* Preview and ask for confirmation before sending media files -* View per conversation media files in contact and conference details screens -* Enable foreground service by default for Android 8 (notification can be disabled by long pressing it) -* Audio player: disable screen and switch to ear piece -* Support TLSv1.3 (ejabberd ≤ 18.06 is incompatible with openssl 1.1.1 - Update ejabberd or downgrade openssl if you get ›Stream opening error‹) - - -### Version 2.2.9 -* Store bookmarks in PEP if server has ability to convert to old bookmarks -* Show Jabber IDs from address book in Start Conversation screen - -### Version 2.2.8 -* fixed regression that broke XMPP uris - -### Version 2.2.7 -* stability improvements - -### Version 2.2.6 -* support old MAM version to work with Prosody - -### Version 2.2.5 -* Persist MUC avatar across restarts / show in bookmarks -* Offer Paste as quote for HTML content - -### Version 2.2.4 -* Use group chat name as primary identifier -* Show group name and subject in group chat details -* Upload group chat avatar on compatible servers - -### Version 2.2.3 -* Introduce Expert Setting to enable direct search -* Introduce Paste As Quote on Android 6+ -* Fixed issues with HTTP Upload - -### Version 2.2.2 -* Fixed connection problems with TLS1.3 servers -* Attempt to delete broken bundles from PEP -* Use FCM instead of GCM - -### Version 2.2.1 -* improved recording quality -* load map tiles over Tor if enabled - -### Version 2.2.0 -* Integrate Voice Recorder -* Integrate Share Location -* Added ability to search messages - -### Version 2.1.4 -* bug fixes - -### Version 2.1.3 -* Do not process stanzas with invalid JIDs - -### Version 2.1.2 -* Fixed avatars not being displayed on new installs - -### Version 2.1.1 -* Improved start up performance -* bug fixes - -### Version 2.1.0 -* Added configurable font size -* Added global OMEMO preference -* Added scroll to bottom button -* Only mark visible messages as read - - -### Version 2.0.0 -* OMEMO by default for everything but public group chats -* Integrate QR code scanner (requires camera permission) -* Removed support for OTR -* Removed support for customizable resources -* Removed slide out panel for conversation overview -* Add ability to change status message -* Highlight irregular unicode code blocks in Jabber IDs -* Conversations now requires Android 4.4+ - -### Version 1.23.8 -* bug fixes - -### Version 1.23.7 -* Improved MAM support + bug fixes - -### Version 1.23.6 -* Fixed crash on receiving invalid HTTP slot response - -### Version 1.23.5 -* improved self chat - -### Version 1.23.4 -* keep screen on while playing audio -* send delivery receipts after MAM catch-up -* reduce number of wake locks - -### Version 1.23.3 -* Fixed OMEMO device list not being announced - -### Version 1.23.2 -* Removed NFC support -* upload Avatars as JPEG -* reduce APK size - -### Version 1.23.1 -* Show icon instead of image preview in conversation overview -* fixed loop when trying to decrypt with YubiKey - -### Version 1.23.0 -* Support for read markers in private, non-anonymous group chats - -### Version 1.22.1 -* Disable swipe to left to end conversation -* Fixed 'No permission to access …' when opening files shared from the SD card -* Always open URLs in new tab - -### Version 1.22.0 -* Text markup *bold*, _italic_,`monospace` and ~strikethrough~ -* Use same emoji style on all Android versions -* Display emojis slightly larger within continuous text - -### Version 1.21.0 -* Inline player for audio messages -* Stronger compression for long videos -* Long press the 'add back' button to show block menu - -### Version 1.20.1 -* fixed OTR encrypted file transfer - -### Version 1.20.0 -* presence subscription no longer required for OMEMO on compatible servers -* display emoji-only messages slightly larger - -### Version 1.19.5 -* fixed connection loop on Android <4.4 - -### Version 1.19.4 -* work around for OpensFire’s self signed certs -* use VPN’s DNS servers first - -### Version 1.19.3 -* Do not create foreground service when all accounts are disabled -* bug fixes - -### Version 1.19.2 -* bug fixes - -### Version 1.19.1 -* Made DNSSEC hostname validation opt-in - -### Version 1.19.0 -* Added 'App Shortcuts' to quickly access frequent contacts -* Use DNSSEC to verify hostname instead of domain in certificate -* Setting to enable Heads-up notifications -* Added date separators in message view - -### Version 1.18.5 -* colorize send button only after history is caught up -* improved MAM catchup strategy - -### Version 1.18.4 -* fixed UI freezes during connection timeout -* fixed notification sound playing twice -* fixed conversations being marked as read -* removed 'copy text' in favor of 'select text' and 'share with' - -### Version 1.18.3 -* limited GPG encryption for MUC offline members - -### Version 1.18.2 -* added support for Android Auto -* fixed HTTP Download over Tor -* work around for nimbuzz.com MUCs - -### Version 1.18.1 -* bug fixes - -### Version 1.18.0 -* Conversations <1.16.0 will be unable to receive OMEMO encrypted messages -* OMEMO: put auth tag into key (verify auth tag as well) -* offer to block entire domain in message from stranger snackbar -* treat URL as file if URL is in oob or contains key - -### Version 1.17.1 -* Switch Aztec to QR for faster scans -* Fixed unread counter for image messages - -### Version 1.17.0 -* Do not notify for messages from strangers by default -* Blocking a JID closes the corresponding conversation -* Show message sender in conversation overview -* Show unread counter for every conversation -* Send typing notifications in private, non-anonymous MUCs -* Support for the latest MAM namespace -* Icons for attach menu - -### Version 1.16.2 -* change mam catchup strategy. support mam:1 -* bug fixes - -### Version 1.16.1 -* UI performance fixes -* bug fixes - -### Version 1.16.0 -* configurable client side message retention period -* compress videos before sending them - -### Version 1.15.5 -* show nick as bold text when mentioned in conference -* bug fixes - -### Version 1.15.4 -* bug fixes - -### Version 1.15.3 -* show offline contacts in MUC as grayed-out -* don't transcode gifs. add overlay indication to gifs -* bug fixes - -### Version 1.15.2 -* bug fixes - -### Version 1.15.1 -* support for POSH (RFC7711) -* support for quoting messages (via select text) -* verified messages show shield icon. unverified messages show lock - -### Version 1.15.0 -* New [Blind Trust Before Verification](https://gultsch.de/trust.html) mode -* Easily share Barcode and XMPP uri from Account details -* Automatically deactivate own devices after 7 day of inactivity -* Improvements fo doze/push mode -* bug fixes - -### Version 1.14.9 -* warn in account details when data saver is enabled -* automatically enable foreground service after detecting frequent restarts -* bug fixes - -### Version 1.14.8 -* bug fixes - -### Version 1.14.7 -* error message accessible via context menu for failed messages -* don't include pgp signature in anonymous mucs -* bug fixes - -### Version 1.14.6 -* make error notification dismissible -* bug fixes - - -### Version 1.14.5 -* expert setting to delete OMEMO identities -* bug fixes - -### Version 1.14.4 -* bug fixes - -### Version 1.14.3 -* XEP-0377: Spam Reporting -* fix rare start up crashes - -### Version 1.14.2 -* support ANONYMOUS SASL -* bug fixes - -### Version 1.14.1 -* Press lock icon to see why OMEMO is deactivated -* bug fixes - -### Version 1.14.0 -* Improvements for N -* Quick Reply to Notifications on N -* Don't download avatars and files when data saver is on -* bug fixes - -### Version 1.13.9 -* bug fixes - -### Version 1.13.8 -* show identities instead of resources in selection dialog -* allow TLS direct connect when port is set to 5223 -* bug fixes - -### Version 1.13.7 -* bug fixes - -### Version 1.13.6 -* thumbnails for videos -* bug fixes - -### Version 1.13.5 -* bug fixes - -### Version 1.13.4 -* support jingle ft:4 -* show contact as DND if one resource is -* bug fixes - -### Version 1.13.3 -* bug fixes - -### Version 1.13.2 -* new PGP decryption logic -* bug fixes - -### Version 1.13.1 -* changed some colors in dark theme -* fixed fall-back message for OMEMO - -### Version 1.13.0 -* configurable dark theme -* opt-in to share Last User Interaction - -### Version 1.12.9 -* make grace period configurable - -### Version 1.12.8 -* more bug fixes :-( - -### Version 1.12.7 -* bug fixes - -### Version 1.12.6 -* bug fixes - -### Version 1.12.5 -* new create conference dialog -* show first unread message on top -* show geo uri as links -* circumvent long message DOS - -### Version 1.12.4 -* show offline members in conference (needs server support) -* various bug fixes - -### Version 1.12.3 -* make omemo default when all resources support it -* show presence of other resources as template -* start typing in StartConversationsActivity to search -* various bug fixes and improvements - -### Version 1.12.2 -* fixed pgp presence signing - -### Version 1.12.1 -* small bug fixes - -### Version 1.12.0 -* new welcome screen that makes it easier to register account -* expert setting to modify presence - -### Version 1.11.7 -* Share xmpp uri from conference details -* add setting to allow quick sharing -* various bug fixes - -### Version 1.11.6 -* added preference to disable notification light -* various bug fixes - -### Version 1.11.5 -* check file ownership to not accidentally share private files - -### Version 1.11.4 -* fixed a bug where contacts are shown as offline -* improved broken PEP detection - -### Version 1.11.3 -* check maximum file size when using HTTP Upload -* properly calculate caps hash - -### Version 1.11.2 -* only add image files to media scanner -* allow to delete files -* various bug fixes - -### Version 1.11.1 -* fixed some bugs when sharing files with Conversations - -### Version 1.11.0 -* OMEMO encrypted conferences - -### Version 1.10.1 -* made message correction opt-in -* various bug fixes - -### Version 1.10.0 -* Support for XEP-0357: Push Notifications -* Support for XEP-0308: Last Message Correction -* introduced build flavors to make dependence on play-services optional - -### Version 1.9.4 -* prevent cleared Conversations from reloading history with MAM -* various MAM fixes - -### Version 1.9.3 -* expert setting that enables host and port configuration -* expert setting opt-out of bookmark autojoin handling -* offer to rejoin a conference after server sent unavailable -* internal rewrites - -### Version 1.9.2 -* prevent startup crash on Sailfish OS -* minor bug fixes - -### Version 1.9.1 -* minor bug fixes incl. a workaround for nimbuzz.com - -### Version 1.9.0 -* Per conference notification settings -* Let user decide whether to compress pictures -* Support for XEP-0368 -* Ask user to exclude Conversations from battery optimizations - -### Version 1.8.4 -* prompt to trust own OMEMO devices -* fixed rotation issues in avatar publication -* invite non-contact JIDs to conferences - -### Version 1.8.3 -* brought text selection back - -### Version 1.8.2 -* fixed stuck at 'connecting...' bug -* make message box behave correctly with multiple links - -### Version 1.8.1 -* enabled direct share on Android 6.0 -* ask for permissions on Android 6.0 -* notify on MAM catchup messages -* bug fixes - -### Version 1.8.0 -* TOR/ORBOT support in advanced settings -* show vcard avatars of participants in a conference - -### Version 1.7.3 -* fixed PGP encrypted file transfer -* fixed repeating messages in slack conferences - -### Version 1.7.2 -* decode PGP messages in background - -### Version 1.7.1 -* performance improvements when opening a conversation - -### Version 1.7.0 -* CAPTCHA support -* SASL EXTERNAL (client certificates) -* fetching MUC history via MAM -* redownload deleted files from HTTP hosts -* Expert setting to automatically set presence -* bug fixes - -### Version 1.6.11 -* tab completion for MUC nicks -* history export -* bug fixes - -### Version 1.6.10 -* fixed facebook login -* fixed bug with ejabberd mam -* use official HTTP File Upload namespace - -### Version 1.6.9 -* basic keyboard support - -### Version 1.6.8 -* reworked 'enter is send' setting -* reworked DNS server discovery on lolipop devices -* various bug fixes - -### Version 1.6.7 -* bug fixes - -### Version 1.6.6 -* best 1.6 release yet - -### Version 1.6.5 -* more OMEMO fixes - -### Version 1.6.4 -* setting to enable white chat bubbles -* limit OMEMO key publish attempts to work around broken PEP -* various bug fixes - -### Version 1.6.3 -* bug fixes - -### Version 1.6.2 -* fixed issues with connection time out when server does not support ping - -### Version 1.6.1 -* fixed crashes - -### Version 1.6.0 -* new multi-end-to-multi-end encryption method -* redesigned chat bubbles -* show unexpected encryption changes as red chat bubbles -* always notify in private/non-anonymous conferences - -### Version 1.5.1 -* fixed rare crashes -* improved otr support - -### Version 1.5.0 -* upload files to HTTP host and share them in MUCs. requires new [HttpUploadComponent](https://github.com/siacs/HttpUploadComponent) on server side - -### Version 1.4.5 -* fixes to message parser to not display some ejabberd muc status messages - -### Version 1.4.4 -* added unread count badges on supported devices -* rewrote message parser - -### Version 1.4.0 -* send button turns into quick action button to offer faster access to take photo, send location or record audio -* visually separate merged messages -* faster reconnects of failed accounts after network switches -* r/o vcard avatars for contacts -* various bug fixes - -### Version 1.3.0 -* swipe conversations to end them -* quickly enable / disable account via slider -* share multiple images at once -* expert option to distrust system CAs -* mlink compatibility -* bug fixes - -### Version 1.2.0 -* Send current location. (requires [plugin](https://play.google.com/store/apps/details?id=eu.siacs.conversations.sharelocation)) -* Invite multiple contacts at once -* performance improvements -* bug fixes - -### Version 1.1.0 -* Typing notifications (must be turned on in settings) -* Various UI performance improvements -* bug fixes - -### Version 1.0.4 -* load avatars asynchronously on start up -* support for XEP-0092: Software Version - -### Version 1.0.3 -* load messages asynchronously on start up -* bug fixes - -### Version 1.0.2 -* skipped - -### Version 1.0.1 -* accept more ciphers - -### Version 1.0 -* MUC controls (Affiliation changes) -* Added download button to notification -* Added check box to hide offline contacts -* Use Material theme and icons on Android L -* Improved security -* bug fixes + code clean up - -### Version 0.10 -* Support for Message Archive Management -* Dynamically load message history -* Ability to block contacts -* New UI to verify fingerprints -* Ability to change password on server -* removed stream compression -* quiet hours -* fixed connection issues on ipv6 servers - -### Version 0.9.3 -* bug fixes - -### Version 0.9.2 -* more bug fixes - -### Version 0.9.1 -* bug fixes including some that caused Conversations to crash on start - -### Version 0.9 -* arbitrary file transfer -* more options to verify OTR (SMP, QR Codes, NFC) -* ability to create instant conferences -* r/o dynamic tags (presence and roster groups) -* optional foreground service (expert option) -* added SCRAM-SHA1 login method -* bug fixes - -### Version 0.8.4 -* bug fixes - -### Version 0.8.3 -* increased UI performance -* fixed rotation bugs - -### Version 0.8.2 -* Share contacts via QR codes or NFC -* Slightly improved UI -* minor bug fixes - -### Version 0.8.1 -* minor bug fixes - -### Version 0.8 -* Download HTTP images -* Show avatars in MUC tiles -* Disabled SSLv3 -* Performance improvements -* bug fixes - -### Version 0.7.3 -* revised tablet ui -* internal rewrites -* bug fixes - -### Version 0.7.2 -* show full timestamp in messages -* brought back option to use JID to identify conferences -* optionally request delivery receipts (expert option) -* more languages -* bug fixes - -### Version 0.7.1 -* Optionally use send button as status indicator - -### Version 0.7 -* Ability to disable notifications for single conversations -* Merge messages in chat bubbles -* Fixes for OpenPGP and OTR (please republish your public key) -* Improved reliability on sending messages -* Join password protected Conferences -* Configurable font size -* Expert options for encryption - -### Version 0.6 -* Support for server side avatars -* save images in gallery -* show contact name and picture in non-anonymous conferences -* reworked account creation -* various bug fixes - -### Version 0.5.2 -* minor bug fixes - -### Version 0.5.1 -* couple of small bug fixes that have been missed in 0.5 -* complete translations for Swedish, Dutch, German, Spanish, French, Russian - -### Version 0.5 -* UI overhaul -* MUC / Conference bookmarks -* A lot of bug fixes - -### Version 0.4 -* OTR file encryption -* keep OTR messages and files on device until both parties or online at the same time -* XEP-0333. Mark whether the other party has read your messages -* Delayed messages are now tagged properly -* Share images from the Gallery -* Infinite history scrolling -* Mark the last used presence in presence selection dialog - -### Version 0.3 -* Mostly bug fixes and internal rewrites -* Touch contact picture in conference to highlight -* Long press on received image to share -* made OTR more reliable -* improved issues with occasional message lost -* experimental conference encryption. (see FAQ) - -### Version 0.2.3 -* regression fix with receiving encrypted images - -### Version 0.2.2 -* Ability to take photos directly -* Improved openPGP offline handling -* Various bug fixes -* Updated Translations - -### Version 0.2.1 -* Various bug fixes -* Updated Translations - -### Version 0.2 -* Image file transfer -* Better integration with OpenKeychain (PGP encryption) -* Nicer conversation tiles for conferences -* Ability to clear conversation history -* A lot of bug fixes and code clean up - -### Version 0.1.3 -* Switched to minidns library to resolve SRV records -* Faster DNS in some cases -* Enabled stream compression -* Added permanent notification when an account fails to connect -* Various bug fixes involving message notifications -* Added support for DIGEST-MD5 auth - -### Version 0.1.2 -* Various bug fixes relating to conferences -* Further DNS lookup improvements - -### Version 0.1.1 -* Fixed the 'server not found' bug - -### Version 0.1 -* Initial release +### Conversations Classic 2.3.1 +* Added account indicator +* Group chats are now marked with an additional icon +* Saved messages now have a brighter background color +* Added nickname in reply in Multi-User Chats(MUC) +* Contact status is now displayed separately +* Updated Russian and Ukrainian localizations + +### Conversations Classic 2.3.0 +* Add Cheogram mention on about screen +* Fix save to downloads menu item visibility +* Update Russian and Ukrainian localizations +* Update caps node field +* Notifications throttling setting for all conversations +* Fix gestures handling conflict in commands +* Support navigation via nav bar +* Per conversation custom backgrounds +* Use original file name in chat attachements +* Support avatars shape customization +* Medium font for unread conversation title +* New settings screen +* Show MUC subject as a pinned message + +### Conversations Classic 2.2.4 +* Get rid of outdated kernel version calculation code +* Fix contact long click crash +* Clean code and optimize imports +* Render reply message author name on the top of replied text +* Fix conversation position handling while grouping enabled +* Properly show contact name in MUC + +### Conversations Classic 2.2.2 + +The app was forked from Conversations Version 2.13.0 and renamed to Conversations Classic. + +* Numerous small fixes +* Info about PM on MUC details screen +* Temporary disable domain JID sanity check +* Support 'Save to downloads' action for attachments +* Reactions +* New UI category in expert settings +* Better self contact handling +* Split roster by different accounts +* Group conversation by tags +* Save expanded items in persistent storage +* Fix not working accent color +* Support conversations grouping on ShareWith screen +* LED color changed to blue +* Show real reply text instead of fallback +* New launcher icons +* Fix multiline reply quote +* Fix swipe to reply handling +* Add leakCanary +* Log reason for SM resume failure +* Fix notifications fallback color +* Get rid of command blicks +* Add checkbox for auto downloading all files when connected to Wi-Fi +* Improve scroll to bottom button behavior +* Add notification sound throttling setting, per each contact separately +* Get rid of upstream bug reports system +* Add contact JID copy button +* Move restore backup button from context menu on welcome screen +* Improve replies on file or image messages +* Fix reactions handling in case of missing reply stanza +* Fix replies to audio messages diff --git a/README.md b/README.md index 8b7a76c3d..1c30db98a 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,12 @@

Conversations Classic: the very last word in instant messaging

+

+[Get it on F-Droid](https://f-droid.org/en/packages/eu.siacs.conversations.classic/) +

+ ## Design principles * Be as beautiful and easy to use as possible without sacrificing security or @@ -109,7 +115,7 @@ SCRAM-SHA1, PLAIN, EXTERNAL (client certs) and DIGEST-MD5. Some Bind failures are transient and resolve themselves after a reconnect. When trying to connect to OpenFire the bind failure can be a permanent problem when the domain part of the Jabber ID entered in Conversations Classic doesn’t match the domain the OpenFire server feels responsible for. For example OpenFire is configured to use the domain `a.tld` but the Jabber ID entered is `user@b.tld` where `b.tld` also points to the same host. During bind OpenFire tries to reassign the Jabber to `user@a.tld`. Conversations Classic doesn’t like that. -This can be fixed by creating a new account in Conversations Classic that uses the Jabber ID `user@a.tld`. +This can be fixed by creating a new account in Conversations Classic that uses the Jabber ID `user@a.tld`. Note: This is kind of a weird quirk in OpenFire. Most other servers would just throw a 'Server not responsible for domain' error instead of attempting to reassign the Jabber ID. @@ -128,7 +134,7 @@ The foreground notification is still controlled over the expert settings within Long press the permanent notification and disable that particular type of notification by moving the slider to the left. This will make the notification disappear but create another notification (this time created by the operating system itself.) that will complain about Conversations (and other apps) using battery. Starting with Android 8.1 you can disable that notification again with the same method described above. ##### Android 9.0+ -Long press the permanent notification and press the info `(i)` button to get into the App info screen. In that screen touch the 'Notification' entry. In the next screen remove the checkbox for the 'Foreground service' entry. +Long press the permanent notification and press the info `(i)` button to get into the App info screen. In that screen touch the 'Notification' entry. In the next screen remove the checkbox for the 'Foreground service' entry. #### Conversations doesn’t work for me. Where can I get help? @@ -284,4 +290,4 @@ Read more about the concept on https://gultsch.de/trust.html #### I found a bug -Please report it to our XMPP conference [`xmppclient-dev@conference.narayana.im`](xmpp:xmppclient-dev@conference.narayana.im). +Please report it to our XMPP conference [`xmppclient-dev@conference.narayana.im`](xmpp:xmppclient-dev@conference.narayana.im). diff --git a/fastlane/metadata/android/en-US/changelogs/4211504.txt b/fastlane/metadata/android/en-US/changelogs/4211504.txt new file mode 100644 index 000000000..c8af84748 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4211504.txt @@ -0,0 +1,6 @@ +* Added account indicator +* Group chats are now marked with an additional icon +* Saved messages now have a brighter background color +* Added nickname in reply in Multi-User Chats(MUC) +* Contact status is now displayed separately +* Updated Russian and Ukrainian localizations diff --git a/fastlane/metadata/android/ru/changelogs/4211504.txt b/fastlane/metadata/android/ru/changelogs/4211504.txt new file mode 100644 index 000000000..39a9d50b1 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/4211504.txt @@ -0,0 +1,6 @@ +* Добавлен индикатор учетной записи +* Групповые чаты теперь отмечены дополнительным значком +* Закрепленные сообщения теперь имеют более яркий цвет фона +* Добавлен никнейм в ответ в многопользовательских чатах(MUC) +* Статус контакта теперь отображается в отдельно +* Обновлены русская и украинская локализации diff --git a/fastlane/metadata/android/uk/changelogs/4211504.txt b/fastlane/metadata/android/uk/changelogs/4211504.txt new file mode 100644 index 000000000..39a9d50b1 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/4211504.txt @@ -0,0 +1,6 @@ +* Добавлен индикатор учетной записи +* Групповые чаты теперь отмечены дополнительным значком +* Закрепленные сообщения теперь имеют более яркий цвет фона +* Добавлен никнейм в ответ в многопользовательских чатах(MUC) +* Статус контакта теперь отображается в отдельно +* Обновлены русская и украинская локализации -- 2.39.2 From 03fcd01ddb9e20f2763bdc9c80a73b79508221e7 Mon Sep 17 00:00:00 2001 From: Sergei Poljanski Date: Wed, 4 Sep 2024 03:34:18 +0300 Subject: [PATCH 08/56] fix icon --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1c30db98a..e102598e6 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@

Conversations Classic: the very last word in instant messaging

-

-[ + Get it on F-Droid](https://f-droid.org/en/packages/eu.siacs.conversations.classic/) -

+ height="80"> +

## Design principles -- 2.39.2 From c64e0925f4027d91b53c4aa09bd6b1688984dd73 Mon Sep 17 00:00:00 2001 From: kosyak Date: Fri, 6 Sep 2024 01:02:52 +0200 Subject: [PATCH 09/56] fallback on message uuid to fix missed reply id for single mode conversations --- src/main/java/eu/siacs/conversations/entities/Message.java | 5 ++++- 1 file changed, 4 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 ba7dc1bdb..4150d20ac 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -352,7 +352,10 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } public String replyId() { - return conversation.getMode() == Conversation.MODE_MULTI || getRemoteMsgId() == null ? getServerMsgId() : getRemoteMsgId(); + if (conversation.getMode() == Conversation.MODE_MULTI) return getServerMsgId(); + final String remote = getRemoteMsgId(); + if (remote == null && getStatus() > STATUS_RECEIVED) return getUuid(); + return remote; } public Message reply() { -- 2.39.2 From c32809b9630efde84b698d7fe7ec176bc844d44b Mon Sep 17 00:00:00 2001 From: kosyak Date: Sun, 8 Sep 2024 23:51:05 +0200 Subject: [PATCH 10/56] allow to disable presence and account indicators --- .../ui/adapter/AccountAdapter.java | 3 +- .../ui/widget/AccountIndicator.kt | 37 +++++++++++++++++++ .../ui/widget/PresenceIndicator.kt | 15 ++++++++ src/main/res/layout/contact.xml | 2 +- src/main/res/layout/conversation_list_row.xml | 2 +- src/main/res/values/defaults.xml | 2 + src/main/res/values/strings.xml | 4 ++ src/main/res/xml/preferences.xml | 10 +++++ 8 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/ui/widget/AccountIndicator.kt diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java index 6a23b2cf8..b4109bfdb 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java @@ -96,7 +96,8 @@ public class AccountAdapter extends ArrayAdapter { } }); - if (this.showColorSelector && activity.xmppConnectionService.getAccounts().size() > 1) { + if (this.showColorSelector && activity.xmppConnectionService.getAccounts().size() > 1 && + activity.xmppConnectionService.getPreferences().getBoolean("show_account_indicator", activity.getResources().getBoolean(R.bool.show_account_indicator))) { int color = UIHelper.getAccountColor(activity, account.getJid()); viewHolder.binding.colorView.setVisibility(View.VISIBLE); ColorUtils.setColorViewValue(viewHolder.binding.colorView, color, false, ColorShape.CIRCLE); diff --git a/src/main/java/eu/siacs/conversations/ui/widget/AccountIndicator.kt b/src/main/java/eu/siacs/conversations/ui/widget/AccountIndicator.kt new file mode 100644 index 000000000..f7343aeca --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/widget/AccountIndicator.kt @@ -0,0 +1,37 @@ +package eu.siacs.conversations.ui.widget + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import eu.siacs.conversations.R +import eu.siacs.conversations.ui.XmppActivity + +class AccountIndicator : View { + constructor(context: Context?) : super(context) + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) + + constructor( + context: Context?, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int + ) : super(context, attrs, defStyleAttr, defStyleRes) + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + val enabled = (context as? XmppActivity) + ?.xmppConnectionService?.preferences + ?.getBoolean("show_account_indicator", context.resources.getBoolean(R.bool.show_account_indicator)) ?: false + + visibility = if (enabled) { + VISIBLE + } else { + INVISIBLE + } + } +} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/widget/PresenceIndicator.kt b/src/main/java/eu/siacs/conversations/ui/widget/PresenceIndicator.kt index f4ecaf21e..12b12f60b 100644 --- a/src/main/java/eu/siacs/conversations/ui/widget/PresenceIndicator.kt +++ b/src/main/java/eu/siacs/conversations/ui/widget/PresenceIndicator.kt @@ -8,8 +8,10 @@ import android.graphics.Paint import android.util.AttributeSet import android.view.View import android.view.ViewOutlineProvider +import eu.siacs.conversations.R import eu.siacs.conversations.entities.Contact import eu.siacs.conversations.entities.Presence +import eu.siacs.conversations.ui.XmppActivity import eu.siacs.conversations.ui.util.StyledAttributes import eu.siacs.conversations.utils.UIHelper import eu.siacs.conversations.xml.Namespace @@ -23,6 +25,8 @@ class PresenceIndicator : View { private var status: Presence.Status? = null + private var enabled = false + constructor(context: Context?) : super(context) constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super( @@ -55,7 +59,18 @@ class PresenceIndicator : View { } } + override fun onAttachedToWindow() { + super.onAttachedToWindow() + enabled = (context as? XmppActivity) + ?.xmppConnectionService?.preferences + ?.getBoolean("show_contact_status", context.resources.getBoolean(R.bool.show_contact_status)) ?: false + } + override fun onDraw(canvas: Canvas) { + if (!enabled) { + return + } + super.onDraw(canvas) val color: Int? = UIHelper.getColorForStatus(status); diff --git a/src/main/res/layout/contact.xml b/src/main/res/layout/contact.xml index db9374f1c..923a80368 100644 --- a/src/main/res/layout/contact.xml +++ b/src/main/res/layout/contact.xml @@ -9,7 +9,7 @@ android:clipToPadding="false" android:padding="@dimen/list_padding"> - - false false rounded_square + true + true diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 92ce2eac0..d56415813 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -612,6 +612,10 @@ Don’t open image editor screen automatically for single image attachments Could not connect to OpenKeychain This device is no longer in use + Contact presence + Show contact presence nearby contact avatar + Account indicator + Mark conversations with different colors in case of several account Computer Mobile phone Tablet diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml index 7cbd5fedf..602b57d59 100644 --- a/src/main/res/xml/preferences.xml +++ b/src/main/res/xml/preferences.xml @@ -64,6 +64,16 @@ android:key="always_full_timestamps" android:summary="@string/pref_always_show_full_timestamps_summary" android:title="@string/pref_always_show_full_timestamps" /> + + -- 2.39.2 From 1a751b8a80ba50cf8058111d9569b1a265ccece6 Mon Sep 17 00:00:00 2001 From: kosyak Date: Mon, 9 Sep 2024 00:03:31 +0200 Subject: [PATCH 11/56] don't check if muc pm was archived in case of new message --- .../java/eu/siacs/conversations/parser/MessageParser.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 2ec1bacf2..2706a55e4 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -508,10 +508,6 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } } - if (nextCounterpart != null && mXmppConnectionService.checkIsArchived(account, counterpart.asBareJid(), nextCounterpart)) { - return; - } - if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || oobUrl != null) && !isMucStatusMessage) { final Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), null, conversationIsProbablyMuc, nextCounterpart != null, false, nextCounterpart); final boolean conversationMultiMode = conversation.getMode() == Conversation.MODE_MULTI; -- 2.39.2 From 389074e8027fde6be616f7452177173aea7c2a79 Mon Sep 17 00:00:00 2001 From: kosyak Date: Sun, 29 Sep 2024 23:36:50 +0200 Subject: [PATCH 12/56] Reapply "OTR implementation" This reverts commit c145a6b8e5fc7d7ff262e61cabc11344a9eb88e2. # Conflicts: # src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java # src/main/res/values/strings.xml --- build.gradle | 1 + src/main/AndroidManifest.xml | 5 + .../java/eu/siacs/conversations/Config.java | 7 +- .../conversations/crypto/OtrService.java | 312 ++++++++++++ .../siacs/conversations/entities/Account.java | 39 ++ .../siacs/conversations/entities/Contact.java | 41 ++ .../conversations/entities/Conversation.java | 164 ++++++- .../conversations/entities/Transferable.java | 2 +- .../generator/AbstractGenerator.java | 6 + .../generator/MessageGenerator.java | 47 ++ .../conversations/parser/MessageParser.java | 137 +++++- .../services/XmppConnectionService.java | 110 ++++- .../ui/ConversationFragment.java | 50 ++ .../ui/ConversationsActivity.java | 31 ++ .../conversations/ui/SendLogActivity.java | 1 - .../conversations/ui/VerifyOTRActivity.java | 450 ++++++++++++++++++ .../siacs/conversations/ui/XmppActivity.java | 15 +- .../conversations/utils/CryptoHelper.java | 1 + .../siacs/conversations/utils/UIHelper.java | 2 + .../eu/siacs/conversations/utils/XmppUri.java | 10 +- .../conversations/xmpp/jid/OtrJidHelper.java | 17 + src/main/res/layout/activity_verify_otr.xml | 148 ++++++ src/main/res/menu/verification_choices.xml | 12 + src/main/res/menu/verify_otr.xml | 15 + src/main/res/values/about.xml | 1 + src/main/res/values/strings.xml | 34 ++ 26 files changed, 1648 insertions(+), 10 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/crypto/OtrService.java create mode 100644 src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jid/OtrJidHelper.java create mode 100644 src/main/res/layout/activity_verify_otr.xml create mode 100644 src/main/res/menu/verification_choices.xml create mode 100644 src/main/res/menu/verify_otr.xml diff --git a/build.gradle b/build.gradle index c9c8eabb7..169129082 100644 --- a/build.gradle +++ b/build.gradle @@ -71,6 +71,7 @@ dependencies { implementation 'com.google.guava:guava:32.1.3-android' implementation 'io.michaelrocks:libphonenumber-android:8.13.17' implementation 'im.conversations.webrtc:webrtc-android:119.0.0' + implementation 'org.jitsi:org.otr4j:0.23' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation "androidx.recyclerview:recyclerview:1.2.1" diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index d848e4027..8c9a0b9b6 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -299,6 +299,11 @@ + bookmarks = new HashMap<>(); private Presence.Status presenceStatus; private String presenceStatusMessage; @@ -535,6 +544,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable } public void initAccountServices(final XmppConnectionService context) { + this.mOtrService = new OtrService(context, this); this.axolotlService = new AxolotlService(this, context); this.pgpDecryptionService = new PgpDecryptionService(context); if (xmppConnection != null) { @@ -542,6 +552,10 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable } } + public OtrService getOtrService() { + return this.mOtrService; + } + public PgpDecryptionService getPgpDecryptionService() { return this.pgpDecryptionService; } @@ -554,6 +568,27 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable this.xmppConnection = connection; } + public String getOtrFingerprint() { + if (this.otrFingerprint == null) { + try { + if (this.mOtrService == null) { + return null; + } + final PublicKey publicKey = this.mOtrService.getPublicKey(); + if (publicKey == null || !(publicKey instanceof DSAPublicKey)) { + return null; + } + this.otrFingerprint = new OtrCryptoEngineImpl().getFingerprint(publicKey).toLowerCase(Locale.US); + return this.otrFingerprint; + } catch (final OtrCryptoException ignored) { + return null; + } + } else { + return this.otrFingerprint; + } + } + + public String getRosterVersion() { if (this.rosterVersion == null) { return ""; @@ -721,6 +756,10 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable private List getFingerprints() { ArrayList fingerprints = new ArrayList<>(); + final String otr = this.getOtrFingerprint(); + if (otr != null) { + fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OTR, otr)); + } if (axolotlService == null) { return fingerprints; } diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index 6b0554240..53f0134bf 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -348,6 +348,47 @@ public class Contact implements ListItem, Blockable { return groups; } + public ArrayList getOtrFingerprints() { + synchronized (this.keys) { + final ArrayList fingerprints = new ArrayList(); + try { + if (this.keys.has("otr_fingerprints")) { + final JSONArray prints = this.keys.getJSONArray("otr_fingerprints"); + for (int i = 0; i < prints.length(); ++i) { + final String print = prints.isNull(i) ? null : prints.getString(i); + if (print != null && !print.isEmpty()) { + fingerprints.add(prints.getString(i).toLowerCase(Locale.US)); + } + } + } + } catch (final JSONException ignored) { + + } + return fingerprints; + } + } + + public boolean addOtrFingerprint(String print) { + synchronized (this.keys) { + if (getOtrFingerprints().contains(print)) { + return false; + } + try { + JSONArray fingerprints; + if (!this.keys.has("otr_fingerprints")) { + fingerprints = new JSONArray(); + } else { + fingerprints = this.keys.getJSONArray("otr_fingerprints"); + } + fingerprints.put(print); + this.keys.put("otr_fingerprints", fingerprints); + return true; + } catch (final JSONException ignored) { + return false; + } + } + } + public long getPgpKeyId() { synchronized (this.keys) { if (this.keys.has("pgp_keyid")) { diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index e3c147320..da60498d3 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -71,6 +71,7 @@ import org.json.JSONException; import org.json.JSONObject; import java.lang.ref.WeakReference; +import java.security.interfaces.DSAPublicKey; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; @@ -81,6 +82,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.ListIterator; +import java.util.Locale; import java.util.Objects; import java.util.Set; import java.util.Timer; @@ -132,6 +134,12 @@ import me.saket.bettermovementmethod.BetterLinkMovementMethod; import static eu.siacs.conversations.entities.Bookmark.printableValue; +import net.java.otr4j.OtrException; +import net.java.otr4j.crypto.OtrCryptoException; +import net.java.otr4j.session.SessionID; +import net.java.otr4j.session.SessionImpl; +import net.java.otr4j.session.SessionStatus; + public class Conversation extends AbstractEntity implements Blockable, Comparable, Conversational, AvatarService.Avatarable { public static final String TABLENAME = "conversations"; @@ -180,10 +188,15 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl private int mode; private JSONObject attributes; private Jid nextCounterpart; + private transient SessionImpl otrSession; + private transient String otrFingerprint = null; + private Smp mSmp = new Smp(); private transient MucOptions mucOptions = null; + private byte[] symmetricKey; private boolean messagesLeftOnServer = true; private ChatState mOutgoingChatState = Config.DEFAULT_CHAT_STATE; private ChatState mIncomingChatState = Config.DEFAULT_CHAT_STATE; + private String mLastReceivedOtrMessageId = null; private String mFirstMamReference = null; protected Message replyTo = null; protected int mCurrentTab = -1; @@ -490,6 +503,17 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } } + public void findUnsentMessagesWithEncryption(int encryptionType, OnMessageFound onMessageFound) { + synchronized (this.messages) { + for (Message message : this.messages) { + if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_WAITING) + && (message.getEncryption() == encryptionType)) { + onMessageFound.onMessageFound(message); + } + } + } + } + public void findUnsentTextMessages(OnMessageFound onMessageFound) { final ArrayList results = new ArrayList<>(); synchronized (this.messages) { @@ -662,6 +686,14 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl return getContact().getBlockedJid(); } + public String getLastReceivedOtrMessageId() { + return this.mLastReceivedOtrMessageId; + } + + public void setLastReceivedOtrMessageId(String id) { + this.mLastReceivedOtrMessageId = id; + } + public int countMessages() { synchronized (this.messages) { return this.messages.size(); @@ -923,6 +955,124 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl this.mode = mode; } + public SessionImpl startOtrSession(String presence, boolean sendStart) { + if (this.otrSession != null) { + return this.otrSession; + } else { + final SessionID sessionId = new SessionID(this.getJid().asBareJid().toString(), + presence, + "xmpp"); + this.otrSession = new SessionImpl(sessionId, getAccount().getOtrService()); + try { + if (sendStart) { + this.otrSession.startSession(); + return this.otrSession; + } + return this.otrSession; + } catch (OtrException e) { + return null; + } + } + + } + + public SessionImpl getOtrSession() { + return this.otrSession; + } + + public void resetOtrSession() { + this.otrFingerprint = null; + this.otrSession = null; + this.mSmp.hint = null; + this.mSmp.secret = null; + this.mSmp.status = Smp.STATUS_NONE; + } + + public Smp smp() { + return mSmp; + } + + public boolean startOtrIfNeeded() { + if (this.otrSession != null && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) { + try { + this.otrSession.startSession(); + return true; + } catch (OtrException e) { + this.resetOtrSession(); + return false; + } + } else { + return true; + } + } + + public boolean endOtrIfNeeded() { + if (this.otrSession != null) { + if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) { + try { + this.otrSession.endSession(); + this.resetOtrSession(); + return true; + } catch (OtrException e) { + this.resetOtrSession(); + return false; + } + } else { + this.resetOtrSession(); + return false; + } + } else { + return false; + } + } + + public boolean hasValidOtrSession() { + return this.otrSession != null; + } + + public synchronized String getOtrFingerprint() { + if (this.otrFingerprint == null) { + try { + if (getOtrSession() == null || getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) { + return null; + } + DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey(); + this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey).toLowerCase(Locale.US); + } catch (final OtrCryptoException ignored) { + return null; + } catch (final UnsupportedOperationException ignored) { + return null; + } + } + return this.otrFingerprint; + } + + public boolean verifyOtrFingerprint() { + final String fingerprint = getOtrFingerprint(); + if (fingerprint != null) { + getContact().addOtrFingerprint(fingerprint); + return true; + } else { + return false; + } + } + + public boolean isOtrFingerprintVerified() { + return getContact().getOtrFingerprints().contains(getOtrFingerprint()); + } + + public class Smp { + public static final int STATUS_NONE = 0; + public static final int STATUS_CONTACT_REQUESTED = 1; + public static final int STATUS_WE_REQUESTED = 2; + public static final int STATUS_FAILED = 3; + public static final int STATUS_VERIFIED = 4; + + public String secret = null; + public String hint = null; + public int status = 0; + } + /** * short for is Private and Non-anonymous */ @@ -964,7 +1114,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } public int getNextEncryption() { - if (!Config.supportOmemo() && !Config.supportOpenPgp()) { + if (!Config.supportOmemo() && !Config.supportOpenPgp() && !Config.supportOtr()) { return Message.ENCRYPTION_NONE; } if (OmemoSetting.isAlways()) { @@ -993,6 +1143,10 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl return nextMessage == null ? "" : nextMessage; } + public boolean smpRequested() { + return smp().status == Smp.STATUS_CONTACT_REQUESTED; + } + public @Nullable Draft getDraft() { long timestamp = getLongAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, 0); @@ -1015,6 +1169,14 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl return changed; } + public void setSymmetricKey(byte[] key) { + this.symmetricKey = key; + } + + public byte[] getSymmetricKey() { + return this.symmetricKey; + } + public Bookmark getBookmark() { return this.account.getBookmark(this.contactJid); } diff --git a/src/main/java/eu/siacs/conversations/entities/Transferable.java b/src/main/java/eu/siacs/conversations/entities/Transferable.java index 5c833f603..58297d26a 100644 --- a/src/main/java/eu/siacs/conversations/entities/Transferable.java +++ b/src/main/java/eu/siacs/conversations/entities/Transferable.java @@ -6,7 +6,7 @@ import java.util.List; public interface Transferable { List VALID_IMAGE_EXTENSIONS = Arrays.asList("webp", "jpeg", "jpg", "png", "jpe"); - List VALID_CRYPTO_EXTENSIONS = Arrays.asList("pgp", "gpg"); + List VALID_CRYPTO_EXTENSIONS = Arrays.asList("pgp", "gpg", "otr"); int STATUS_UNKNOWN = 0x200; int STATUS_CHECKING = 0x201; diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java index 706b50043..1088427fb 100644 --- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java @@ -57,6 +57,9 @@ public abstract class AbstractGenerator { private final String[] PRIVACY_SENSITIVE = { "urn:xmpp:time" //XEP-0202: Entity Time leaks time zone }; + private final String[] OTR = { + "urn:xmpp:otr:0" + }; private final String[] VOIP_NAMESPACES = { Namespace.JINGLE_TRANSPORT_ICE_UDP, Namespace.JINGLE_FEATURE_AUDIO, @@ -125,6 +128,9 @@ public abstract class AbstractGenerator { features.addAll(Arrays.asList(PRIVACY_SENSITIVE)); features.addAll(Arrays.asList(VOIP_NAMESPACES)); } + if (Config.supportOtr()) { + features.addAll(Arrays.asList(OTR)); + } if (mXmppConnectionService.broadcastLastActivity()) { features.add(Namespace.IDLE); } diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index 944e36d15..ddbb6d71c 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -1,5 +1,8 @@ package eu.siacs.conversations.generator; +import net.java.otr4j.OtrException; +import net.java.otr4j.session.Session; + import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; @@ -24,6 +27,7 @@ import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; public class MessageGenerator extends AbstractGenerator { + public static final String OTR_FALLBACK_MESSAGE = "I would like to start a private (OTR encrypted) conversation but your client doesn’t seem to support that"; private static final String OMEMO_FALLBACK_MESSAGE = "I sent you an OMEMO encrypted message but your client doesn’t seem to support that. Find more information on https://conversations.im/omemo"; private static final String PGP_FALLBACK_MESSAGE = "I sent you a PGP encrypted message but your client doesn’t seem to support that."; @@ -102,6 +106,36 @@ public class MessageGenerator extends AbstractGenerator { return packet; } + public static void addMessageHints(MessagePacket packet) { + packet.addChild("private", "urn:xmpp:carbons:2"); + packet.addChild("no-copy", "urn:xmpp:hints"); + packet.addChild("no-permanent-store", "urn:xmpp:hints"); + packet.addChild("no-permanent-storage", "urn:xmpp:hints"); //do not copy this. this is wrong. it is *store* + } + + public MessagePacket generateOtrChat(Message message) { + Conversation conversation = (Conversation) message.getConversation(); + Session otrSession = conversation.getOtrSession(); + if (otrSession == null) { + return null; + } + MessagePacket packet = preparePacket(message); + addMessageHints(packet); + try { + String content; + if (message.hasFileOnRemoteHost()) { + content = message.getFileParams().url.toString(); + } else { + content = message.getBody(); + } + packet.setBody(otrSession.transformSending(content)[0]); + packet.addChild("encryption", "urn:xmpp:eme:0").setAttribute("namespace", "urn:xmpp:otr:0"); + return packet; + } catch (OtrException e) { + return null; + } + } + public MessagePacket generateChat(Message message) { MessagePacket packet = preparePacket(message); String content; @@ -233,6 +267,19 @@ public class MessageGenerator extends AbstractGenerator { return packet; } + public MessagePacket generateOtrError(Jid to, String id, String errorText) { + MessagePacket packet = new MessagePacket(); + packet.setType(MessagePacket.TYPE_ERROR); + packet.setAttribute("id", id); + packet.setTo(to); + Element error = packet.addChild("error"); + error.setAttribute("code", "406"); + error.setAttribute("type", "modify"); + error.addChild("not-acceptable", "urn:ietf:params:xml:ns:xmpp-stanzas"); + error.addChild("text").setContent("?OTR Error:" + errorText); + return packet; + } + public MessagePacket sessionProposal(final JingleConnectionManager.RtpSessionProposal proposal) { final MessagePacket packet = new MessagePacket(); packet.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 2706a55e4..0c9dafcba 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -1,8 +1,13 @@ package eu.siacs.conversations.parser; +import android.os.Build; +import android.text.Html; import android.util.Log; import android.util.Pair; +import net.java.otr4j.session.Session; +import net.java.otr4j.session.SessionStatus; + import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; @@ -16,6 +21,7 @@ import java.util.UUID; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.OtrService; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.BrokenSessionException; import eu.siacs.conversations.crypto.axolotl.NotEncryptedForThisDeviceException; @@ -28,9 +34,11 @@ import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; +import eu.siacs.conversations.entities.Presence; import eu.siacs.conversations.entities.ReadByMarker; import eu.siacs.conversations.entities.ReceiptRequest; import eu.siacs.conversations.entities.RtpSessionStatus; +import eu.siacs.conversations.entities.ServiceDiscoveryResult; import eu.siacs.conversations.http.HttpConnectionManager; import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.QuickConversationsService; @@ -49,6 +57,7 @@ import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; public class MessageParser extends AbstractParser implements OnMessagePacketReceived { + private static final List CLIENTS_SENDING_HTML_IN_OTR = Arrays.asList("Pidgin", "Adium", "Trillian"); private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss", Locale.ENGLISH); @@ -95,6 +104,30 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece return result != null ? result : fallback; } + private static boolean clientMightSendHtml(Account account, Jid from) { + String resource = from.getResource(); + if (resource == null) { + return false; + } + Presence presence = account.getRoster().getContact(from).getPresences().getPresencesMap().get(resource); + ServiceDiscoveryResult disco = presence == null ? null : presence.getServiceDiscoveryResult(); + if (disco == null) { + return false; + } + return hasIdentityKnowForSendingHtml(disco.getIdentities()); + } + + private static boolean hasIdentityKnowForSendingHtml(List identities) { + for (ServiceDiscoveryResult.Identity identity : identities) { + if (identity.getName() != null) { + if (CLIENTS_SENDING_HTML_IN_OTR.contains(identity.getName())) { + return true; + } + } + } + return false; + } + private boolean extractChatState(Conversation c, final boolean isTypeGroupChat, final MessagePacket packet) { ChatState state = ChatState.parse(packet); if (state != null && c != null) { @@ -126,6 +159,66 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece return false; } + private Message parseOtrChat(String body, Jid from, String id, Conversation conversation) { + String presence; + if (from.isBareJid()) { + presence = ""; + } else { + presence = from.getResource(); + } + if (body.matches("^\\?OTRv\\d{1,2}\\?.*")) { + conversation.endOtrIfNeeded(); + } + if (!conversation.hasValidOtrSession()) { + conversation.startOtrSession(presence, false); + } else { + String foreignPresence = conversation.getOtrSession().getSessionID().getUserID(); + if (!foreignPresence.equals(presence)) { + conversation.endOtrIfNeeded(); + conversation.startOtrSession(presence, false); + } + } + try { + conversation.setLastReceivedOtrMessageId(id); + Session otrSession = conversation.getOtrSession(); + body = otrSession.transformReceiving(body); + SessionStatus status = otrSession.getSessionStatus(); + if (body == null && status == SessionStatus.ENCRYPTED) { + mXmppConnectionService.onOtrSessionEstablished(conversation); + return null; + } else if (body == null && status == SessionStatus.FINISHED) { + conversation.resetOtrSession(); + mXmppConnectionService.updateConversationUi(); + return null; + } else if (body == null || (body.isEmpty())) { + return null; + } + if (body.startsWith(CryptoHelper.FILETRANSFER)) { + String key = body.substring(CryptoHelper.FILETRANSFER.length()); + conversation.setSymmetricKey(CryptoHelper.hexToBytes(key)); + return null; + } + if (clientMightSendHtml(conversation.getAccount(), from)) { + Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": received OTR message from bad behaving client. escaping HTML…"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + body = Html.fromHtml(body, Html.FROM_HTML_MODE_LEGACY).toString(); + } else { + body = Html.fromHtml(body).toString(); + } + } + + final OtrService otrService = conversation.getAccount().getOtrService(); + Message finishedMessage = new Message(conversation, body, Message.ENCRYPTION_OTR, Message.STATUS_RECEIVED); + finishedMessage.setFingerprint(otrService.getFingerprint(otrSession.getRemotePublicKey())); + conversation.setLastReceivedOtrMessageId(null); + + return finishedMessage; + } catch (Exception e) { + conversation.resetOtrSession(); + return null; + } + } + private Message parseAxolotlChat(Element axolotlMessage, Jid from, Conversation conversation, int status, final boolean checkedForDuplicates, boolean postpone) { final AxolotlService service = conversation.getAccount().getAxolotlService(); final XmppAxolotlMessage xmppAxolotlMessage; @@ -327,6 +420,11 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece final Jid from = packet.getFrom(); final String id = packet.getId(); if (from != null && id != null) { + final Message message = mXmppConnectionService.markMessage(account, + from.asBareJid(), + packet.getId(), + Message.STATUS_SEND_FAILED, + extractErrorMessage(packet)); if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) { final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length()); mXmppConnectionService.getJingleConnectionManager() @@ -335,8 +433,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROCEED_ID_PREFIX)) { final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROCEED_ID_PREFIX.length()); - final String message = extractErrorMessage(packet); - mXmppConnectionService.getJingleConnectionManager().failProceed(account, from, sessionId, message); + final String errorMessage = extractErrorMessage(packet); + mXmppConnectionService.getJingleConnectionManager().failProceed(account, from, sessionId, errorMessage); return true; } mXmppConnectionService.markMessage(account, @@ -355,6 +453,13 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } } } + + if (message != null) { + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + Conversation conversation = (Conversation) message.getConversation(); + conversation.endOtrIfNeeded(); + } + } } return true; } @@ -368,6 +473,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } final MessagePacket packet; Long timestamp = null; + final boolean isForwarded; boolean isCarbon = false; String serverMsgId = null; final Element fin = original.findChild("fin", MessageArchiveService.Version.MAM_0.namespace); @@ -385,7 +491,9 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } timestamp = f.second; packet = f.first; + isForwarded = true; serverMsgId = result.getAttribute("id"); + query.incrementMessageCount(); if (handleErrorMessage(account, packet)) { return; @@ -403,8 +511,10 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } timestamp = f != null ? f.second : null; isCarbon = f != null; + isForwarded = isCarbon; } else { packet = original; + isForwarded = false; } if (timestamp == null) { @@ -449,6 +559,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": received groupchat (" + from + ") message on regular MAM request. skipping"); return; } + boolean isProperlyAddressed = (to != null) && (!to.isBareJid() || account.countPresences() == 0); boolean isMucStatusMessage = InvalidJid.hasValidFrom(packet) && from.isBareJid() && mucUserElement != null && mucUserElement.hasChild("status"); boolean selfAddressed; if (packet.fromAccount(account)) { @@ -547,7 +658,20 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } } final Message message; - if (pgpEncrypted != null && Config.supportOpenPgp()) { + if (body != null && body.content.startsWith("?OTR") && Config.supportOtr()) { + if (!isForwarded && !isTypeGroupChat && isProperlyAddressed && !conversationMultiMode) { + message = parseOtrChat(body.content, from, remoteMsgId, conversation); + if (message == null) { + return; + } + } else { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring OTR message from " + from + " isForwarded=" + Boolean.toString(isForwarded) + ", isProperlyAddressed=" + Boolean.valueOf(isProperlyAddressed)); + message = new Message(conversation, body.content, Message.ENCRYPTION_NONE, status); + if (body.count > 1) { + message.setBodyLanguage(body.language); + } + } + } else if (pgpEncrypted != null && Config.supportOpenPgp()) { message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status); } else if (axolotlEncrypted != null && Config.supportOmemo()) { Jid origin; @@ -796,6 +920,13 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece processMessageReceipts(account, packet, remoteMsgId, query); } + if (message.getStatus() == Message.STATUS_RECEIVED + && conversation.getOtrSession() != null + && !conversation.getOtrSession().getSessionID().getUserID() + .equals(message.getCounterpart().getResource())) { + conversation.endOtrIfNeeded(); + } + mXmppConnectionService.databaseBackend.createMessage(message); final HttpConnectionManager manager = this.mXmppConnectionService.getHttpConnectionManager(); if (message.trusted() && message.treatAsDownloadable() && manager.getAutoAcceptFileSize() > 0) { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 0c16df6af..bf3f07a35 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -61,6 +61,11 @@ import com.google.common.base.Objects; import com.google.common.base.Optional; import com.google.common.base.Strings; +import net.java.otr4j.session.Session; +import net.java.otr4j.session.SessionID; +import net.java.otr4j.session.SessionImpl; +import net.java.otr4j.session.SessionStatus; + import org.conscrypt.Conscrypt; import org.jxmpp.stringprep.libidn.LibIdnXmppStringprep; import org.openintents.openpgp.IOpenPgpService2; @@ -171,6 +176,7 @@ import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.forms.Data; +import eu.siacs.conversations.xmpp.jid.OtrJidHelper; import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; @@ -255,9 +261,18 @@ public class XmppConnectionService extends Service { Conversation conversation = find(getConversations(), contact); if (conversation != null) { if (online) { + conversation.endOtrIfNeeded(); if (contact.getPresences().size() == 1) { sendUnsentMessages(conversation); } + } else { + //check if the resource we are haveing a conversation with is still online + if (conversation.hasValidOtrSession()) { + String otrResource = conversation.getOtrSession().getSessionID().getUserID(); + if (!(Arrays.asList(contact.getPresences().toResourceArray()).contains(otrResource))) { + conversation.endOtrIfNeeded(); + } + } } } }; @@ -447,6 +462,9 @@ public class XmppConnectionService extends Service { if (conversation.getAccount() == account && !pendingJoin && !inProgressJoin) { + if (!conversation.startOtrIfNeeded()) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": couldn't start OTR with " + conversation.getContact().getJid() + " when needed"); + } sendUnsentMessages(conversation); } } @@ -1704,6 +1722,12 @@ public class XmppConnectionService extends Service { } } + if (!resend && message.getEncryption() != Message.ENCRYPTION_OTR) { + conversation.endOtrIfNeeded(); + conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR, + message1 -> markMessage(message1, Message.STATUS_SEND_FAILED)); + } + final boolean inProgressJoin = isJoinInProgress(conversation); @@ -1736,6 +1760,30 @@ public class XmppConnectionService extends Service { packet = mMessageGenerator.generatePgpChat(message); } break; + case Message.ENCRYPTION_OTR: + SessionImpl otrSession = conversation.getOtrSession(); + if (otrSession != null && otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) { + try { + message.setCounterpart(OtrJidHelper.fromSessionID(otrSession.getSessionID())); + } catch (IllegalArgumentException e) { + break; + } + if (message.needsUploading()) { + mJingleConnectionManager.startJingleFileTransfer(message); + } else { + packet = mMessageGenerator.generateOtrChat(message); + } + } else if (otrSession == null) { + if (message.fixCounterpart()) { + conversation.startOtrSession(message.getCounterpart().getResource(), true); + } else { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not fix counterpart for OTR message to contact " + message.getCounterpart()); + break; + } + } else { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + " OTR session with " + message.getContact() + " is in wrong state: " + otrSession.getSessionStatus().toString()); + } + break; case Message.ENCRYPTION_AXOLOTL: message.setFingerprint(account.getAxolotlService().getOwnFingerprint()); if (message.needsUploading()) { @@ -1789,6 +1837,12 @@ public class XmppConnectionService extends Service { } } break; + case Message.ENCRYPTION_OTR: + if (!conversation.hasValidOtrSession() && message.getCounterpart() != null) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": create otr session without starting for " + message.getContact().getJid()); + conversation.startOtrSession(message.getCounterpart().getResource(), false); + } + break; case Message.ENCRYPTION_AXOLOTL: message.setFingerprint(account.getAxolotlService().getOwnFingerprint()); break; @@ -3922,6 +3976,12 @@ public class XmppConnectionService extends Service { if (conversation.getAccount() == account) { if (conversation.getMode() == Conversation.MODE_MULTI) { leaveMuc(conversation, true); + } else { + if (conversation.endOtrIfNeeded()) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + + ": ended otr session with " + + conversation.getJid()); + } } } } @@ -3978,6 +4038,39 @@ public class XmppConnectionService extends Service { pushContactToServer(contact, preAuth); } + public void onOtrSessionEstablished(Conversation conversation) { + final Account account = conversation.getAccount(); + final Session otrSession = conversation.getOtrSession(); + Log.d(Config.LOGTAG, + account.getJid().asBareJid() + " otr session established with " + + conversation.getJid() + "/" + + otrSession.getSessionID().getUserID()); + conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR, new Conversation.OnMessageFound() { + + @Override + public void onMessageFound(Message message) { + SessionID id = otrSession.getSessionID(); + try { + message.setCounterpart(Jid.of(id.getAccountID() + "/" + id.getUserID())); + } catch (IllegalArgumentException e) { + return; + } + if (message.needsUploading()) { + mJingleConnectionManager.startJingleFileTransfer(message); + } else { + MessagePacket outPacket = mMessageGenerator.generateOtrChat(message); + if (outPacket != null) { + mMessageGenerator.addDelay(outPacket, message.getTimeSent()); + message.setStatus(Message.STATUS_SEND); + databaseBackend.updateMessage(message, false); + sendMessagePacket(account, outPacket); + } + } + updateConversationUi(); + } + }); + } + public void pushContactToServer(final Contact contact) { pushContactToServer(contact, null); } @@ -4503,6 +4596,7 @@ public class XmppConnectionService extends Service { return false; } else { final Message message = conversation.findSentMessageWithUuid(uuid); + if (message != null) { if (message.getServerMsgId() == null) { message.setServerMsgId(serverMessageId); @@ -4805,6 +4899,11 @@ public class XmppConnectionService extends Service { setMemorizingTrustManager(tm); } + public void syncRosterToDisk(final Account account) { + Runnable runnable = () -> databaseBackend.writeRoster(account.getRoster()); + mDatabaseWriterExecutor.execute(runnable); + } + public LruCache getBitmapCache() { return this.mBitmapCache; } @@ -5272,10 +5371,14 @@ public class XmppConnectionService extends Service { } public boolean verifyFingerprints(Contact contact, List fingerprints) { + boolean needsRosterWrite = false; boolean performedVerification = false; final AxolotlService axolotlService = contact.getAccount().getAxolotlService(); for (XmppUri.Fingerprint fp : fingerprints) { - if (fp.type == XmppUri.FingerprintType.OMEMO) { + if (fp.type == XmppUri.FingerprintType.OTR) { + performedVerification |= contact.addOtrFingerprint(fp.fingerprint); + needsRosterWrite |= performedVerification; + } else if (fp.type == XmppUri.FingerprintType.OMEMO) { String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", ""); FingerprintStatus fingerprintStatus = axolotlService.getFingerprintTrust(fingerprint); if (fingerprintStatus != null) { @@ -5288,6 +5391,11 @@ public class XmppConnectionService extends Service { } } } + + if (needsRosterWrite) { + syncRosterToDisk(contact.getAccount()); + } + return performedVerification; } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 1b6eb44a4..50c870489 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -79,6 +79,8 @@ import androidx.viewpager.widget.PagerAdapter; import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; +import net.java.otr4j.session.SessionStatus; + import org.jetbrains.annotations.NotNull; import java.io.File; @@ -223,6 +225,14 @@ public class ConversationFragment extends XmppFragment private ConversationsActivity activity; private Vibrator vibrator; private boolean reInitRequiredOnStart = true; + + protected OnClickListener clickToVerify = new OnClickListener() { + @Override + public void onClick(View v) { + activity.verifyOtrSessionDialog(conversation, v); + } + }; + @ColorInt private int primaryColor = -1; @@ -534,6 +544,20 @@ public class ConversationFragment extends XmppFragment } } }; + + private OnClickListener mAnswerSmpClickListener = new OnClickListener() { + @Override + public void onClick(View view) { + Intent intent = new Intent(activity, VerifyOTRActivity.class); + intent.setAction(VerifyOTRActivity.ACTION_VERIFY_CONTACT); + intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toString()); + intent.putExtra(VerifyOTRActivity.EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toString()); + intent.putExtra("mode", VerifyOTRActivity.MODE_ANSWER_QUESTION); + startActivity(intent); + activity.overridePendingTransition(R.animator.fade_in, R.animator.fade_out); + } + }; + protected OnClickListener clickToDecryptListener = new OnClickListener() { @@ -1064,6 +1088,8 @@ public class ConversationFragment extends XmppFragment message.setUuid(UUID.randomUUID().toString()); } switch (conversation.getNextEncryption()) { + case Message.ENCRYPTION_OTR: + sendOtrMessage(message); case Message.ENCRYPTION_PGP: sendPgpMessage(message); break; @@ -3550,6 +3576,14 @@ public class ConversationFragment extends XmppFragment } } else if (account.hasPendingPgpIntent(conversation)) { showSnackbar(R.string.openpgp_messages_found, R.string.decrypt, clickToDecryptListener); + } else if (mode == Conversation.MODE_SINGLE + && conversation.smpRequested()) { + showSnackbar(R.string.smp_requested, R.string.verify, this.mAnswerSmpClickListener); + } else if (mode == Conversation.MODE_SINGLE + && conversation.hasValidOtrSession() + && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) + && (!conversation.isOtrFingerprintVerified())) { + showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify, clickToVerify); } else if (connection != null && connection.getFeatures().blocking() && conversation.countMessages() != 0 @@ -3650,6 +3684,11 @@ public class ConversationFragment extends XmppFragment new Handler() .post( () -> { + if (conversation.isInHistoryPart()) { + conversation.jumpToLatest(); + refresh(false); + } + int size = messageList.size(); this.binding.messagesView.setSelection(size - 1); }); @@ -3937,6 +3976,17 @@ public class ConversationFragment extends XmppFragment messageSent(); } + protected void sendOtrMessage(final Message message) { + final ConversationsActivity activity = (ConversationsActivity) getActivity(); + final XmppConnectionService xmppService = activity.xmppConnectionService; + activity.selectPresence(conversation, + () -> { + message.setCounterpart(conversation.getNextCounterpart()); + xmppService.sendMessage(message); + messageSent(); + }); + } + protected void sendPgpMessage(final Message message) { final XmppConnectionService xmppService = activity.xmppConnectionService; final Contact contact = message.getConversation().getContact(); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java index 79c70d46b..e98fb8f4e 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java @@ -63,9 +63,12 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.PopupMenu; import androidx.core.app.ActivityCompat; import androidx.databinding.DataBindingUtil; +import net.java.otr4j.session.SessionStatus; + import com.google.android.material.bottomnavigation.BottomNavigationView; import com.google.android.material.navigation.NavigationBarView; @@ -105,6 +108,7 @@ import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import io.michaelrocks.libphonenumber.android.NumberParseException; +import me.drakeet.support.toast.ToastCompat; public class ConversationsActivity extends XmppActivity implements OnConversationSelected, OnConversationArchived, OnConversationsListItemUpdated, OnConversationRead, XmppConnectionService.OnAccountUpdate, XmppConnectionService.OnConversationUpdate, XmppConnectionService.OnRosterUpdate, OnUpdateBlocklist, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnAffiliationChanged { @@ -812,6 +816,33 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio } } + public void verifyOtrSessionDialog(final Conversation conversation, View view) { + if (!conversation.hasValidOtrSession() || conversation.getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) { + ToastCompat.makeText(this, R.string.otr_session_not_started, Toast.LENGTH_LONG).show(); + return; + } + if (view == null) { + return; + } + PopupMenu popup = new PopupMenu(this, view); + popup.inflate(R.menu.verification_choices); + popup.setOnMenuItemClickListener(menuItem -> { + Intent intent = new Intent(ConversationsActivity.this, VerifyOTRActivity.class); + intent.setAction(VerifyOTRActivity.ACTION_VERIFY_CONTACT); + intent.putExtra("contact", conversation.getContact().getJid().asBareJid().toString()); + intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toString()); + switch (menuItem.getItemId()) { + case R.id.ask_question: + intent.putExtra("mode", VerifyOTRActivity.MODE_ASK_QUESTION); + break; + } + startActivity(intent); + overridePendingTransition(R.animator.fade_in, R.animator.fade_out); + return true; + }); + popup.show(); + } + @Override public void onConversationArchived(Conversation conversation) { if (performRedirectIfNecessary(conversation, false)) { diff --git a/src/main/java/eu/siacs/conversations/ui/SendLogActivity.java b/src/main/java/eu/siacs/conversations/ui/SendLogActivity.java index 8dabc77cd..ec537d703 100644 --- a/src/main/java/eu/siacs/conversations/ui/SendLogActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/SendLogActivity.java @@ -265,7 +265,6 @@ public class SendLogActivity extends ActionBarActivity { log.insert(0, mAdditonalInfo); } - android.util.Log.e("35fd", log.toString()); writer.write(log.toString()); } catch (IOException e){ diff --git a/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java b/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java new file mode 100644 index 000000000..2eb69e5e2 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java @@ -0,0 +1,450 @@ +package eu.siacs.conversations.ui; + +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.view.Menu; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AlertDialog; + +import net.java.otr4j.OtrException; +import net.java.otr4j.session.Session; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.utils.XmppUri; +import eu.siacs.conversations.xmpp.Jid; +import me.drakeet.support.toast.ToastCompat; + +public class VerifyOTRActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate { + + public static final String ACTION_VERIFY_CONTACT = "verify_contact"; + public static final int MODE_SCAN_FINGERPRINT = -0x0502; + public static final int MODE_ASK_QUESTION = 0x0503; + public static final int MODE_ANSWER_QUESTION = 0x0504; + public static final int MODE_MANUAL_VERIFICATION = 0x0505; + + private LinearLayout mManualVerificationArea; + private LinearLayout mSmpVerificationArea; + private TextView mRemoteFingerprint; + private TextView mYourFingerprint; + private TextView mVerificationExplain; + private TextView mStatusMessage; + private TextView mSharedSecretHint; + private EditText mSharedSecretHintEditable; + private EditText mSharedSecretSecret; + private Button mLeftButton; + private Button mRightButton; + private Account mAccount; + private Conversation mConversation; + private int mode = MODE_MANUAL_VERIFICATION; + private XmppUri mPendingUri = null; + + private DialogInterface.OnClickListener mVerifyFingerprintListener = new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialogInterface, int click) { + mConversation.verifyOtrFingerprint(); + xmppConnectionService.syncRosterToDisk(mConversation.getAccount()); + ToastCompat.makeText(VerifyOTRActivity.this, R.string.verified, Toast.LENGTH_SHORT).show(); + finish(); + } + }; + + private View.OnClickListener mCreateSharedSecretListener = new View.OnClickListener() { + @Override + public void onClick(final View view) { + if (isAccountOnline()) { + final String question = mSharedSecretHintEditable.getText().toString(); + final String secret = mSharedSecretSecret.getText().toString(); + if (question.trim().isEmpty()) { + mSharedSecretHintEditable.requestFocus(); + mSharedSecretHintEditable.setError(getString(R.string.shared_secret_hint_should_not_be_empty)); + } else if (secret.trim().isEmpty()) { + mSharedSecretSecret.requestFocus(); + mSharedSecretSecret.setError(getString(R.string.shared_secret_can_not_be_empty)); + } else { + mSharedSecretSecret.setError(null); + mSharedSecretHintEditable.setError(null); + initSmp(question, secret); + updateView(); + } + } + } + }; + private View.OnClickListener mCancelSharedSecretListener = new View.OnClickListener() { + @Override + public void onClick(View view) { + if (isAccountOnline()) { + abortSmp(); + updateView(); + } + } + }; + private View.OnClickListener mRespondSharedSecretListener = new View.OnClickListener() { + + @Override + public void onClick(View view) { + if (isAccountOnline()) { + final String question = mSharedSecretHintEditable.getText().toString(); + final String secret = mSharedSecretSecret.getText().toString(); + respondSmp(question, secret); + updateView(); + } + } + }; + private View.OnClickListener mRetrySharedSecretListener = new View.OnClickListener() { + @Override + public void onClick(View view) { + mConversation.smp().status = Conversation.Smp.STATUS_NONE; + mConversation.smp().hint = null; + mConversation.smp().secret = null; + updateView(); + } + }; + private View.OnClickListener mFinishListener = new View.OnClickListener() { + @Override + public void onClick(View view) { + mConversation.smp().status = Conversation.Smp.STATUS_NONE; + finish(); + } + }; + + protected boolean initSmp(final String question, final String secret) { + final Session session = mConversation.getOtrSession(); + if (session != null) { + try { + session.initSmp(question, secret); + mConversation.smp().status = Conversation.Smp.STATUS_WE_REQUESTED; + mConversation.smp().secret = secret; + mConversation.smp().hint = question; + return true; + } catch (OtrException e) { + return false; + } + } else { + return false; + } + } + + protected boolean abortSmp() { + final Session session = mConversation.getOtrSession(); + if (session != null) { + try { + session.abortSmp(); + mConversation.smp().status = Conversation.Smp.STATUS_NONE; + mConversation.smp().hint = null; + mConversation.smp().secret = null; + return true; + } catch (OtrException e) { + return false; + } + } else { + return false; + } + } + + protected boolean respondSmp(final String question, final String secret) { + final Session session = mConversation.getOtrSession(); + if (session != null) { + try { + session.respondSmp(question, secret); + return true; + } catch (OtrException e) { + return false; + } + } else { + return false; + } + } + + protected boolean verifyWithUri(XmppUri uri) { + Contact contact = mConversation.getContact(); + if (this.mConversation.getContact().getJid().equals(uri.getJid()) && uri.hasFingerprints()) { + xmppConnectionService.verifyFingerprints(contact, uri.getFingerprints()); + ToastCompat.makeText(this, R.string.verified, Toast.LENGTH_SHORT).show(); + updateView(); + return true; + } else { + ToastCompat.makeText(this, R.string.could_not_verify_fingerprint, Toast.LENGTH_SHORT).show(); + return false; + } + } + + protected boolean isAccountOnline() { + if (this.mAccount.getStatus() != Account.State.ONLINE) { + ToastCompat.makeText(this, R.string.not_connected_try_again, Toast.LENGTH_SHORT).show(); + return false; + } else { + return true; + } + } + + protected boolean handleIntent(Intent intent) { + if (intent != null && intent.getAction().equals(ACTION_VERIFY_CONTACT)) { + this.mAccount = extractAccount(intent); + if (this.mAccount == null) { + return false; + } + try { + this.mConversation = this.xmppConnectionService.find(this.mAccount, Jid.of(intent.getExtras().getString("contact")), null); + if (this.mConversation == null) { + return false; + } + } catch (final IllegalArgumentException ignored) { + ignored.printStackTrace(); + return false; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + this.mode = intent.getIntExtra("mode", MODE_MANUAL_VERIFICATION); + // todo scan OTR fingerprint + if (this.mode == MODE_SCAN_FINGERPRINT) { + Log.d(Config.LOGTAG, "Scan OTR fingerprint is not implemented in this version"); + //new IntentIntegrator(this).initiateScan(); + return false; + } + return true; + } else { + return false; + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent intent) { + // todo onActivityResult for OTR scan + Log.d(Config.LOGTAG, "Scan OTR fingerprint result is not implemented in this version"); + /*if ((requestCode & 0xFFFF) == IntentIntegrator.REQUEST_CODE) { + IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); + if (scanResult != null && scanResult.getFormatName() != null) { + String data = scanResult.getContents(); + XmppUri uri = new XmppUri(data); + if (xmppConnectionServiceBound) { + verifyWithUri(uri); + finish(); + } else { + this.mPendingUri = uri; + } + } else { + finish(); + } + }*/ + super.onActivityResult(requestCode, requestCode, intent); + } + + @Override + protected void onBackendConnected() { + if (handleIntent(getIntent())) { + updateView(); + } else if (mPendingUri != null) { + verifyWithUri(mPendingUri); + finish(); + mPendingUri = null; + } + setIntent(null); + } + + protected void updateView() { + if (this.mConversation != null && this.mConversation.hasValidOtrSession()) { + final ActionBar actionBar = getSupportActionBar(); + this.mVerificationExplain.setText(R.string.no_otr_session_found); + invalidateOptionsMenu(); + switch (this.mode) { + case MODE_ASK_QUESTION: + if (actionBar != null) { + actionBar.setTitle(R.string.ask_question); + } + this.updateViewAskQuestion(); + break; + case MODE_ANSWER_QUESTION: + if (actionBar != null) { + actionBar.setTitle(R.string.smp_requested); + } + this.updateViewAnswerQuestion(); + break; + case MODE_MANUAL_VERIFICATION: + default: + if (actionBar != null) { + actionBar.setTitle(R.string.manually_verify); + } + this.updateViewManualVerification(); + break; + } + } else { + this.mManualVerificationArea.setVisibility(View.GONE); + this.mSmpVerificationArea.setVisibility(View.GONE); + } + } + + protected void updateViewManualVerification() { + this.mVerificationExplain.setText(R.string.manual_verification_explanation); + this.mManualVerificationArea.setVisibility(View.VISIBLE); + this.mSmpVerificationArea.setVisibility(View.GONE); + this.mYourFingerprint.setText(CryptoHelper.prettifyFingerprint(this.mAccount.getOtrFingerprint())); + this.mRemoteFingerprint.setText(CryptoHelper.prettifyFingerprint(this.mConversation.getOtrFingerprint())); + if (this.mConversation.isOtrFingerprintVerified()) { + deactivateButton(this.mRightButton, R.string.verified); + activateButton(this.mLeftButton, R.string.cancel, this.mFinishListener); + } else { + activateButton(this.mLeftButton, R.string.cancel, this.mFinishListener); + activateButton(this.mRightButton, R.string.verify, new View.OnClickListener() { + @Override + public void onClick(View view) { + showManuallyVerifyDialog(); + } + }); + } + } + + protected void updateViewAskQuestion() { + this.mManualVerificationArea.setVisibility(View.GONE); + this.mSmpVerificationArea.setVisibility(View.VISIBLE); + this.mVerificationExplain.setText(R.string.smp_explain_question); + final int smpStatus = this.mConversation.smp().status; + switch (smpStatus) { + case Conversation.Smp.STATUS_WE_REQUESTED: + this.mStatusMessage.setVisibility(View.GONE); + this.mSharedSecretHintEditable.setVisibility(View.VISIBLE); + this.mSharedSecretSecret.setVisibility(View.VISIBLE); + this.mSharedSecretHintEditable.setText(this.mConversation.smp().hint); + this.mSharedSecretSecret.setText(this.mConversation.smp().secret); + this.activateButton(this.mLeftButton, R.string.cancel, this.mCancelSharedSecretListener); + this.deactivateButton(this.mRightButton, R.string.in_progress); + break; + case Conversation.Smp.STATUS_FAILED: + this.mStatusMessage.setVisibility(View.GONE); + this.mSharedSecretHintEditable.setVisibility(View.VISIBLE); + this.mSharedSecretSecret.setVisibility(View.VISIBLE); + this.mSharedSecretSecret.requestFocus(); + this.mSharedSecretSecret.setError(getString(R.string.secrets_do_not_match)); + this.deactivateButton(this.mLeftButton, R.string.cancel); + this.activateButton(this.mRightButton, R.string.try_again, this.mRetrySharedSecretListener); + break; + case Conversation.Smp.STATUS_VERIFIED: + this.mSharedSecretHintEditable.setText(""); + this.mSharedSecretHintEditable.setVisibility(View.GONE); + this.mSharedSecretSecret.setText(""); + this.mSharedSecretSecret.setVisibility(View.GONE); + this.mStatusMessage.setVisibility(View.VISIBLE); + this.deactivateButton(this.mLeftButton, R.string.cancel); + this.activateButton(this.mRightButton, R.string.finish, this.mFinishListener); + break; + default: + this.mStatusMessage.setVisibility(View.GONE); + this.mSharedSecretHintEditable.setVisibility(View.VISIBLE); + this.mSharedSecretSecret.setVisibility(View.VISIBLE); + this.activateButton(this.mLeftButton, R.string.cancel, this.mFinishListener); + this.activateButton(this.mRightButton, R.string.ask_question, this.mCreateSharedSecretListener); + break; + } + } + + protected void updateViewAnswerQuestion() { + this.mManualVerificationArea.setVisibility(View.GONE); + this.mSmpVerificationArea.setVisibility(View.VISIBLE); + this.mVerificationExplain.setText(R.string.smp_explain_answer); + this.mSharedSecretHintEditable.setVisibility(View.GONE); + this.mSharedSecretHint.setVisibility(View.VISIBLE); + this.deactivateButton(this.mLeftButton, R.string.cancel); + final int smpStatus = this.mConversation.smp().status; + switch (smpStatus) { + case Conversation.Smp.STATUS_CONTACT_REQUESTED: + this.mStatusMessage.setVisibility(View.GONE); + this.mSharedSecretHint.setText(this.mConversation.smp().hint); + this.activateButton(this.mRightButton, R.string.respond, this.mRespondSharedSecretListener); + break; + case Conversation.Smp.STATUS_VERIFIED: + this.mSharedSecretHintEditable.setText(""); + this.mSharedSecretHintEditable.setVisibility(View.GONE); + this.mSharedSecretHint.setVisibility(View.GONE); + this.mSharedSecretSecret.setText(""); + this.mSharedSecretSecret.setVisibility(View.GONE); + this.mStatusMessage.setVisibility(View.VISIBLE); + this.activateButton(this.mRightButton, R.string.finish, this.mFinishListener); + break; + case Conversation.Smp.STATUS_FAILED: + default: + this.mSharedSecretSecret.requestFocus(); + this.mSharedSecretSecret.setError(getString(R.string.secrets_do_not_match)); + this.activateButton(this.mRightButton, R.string.finish, this.mFinishListener); + break; + } + } + + protected void activateButton(Button button, int text, View.OnClickListener listener) { + button.setEnabled(true); + button.setText(text); + button.setOnClickListener(listener); + } + + protected void deactivateButton(Button button, int text) { + button.setEnabled(false); + button.setText(text); + button.setOnClickListener(null); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_verify_otr); + this.mRemoteFingerprint = findViewById(R.id.remote_fingerprint); + this.mYourFingerprint = findViewById(R.id.your_fingerprint); + this.mLeftButton = findViewById(R.id.left_button); + this.mRightButton = findViewById(R.id.right_button); + this.mVerificationExplain = findViewById(R.id.verification_explanation); + this.mStatusMessage = findViewById(R.id.status_message); + this.mSharedSecretSecret = findViewById(R.id.shared_secret_secret); + this.mSharedSecretHintEditable = findViewById(R.id.shared_secret_hint_editable); + this.mSharedSecretHint = findViewById(R.id.shared_secret_hint); + this.mManualVerificationArea = findViewById(R.id.manual_verification_area); + this.mSmpVerificationArea = findViewById(R.id.smp_verification_area); + } + + @Override + public boolean onCreateOptionsMenu(final Menu menu) { + super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.verify_otr, menu); + return true; + } + + private void showManuallyVerifyDialog() { + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.manually_verify); + builder.setMessage(R.string.are_you_sure_verify_fingerprint); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.verify, mVerifyFingerprintListener); + builder.create().show(); + } + + @Override + protected String getShareableUri() { + if (mAccount != null) { + return mAccount.getShareableUri(); + } else { + return ""; + } + } + + public void onConversationUpdate() { + refreshUi(); + } + + @Override + protected void refreshUiReal() { + updateView(); + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 47ce3be01..3e1f878a9 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -56,6 +56,8 @@ import androidx.databinding.DataBindingUtil; import com.google.common.base.Strings; +import net.java.otr4j.session.SessionID; + import java.io.IOException; import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -446,7 +448,18 @@ public abstract class XmppActivity extends ActionBarActivity { public void selectPresence(final Conversation conversation, final PresenceSelector.OnPresenceSelected listener) { final Contact contact = conversation.getContact(); - if (contact.showInRoster() || contact.isSelf()) { + + if (conversation.hasValidOtrSession()) { + SessionID id = conversation.getOtrSession().getSessionID(); + Jid jid; + try { + jid = Jid.of(id.getAccountID() + "/" + id.getUserID()); + } catch (IllegalArgumentException e) { + jid = null; + } + conversation.setNextCounterpart(jid); + listener.onPresenceSelected(); + } else if (contact.showInRoster() || contact.isSelf()) { final Presences presences = contact.getPresences(); if (presences.size() == 0) { if (contact.isSelf()) { diff --git a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java index 85ea2bb86..59399fbca 100644 --- a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java @@ -41,6 +41,7 @@ public final class CryptoHelper { private static final int PW_LENGTH = 12; private static final char[] VOWELS = "aeiou".toCharArray(); private static final char[] CONSONANTS = "bcfghjklmnpqrstvwxyz".toCharArray(); + public static final String FILETRANSFER = "?FILETRANSFERv1:"; private final static char[] hexArray = "0123456789abcdef".toCharArray(); public static String bytesToHex(byte[] bytes) { diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java index f684c5f46..d1b863082 100644 --- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java @@ -609,6 +609,8 @@ public class UIHelper { } else { return context.getString(R.string.send_message_to_x, conversation.getName()); } + case Message.ENCRYPTION_OTR: + return context.getString(R.string.send_otr_message); case Message.ENCRYPTION_AXOLOTL: AxolotlService axolotlService = conversation.getAccount().getAxolotlService(); if (axolotlService != null && axolotlService.trustedSessionVerified(conversation)) { diff --git a/src/main/java/eu/siacs/conversations/utils/XmppUri.java b/src/main/java/eu/siacs/conversations/utils/XmppUri.java index 6c3075be9..6f7b8481c 100644 --- a/src/main/java/eu/siacs/conversations/utils/XmppUri.java +++ b/src/main/java/eu/siacs/conversations/utils/XmppUri.java @@ -29,6 +29,7 @@ public class XmppUri { public static final String PARAMETER_PRE_AUTH = "preauth"; public static final String PARAMETER_IBR = "ibr"; private static final String OMEMO_URI_PARAM = "omemo-sid-"; + private static final String OTR_URI_PARAM = "otr-fingerprint"; protected Uri uri; protected String jid; private List fingerprints = new ArrayList<>(); @@ -111,6 +112,8 @@ public class XmppUri { if (type == XmppUri.FingerprintType.OMEMO) { builder.append(XmppUri.OMEMO_URI_PARAM); builder.append(fingerprints.get(i).deviceId); + } else if (type == XmppUri.FingerprintType.OTR) { + builder.append(XmppUri.OTR_URI_PARAM); } builder.append('='); builder.append(fingerprints.get(i).fingerprint); @@ -241,7 +244,8 @@ public class XmppUri { } public enum FingerprintType { - OMEMO + OMEMO, + OTR } public static class Fingerprint { @@ -249,6 +253,10 @@ public class XmppUri { public final String fingerprint; final int deviceId; + public Fingerprint(FingerprintType type, String fingerprint) { + this(type, fingerprint, 0); + } + public Fingerprint(FingerprintType type, String fingerprint, int deviceId) { this.type = type; this.fingerprint = fingerprint; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jid/OtrJidHelper.java b/src/main/java/eu/siacs/conversations/xmpp/jid/OtrJidHelper.java new file mode 100644 index 000000000..09c4dec53 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jid/OtrJidHelper.java @@ -0,0 +1,17 @@ + +package eu.siacs.conversations.xmpp.jid; + +import net.java.otr4j.session.SessionID; + +import eu.siacs.conversations.xmpp.Jid; + +public final class OtrJidHelper { + + public static Jid fromSessionID(final SessionID id) throws IllegalArgumentException { + if (id.getUserID().isEmpty()) { + return Jid.of(id.getAccountID()); + } else { + return Jid.of(id.getAccountID() + "/" + id.getUserID()); + } + } +} \ No newline at end of file diff --git a/src/main/res/layout/activity_verify_otr.xml b/src/main/res/layout/activity_verify_otr.xml new file mode 100644 index 000000000..5c15dd3e6 --- /dev/null +++ b/src/main/res/layout/activity_verify_otr.xml @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +