1package eu.siacs.conversations.ui;
2
3import android.Manifest;
4import android.content.ActivityNotFoundException;
5import android.content.Context;
6import android.content.DialogInterface;
7import android.content.Intent;
8import android.content.SharedPreferences;
9import android.content.pm.PackageManager;
10import android.graphics.drawable.Drawable;
11import android.content.res.ColorStateList;
12import android.net.Uri;
13import android.os.Build;
14import android.os.Bundle;
15import android.preference.PreferenceManager;
16import android.provider.ContactsContract.CommonDataKinds;
17import android.provider.ContactsContract.Contacts;
18import android.provider.ContactsContract.Intents;
19import android.text.Spannable;
20import android.text.SpannableString;
21import android.text.style.RelativeSizeSpan;
22import android.util.TypedValue;
23import android.view.inputmethod.InputMethodManager;
24import android.view.LayoutInflater;
25import android.view.Menu;
26import android.view.MenuItem;
27import android.view.View;
28import android.view.View.OnClickListener;
29import android.view.ViewGroup;
30import android.widget.ArrayAdapter;
31import android.widget.CompoundButton;
32import android.widget.CompoundButton.OnCheckedChangeListener;
33import android.widget.EditText;
34import android.widget.TextView;
35import android.widget.Toast;
36
37import androidx.annotation.NonNull;
38import androidx.core.content.ContextCompat;
39import androidx.core.view.ViewCompat;
40import androidx.databinding.DataBindingUtil;
41
42import com.cheogram.android.Util;
43
44import com.google.android.material.color.MaterialColors;
45import com.google.android.material.dialog.MaterialAlertDialogBuilder;
46import com.google.common.collect.ImmutableList;
47import com.google.common.primitives.Ints;
48
49import org.openintents.openpgp.util.OpenPgpUtils;
50
51import java.util.ArrayList;
52import java.util.Collection;
53import java.util.Collections;
54import java.util.Comparator;
55import java.util.List;
56import java.util.Map;
57import java.util.stream.Collectors;
58
59import eu.siacs.conversations.AppSettings;
60import eu.siacs.conversations.Config;
61import eu.siacs.conversations.R;
62import eu.siacs.conversations.crypto.axolotl.AxolotlService;
63import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
64import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
65import eu.siacs.conversations.databinding.ActivityContactDetailsBinding;
66import eu.siacs.conversations.databinding.CommandRowBinding;
67import eu.siacs.conversations.entities.Account;
68import eu.siacs.conversations.entities.Bookmark;
69import eu.siacs.conversations.entities.Contact;
70import eu.siacs.conversations.entities.ListItem;
71import eu.siacs.conversations.entities.Presence;
72import eu.siacs.conversations.services.AbstractQuickConversationsService;
73import eu.siacs.conversations.services.QuickConversationsService;
74import eu.siacs.conversations.services.XmppConnectionService;
75import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
76import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
77import eu.siacs.conversations.ui.adapter.MediaAdapter;
78import eu.siacs.conversations.ui.interfaces.OnMediaLoaded;
79import eu.siacs.conversations.ui.text.FixedURLSpan;
80import eu.siacs.conversations.ui.util.Attachment;
81import eu.siacs.conversations.ui.util.AvatarWorkerTask;
82import eu.siacs.conversations.ui.util.GridManager;
83import eu.siacs.conversations.ui.util.JidDialog;
84import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
85import eu.siacs.conversations.ui.util.ShareUtil;
86import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
87import eu.siacs.conversations.utils.AccountUtils;
88import eu.siacs.conversations.utils.Compatibility;
89import eu.siacs.conversations.utils.Emoticons;
90import eu.siacs.conversations.utils.IrregularUnicodeDetector;
91import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
92import eu.siacs.conversations.utils.UIHelper;
93import eu.siacs.conversations.utils.XEP0392Helper;
94import eu.siacs.conversations.utils.XmppUri;
95import eu.siacs.conversations.xml.Element;
96import eu.siacs.conversations.xml.Namespace;
97import eu.siacs.conversations.xmpp.Jid;
98import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
99import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
100import eu.siacs.conversations.xmpp.XmppConnection;
101
102public class ContactDetailsActivity extends OmemoActivity implements OnAccountUpdate, OnRosterUpdate, OnUpdateBlocklist, OnKeyStatusUpdated, OnMediaLoaded {
103 public static final String ACTION_VIEW_CONTACT = "view_contact";
104 private final int REQUEST_SYNC_CONTACTS = 0x28cf;
105 ActivityContactDetailsBinding binding;
106 private MediaAdapter mMediaAdapter;
107 protected MenuItem edit = null;
108 protected MenuItem save = null;
109
110 private Contact contact;
111 private final DialogInterface.OnClickListener removeFromRoster = new DialogInterface.OnClickListener() {
112
113 @Override
114 public void onClick(DialogInterface dialog, int which) {
115 xmppConnectionService.deleteContactOnServer(contact);
116 }
117 };
118 private final OnCheckedChangeListener mOnSendCheckedChange = new OnCheckedChangeListener() {
119
120 @Override
121 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
122 if (isChecked) {
123 if (contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
124 xmppConnectionService.stopPresenceUpdatesTo(contact);
125 } else {
126 contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
127 }
128 } else {
129 contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
130 xmppConnectionService.sendPresencePacket(contact.getAccount(), xmppConnectionService.getPresenceGenerator().stopPresenceUpdatesTo(contact));
131 }
132 }
133 };
134 private final OnCheckedChangeListener mOnReceiveCheckedChange = new OnCheckedChangeListener() {
135
136 @Override
137 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
138 if (isChecked) {
139 xmppConnectionService.sendPresencePacket(contact.getAccount(), xmppConnectionService.getPresenceGenerator().requestPresenceUpdatesFrom(contact));
140 } else {
141 xmppConnectionService.sendPresencePacket(contact.getAccount(), xmppConnectionService.getPresenceGenerator().stopPresenceUpdatesFrom(contact));
142 }
143 }
144 };
145 private Jid accountJid;
146 private Jid contactJid;
147 private boolean showDynamicTags = false;
148 private boolean showLastSeen = false;
149 private boolean showInactiveOmemo = false;
150 private String messageFingerprint;
151
152 private void checkContactPermissionAndShowAddDialog() {
153 if (hasContactsPermission()) {
154 showAddToPhoneBookDialog();
155 } else if (QuickConversationsService.isContactListIntegration(this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
156 requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
157 }
158 }
159
160 private boolean hasContactsPermission() {
161 if (QuickConversationsService.isContactListIntegration(this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
162 return checkSelfPermission(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED;
163 } else {
164 return true;
165 }
166 }
167
168 private void showAddToPhoneBookDialog() {
169 final Jid jid = contact.getJid();
170 final boolean quicksyContact = AbstractQuickConversationsService.isQuicksy()
171 && Config.QUICKSY_DOMAIN.equals(jid.getDomain())
172 && jid.getLocal() != null;
173 final String value;
174 if (quicksyContact) {
175 value = PhoneNumberUtilWrapper.toFormattedPhoneNumber(this, jid);
176 } else {
177 value = jid.toEscapedString();
178 }
179 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
180 builder.setTitle(getString(R.string.action_add_phone_book));
181 builder.setMessage(getString(R.string.add_phone_book_text, value));
182 builder.setNegativeButton(getString(R.string.cancel), null);
183 builder.setPositiveButton(getString(R.string.add), (dialog, which) -> {
184 final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
185 intent.setType(Contacts.CONTENT_ITEM_TYPE);
186 if (quicksyContact) {
187 intent.putExtra(Intents.Insert.PHONE, value);
188 } else {
189 intent.putExtra(Intents.Insert.IM_HANDLE, value);
190 intent.putExtra(Intents.Insert.IM_PROTOCOL, CommonDataKinds.Im.PROTOCOL_JABBER);
191 //TODO for modern use we want PROTOCOL_CUSTOM and an extra field with a value of 'XMPP'
192 // however we don’t have such a field and thus have to use the legacy PROTOCOL_JABBER
193 }
194 intent.putExtra("finishActivityOnSaveCompleted", true);
195 try {
196 startActivityForResult(intent, 0);
197 } catch (ActivityNotFoundException e) {
198 Toast.makeText(ContactDetailsActivity.this, R.string.no_application_found_to_view_contact, Toast.LENGTH_SHORT).show();
199 }
200 });
201 builder.create().show();
202 }
203
204 @Override
205 public void onRosterUpdate(final XmppConnectionService.UpdateRosterReason reason, final Contact contact) {
206 refreshUi();
207 }
208
209 @Override
210 public void onAccountUpdate() {
211 refreshUi();
212 }
213
214 @Override
215 public void OnUpdateBlocklist(final Status status) {
216 refreshUi();
217 }
218
219 @Override
220 protected void refreshUiReal() {
221 populateView();
222 }
223
224 @Override
225 protected String getShareableUri(boolean http) {
226 if (http) {
227 return "https://conversations.im/i/" + XmppUri.lameUrlEncode(contact.getJid().asBareJid().toEscapedString());
228 } else {
229 return "xmpp:" + Uri.encode(contact.getJid().asBareJid().toEscapedString(), "@/+");
230 }
231 }
232
233 @Override
234 protected void onCreate(final Bundle savedInstanceState) {
235 super.onCreate(savedInstanceState);
236 showInactiveOmemo = savedInstanceState != null && savedInstanceState.getBoolean("show_inactive_omemo", false);
237 if (getIntent().getAction().equals(ACTION_VIEW_CONTACT)) {
238 try {
239 this.accountJid = Jid.ofEscaped(getIntent().getExtras().getString(EXTRA_ACCOUNT));
240 } catch (final IllegalArgumentException ignored) {
241 }
242 try {
243 this.contactJid = Jid.ofEscaped(getIntent().getExtras().getString("contact"));
244 } catch (final IllegalArgumentException ignored) {
245 }
246 }
247 this.messageFingerprint = getIntent().getStringExtra("fingerprint");
248 this.binding = DataBindingUtil.setContentView(this, R.layout.activity_contact_details);
249 Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
250
251 setSupportActionBar(binding.toolbar);
252 configureActionBar(getSupportActionBar());
253 binding.showInactiveDevices.setOnClickListener(v -> {
254 showInactiveOmemo = !showInactiveOmemo;
255 populateView();
256 });
257 binding.addContactButton.setOnClickListener(v -> showAddToRosterDialog(contact));
258
259 mMediaAdapter = new MediaAdapter(this, R.dimen.media_size);
260 this.binding.media.setAdapter(mMediaAdapter);
261 GridManager.setupLayoutManager(this, this.binding.media, R.dimen.media_size);
262 }
263
264 @Override
265 public void onSaveInstanceState(final Bundle savedInstanceState) {
266 savedInstanceState.putBoolean("show_inactive_omemo", showInactiveOmemo);
267 super.onSaveInstanceState(savedInstanceState);
268 }
269
270 @Override
271 public void onStart() {
272 super.onStart();
273 final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
274 this.showDynamicTags = preferences.getBoolean(AppSettings.SHOW_DYNAMIC_TAGS, false);
275 this.showLastSeen = preferences.getBoolean("last_activity", false);
276 binding.mediaWrapper.setVisibility(Compatibility.hasStoragePermission(this) ? View.VISIBLE : View.GONE);
277 mMediaAdapter.setAttachments(Collections.emptyList());
278 }
279
280 @Override
281 public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
282 super.onRequestPermissionsResult(requestCode, permissions, grantResults);
283 if (grantResults.length > 0)
284 if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
285 if (requestCode == REQUEST_SYNC_CONTACTS && xmppConnectionServiceBound) {
286 showAddToPhoneBookDialog();
287 xmppConnectionService.loadPhoneContacts();
288 xmppConnectionService.startContactObserver();
289 }
290 }
291 }
292
293 protected void saveEdits() {
294 binding.editTags.setVisibility(View.GONE);
295 if (edit != null) {
296 EditText text = edit.getActionView().findViewById(R.id.search_field);
297 contact.setServerName(text.getText().toString());
298 contact.setGroups(binding.editTags.getObjects().stream().map(tag -> tag.getName()).collect(Collectors.toList()));
299 ContactDetailsActivity.this.xmppConnectionService.pushContactToServer(contact);
300 populateView();
301 edit.collapseActionView();
302 }
303 if (save != null) save.setVisible(false);
304 }
305
306 @Override
307 public boolean onOptionsItemSelected(final MenuItem menuItem) {
308 if (MenuDoubleTabUtil.shouldIgnoreTap()) {
309 return false;
310 }
311 switch (menuItem.getItemId()) {
312 case android.R.id.home:
313 finish();
314 break;
315 case R.id.action_share_http:
316 shareLink(true);
317 break;
318 case R.id.action_share_uri:
319 shareLink(false);
320 break;
321 case R.id.action_delete_contact:
322 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
323 builder.setNegativeButton(getString(R.string.cancel), null);
324 builder.setTitle(getString(R.string.action_delete_contact))
325 .setMessage(JidDialog.style(this, R.string.remove_contact_text, contact.getJid().toEscapedString()))
326 .setPositiveButton(getString(R.string.delete),
327 removeFromRoster).create().show();
328 break;
329 case R.id.action_save:
330 saveEdits();
331 break;
332 case R.id.action_edit_contact:
333 Uri systemAccount = contact.getSystemAccount();
334 if (systemAccount == null) {
335 menuItem.expandActionView();
336 EditText text = menuItem.getActionView().findViewById(R.id.search_field);
337 text.setOnEditorActionListener((v, actionId, event) -> {
338 saveEdits();
339 return true;
340 });
341 text.setText(contact.getServerName());
342 text.requestFocus();
343 InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
344 if (imm != null) {
345 imm.showSoftInput(text, InputMethodManager.SHOW_IMPLICIT);
346 }
347 binding.tags.setVisibility(View.GONE);
348 binding.editTags.clearSync();
349 for (final ListItem.Tag group : contact.getGroupTags()) {
350 binding.editTags.addObjectSync(group);
351 }
352 ArrayList<ListItem.Tag> tags = new ArrayList<>();
353 for (final Account account : xmppConnectionService.getAccounts()) {
354 for (Contact contact : account.getRoster().getContacts()) {
355 tags.addAll(contact.getTags(this));
356 }
357 for (Bookmark bookmark : account.getBookmarks()) {
358 tags.addAll(bookmark.getTags(this));
359 }
360 }
361 Comparator<Map.Entry<ListItem.Tag,Integer>> sortTagsBy = Map.Entry.comparingByValue(Comparator.reverseOrder());
362 sortTagsBy = sortTagsBy.thenComparing(entry -> entry.getKey().getName());
363
364 ArrayAdapter<ListItem.Tag> adapter = new ArrayAdapter<>(
365 this,
366 android.R.layout.simple_list_item_1,
367 tags.stream()
368 .collect(Collectors.toMap((x) -> x, (t) -> 1, (c1, c2) -> c1 + c2))
369 .entrySet().stream()
370 .sorted(sortTagsBy)
371 .map(e -> e.getKey()).collect(Collectors.toList())
372 );
373 binding.editTags.setAdapter(adapter);
374 if (showDynamicTags) binding.editTags.setVisibility(View.VISIBLE);
375 if (save != null) save.setVisible(true);
376 } else {
377 menuItem.collapseActionView();
378 if (save != null) save.setVisible(false);
379 Intent intent = new Intent(Intent.ACTION_EDIT);
380 intent.setDataAndType(systemAccount, Contacts.CONTENT_ITEM_TYPE);
381 intent.putExtra("finishActivityOnSaveCompleted", true);
382 try {
383 startActivity(intent);
384 } catch (ActivityNotFoundException e) {
385 Toast.makeText(ContactDetailsActivity.this, R.string.no_application_found_to_view_contact, Toast.LENGTH_SHORT).show();
386 }
387
388 }
389 break;
390 case R.id.action_block:
391 BlockContactDialog.show(this, contact);
392 break;
393 case R.id.action_unblock:
394 BlockContactDialog.show(this, contact);
395 break;
396 }
397 return super.onOptionsItemSelected(menuItem);
398 }
399
400 @Override
401 public boolean onCreateOptionsMenu(final Menu menu) {
402 getMenuInflater().inflate(R.menu.contact_details, menu);
403 AccountUtils.showHideMenuItems(menu);
404 edit = menu.findItem(R.id.action_edit_contact);
405 save = menu.findItem(R.id.action_save);
406 MenuItem block = menu.findItem(R.id.action_block);
407 MenuItem unblock = menu.findItem(R.id.action_unblock);
408 MenuItem edit = menu.findItem(R.id.action_edit_contact);
409 MenuItem delete = menu.findItem(R.id.action_delete_contact);
410 if (contact == null) {
411 return true;
412 }
413 final XmppConnection connection = contact.getAccount().getXmppConnection();
414 if (connection != null && connection.getFeatures().blocking()) {
415 if (this.contact.isBlocked()) {
416 block.setVisible(false);
417 } else {
418 unblock.setVisible(false);
419 }
420 } else {
421 unblock.setVisible(false);
422 block.setVisible(false);
423 }
424 if (!contact.showInRoster()) {
425 edit.setVisible(false);
426 delete.setVisible(false);
427 }
428 edit.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
429 @Override
430 public boolean onMenuItemActionCollapse(MenuItem item) {
431 SoftKeyboardUtils.hideSoftKeyboard(ContactDetailsActivity.this);
432 binding.editTags.setVisibility(View.GONE);
433 if (save != null) save.setVisible(false);
434 populateView();
435 return true;
436 }
437
438 @Override
439 public boolean onMenuItemActionExpand(MenuItem item) { return true; }
440 });
441 return super.onCreateOptionsMenu(menu);
442 }
443
444 private void populateView() {
445 if (contact == null) {
446 return;
447 }
448 if (binding.editTags.getVisibility() != View.GONE) return;
449 invalidateOptionsMenu();
450 setTitle(contact.getDisplayName());
451 if (contact.showInRoster()) {
452 binding.detailsSendPresence.setVisibility(View.VISIBLE);
453 binding.detailsReceivePresence.setVisibility(View.VISIBLE);
454 binding.addContactButton.setVisibility(View.GONE);
455 binding.detailsSendPresence.setOnCheckedChangeListener(null);
456 binding.detailsReceivePresence.setOnCheckedChangeListener(null);
457
458 List<String> statusMessages = contact.getPresences().getStatusMessages();
459 if (statusMessages.size() == 0) {
460 binding.statusMessage.setVisibility(View.GONE);
461 } else if (statusMessages.size() == 1) {
462 final String message = statusMessages.get(0);
463 binding.statusMessage.setVisibility(View.VISIBLE);
464 final Spannable span = new SpannableString(message);
465 if (Emoticons.isOnlyEmoji(message)) {
466 span.setSpan(new RelativeSizeSpan(2.0f), 0, message.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
467 }
468 binding.statusMessage.setText(span);
469 } else {
470 StringBuilder builder = new StringBuilder();
471 binding.statusMessage.setVisibility(View.VISIBLE);
472 int s = statusMessages.size();
473 for (int i = 0; i < s; ++i) {
474 builder.append(statusMessages.get(i));
475 if (i < s - 1) {
476 builder.append("\n");
477 }
478 }
479 binding.statusMessage.setText(builder);
480 }
481
482 if (contact.getOption(Contact.Options.FROM)) {
483 binding.detailsSendPresence.setText(R.string.send_presence_updates);
484 binding.detailsSendPresence.setChecked(true);
485 } else if (contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
486 binding.detailsSendPresence.setChecked(false);
487 binding.detailsSendPresence.setText(R.string.send_presence_updates);
488 } else {
489 binding.detailsSendPresence.setText(R.string.preemptively_grant);
490 binding.detailsSendPresence.setChecked(contact.getOption(Contact.Options.PREEMPTIVE_GRANT));
491 }
492 if (contact.getOption(Contact.Options.TO)) {
493 binding.detailsReceivePresence.setText(R.string.receive_presence_updates);
494 binding.detailsReceivePresence.setChecked(true);
495 } else {
496 binding.detailsReceivePresence.setText(R.string.ask_for_presence_updates);
497 binding.detailsReceivePresence.setChecked(contact.getOption(Contact.Options.ASKING));
498 }
499 if (contact.getAccount().isOnlineAndConnected()) {
500 binding.detailsReceivePresence.setEnabled(true);
501 binding.detailsSendPresence.setEnabled(true);
502 } else {
503 binding.detailsReceivePresence.setEnabled(false);
504 binding.detailsSendPresence.setEnabled(false);
505 }
506 binding.detailsSendPresence.setOnCheckedChangeListener(this.mOnSendCheckedChange);
507 binding.detailsReceivePresence.setOnCheckedChangeListener(this.mOnReceiveCheckedChange);
508 } else {
509 binding.addContactButton.setVisibility(View.VISIBLE);
510 binding.detailsSendPresence.setVisibility(View.GONE);
511 binding.detailsReceivePresence.setVisibility(View.GONE);
512 binding.statusMessage.setVisibility(View.GONE);
513 }
514
515 if (contact.isBlocked() && !this.showDynamicTags) {
516 binding.detailsLastseen.setVisibility(View.VISIBLE);
517 binding.detailsLastseen.setText(R.string.contact_blocked);
518 } else {
519 if (showLastSeen
520 && contact.getLastseen() > 0
521 && contact.getPresences().allOrNonSupport(Namespace.IDLE)) {
522 binding.detailsLastseen.setVisibility(View.VISIBLE);
523 binding.detailsLastseen.setText(UIHelper.lastseen(getApplicationContext(), contact.isActive(), contact.getLastseen()));
524 } else {
525 binding.detailsLastseen.setVisibility(View.GONE);
526 }
527 }
528
529 binding.detailsContactjid.setText(IrregularUnicodeDetector.style(this, contact.getJid()));
530 final String account = contact.getAccount().getJid().asBareJid().toEscapedString();
531 binding.detailsAccount.setText(getString(R.string.using_account, account));
532 AvatarWorkerTask.loadAvatar(contact, binding.detailsContactBadge, R.dimen.avatar_on_details_screen_size);
533 binding.detailsContactBadge.setOnClickListener(this::onBadgeClick);
534
535 binding.detailsContactKeys.removeAllViews();
536 boolean hasKeys = false;
537 final LayoutInflater inflater = getLayoutInflater();
538 final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
539 if (Config.supportOmemo() && axolotlService != null) {
540 final Collection<XmppAxolotlSession> sessions = axolotlService.findSessionsForContact(contact);
541 boolean anyActive = false;
542 for (XmppAxolotlSession session : sessions) {
543 anyActive = session.getTrust().isActive();
544 if (anyActive) {
545 break;
546 }
547 }
548 boolean skippedInactive = false;
549 boolean showsInactive = false;
550 boolean showUnverifiedWarning = false;
551 for (final XmppAxolotlSession session : sessions) {
552 final FingerprintStatus trust = session.getTrust();
553 hasKeys |= !trust.isCompromised();
554 if (!trust.isActive() && anyActive) {
555 if (showInactiveOmemo) {
556 showsInactive = true;
557 } else {
558 skippedInactive = true;
559 continue;
560 }
561 }
562 if (!trust.isCompromised()) {
563 boolean highlight = session.getFingerprint().equals(messageFingerprint);
564 addFingerprintRow(binding.detailsContactKeys, session, highlight);
565 }
566 if (trust.isUnverified()) {
567 showUnverifiedWarning = true;
568 }
569 }
570 binding.unverifiedWarning.setVisibility(showUnverifiedWarning ? View.VISIBLE : View.GONE);
571 if (showsInactive || skippedInactive) {
572 binding.showInactiveDevices.setText(showsInactive ? R.string.hide_inactive_devices : R.string.show_inactive_devices);
573 binding.showInactiveDevices.setVisibility(View.VISIBLE);
574 } else {
575 binding.showInactiveDevices.setVisibility(View.GONE);
576 }
577 } else {
578 binding.showInactiveDevices.setVisibility(View.GONE);
579 }
580 final boolean isCameraFeatureAvailable = isCameraFeatureAvailable();
581 binding.scanButton.setVisibility(hasKeys && isCameraFeatureAvailable ? View.VISIBLE : View.GONE);
582 if (hasKeys) {
583 binding.scanButton.setOnClickListener((v) -> ScanActivity.scan(this));
584 }
585 if (Config.supportOpenPgp() && contact.getPgpKeyId() != 0) {
586 hasKeys = true;
587 View view = inflater.inflate(R.layout.contact_key, binding.detailsContactKeys, false);
588 TextView key = view.findViewById(R.id.key);
589 TextView keyType = view.findViewById(R.id.key_type);
590 keyType.setText(R.string.openpgp_key_id);
591 if ("pgp".equals(messageFingerprint)) {
592 keyType.setTextColor(MaterialColors.getColor(keyType, com.google.android.material.R.attr.colorPrimaryVariant));
593 }
594 key.setText(OpenPgpUtils.convertKeyIdToHex(contact.getPgpKeyId()));
595 final OnClickListener openKey = v -> launchOpenKeyChain(contact.getPgpKeyId());
596 view.setOnClickListener(openKey);
597 key.setOnClickListener(openKey);
598 keyType.setOnClickListener(openKey);
599 binding.detailsContactKeys.addView(view);
600 }
601 binding.keysWrapper.setVisibility(hasKeys ? View.VISIBLE : View.GONE);
602
603 final List<ListItem.Tag> tagList = contact.getTags(this);
604 final boolean hasMetaTags = contact.isBlocked() || contact.getShownStatus() != Presence.Status.OFFLINE;
605 if ((tagList.isEmpty() && !hasMetaTags) || !this.showDynamicTags) {
606 binding.tags.setVisibility(View.GONE);
607 } else {
608 binding.tags.setVisibility(View.VISIBLE);
609 binding.tags.removeViews(1, binding.tags.getChildCount() - 1);
610 final ImmutableList.Builder<Integer> viewIdBuilder = new ImmutableList.Builder<>();
611 for (final ListItem.Tag tag : tagList) {
612 final String name = tag.getName();
613 final TextView tv = (TextView) inflater.inflate(R.layout.item_tag, binding.tags, false);
614 tv.setText(name);
615 tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(this,XEP0392Helper.rgbFromNick(name))));
616 final int id = ViewCompat.generateViewId();
617 tv.setId(id);
618 viewIdBuilder.add(id);
619 binding.tags.addView(tv);
620 }
621 if (contact.isBlocked()) {
622 final TextView tv =
623 (TextView)
624 inflater.inflate(
625 R.layout.item_tag, binding.tags, false);
626 tv.setText(R.string.blocked);
627 tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(tv.getContext(), ContextCompat.getColor(tv.getContext(),R.color.gray_800))));
628 final int id = ViewCompat.generateViewId();
629 tv.setId(id);
630 viewIdBuilder.add(id);
631 binding.tags.addView(tv);
632 } else {
633 final Presence.Status status = contact.getShownStatus();
634 if (status != Presence.Status.OFFLINE) {
635 final TextView tv =
636 (TextView)
637 inflater.inflate(
638 R.layout.item_tag, binding.tags, false);
639 UIHelper.setStatus(tv, status);
640 final int id = ViewCompat.generateViewId();
641 tv.setId(id);
642 viewIdBuilder.add(id);
643 binding.tags.addView(tv);
644 }
645 }
646 if (contact.getJid().isDomainJid()) {
647 for (final var p : contact.getPresences().getPresences()) {
648 final var disco = p.getServiceDiscoveryResult();
649 if (disco == null) continue;
650 for (final var identity : disco.getIdentities()) {
651 final var txt = identity.getCategory() + "/" + identity.getType();
652 final TextView tv =
653 (TextView)
654 inflater.inflate(
655 R.layout.item_tag, binding.tags, false);
656 tv.setText(txt);
657 tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(this,XEP0392Helper.rgbFromNick(txt))));
658 final int id = ViewCompat.generateViewId();
659 tv.setId(id);
660 viewIdBuilder.add(id);
661 binding.tags.addView(tv);
662 }
663 }
664 }
665 binding.flowWidget.setReferencedIds(Ints.toArray(viewIdBuilder.build()));
666 }
667 }
668
669 private void onBadgeClick(final View view) {
670 if (QuickConversationsService.isContactListIntegration(this)) {
671 final Uri systemAccount = contact.getSystemAccount();
672 if (systemAccount == null) {
673 checkContactPermissionAndShowAddDialog();
674 } else {
675 final Intent intent = new Intent(Intent.ACTION_VIEW);
676 intent.setData(systemAccount);
677 try {
678 startActivity(intent);
679 } catch (final ActivityNotFoundException e) {
680 Toast.makeText(
681 this,
682 R.string.no_application_found_to_view_contact,
683 Toast.LENGTH_SHORT)
684 .show();
685 }
686 }
687 } else {
688 Toast.makeText(
689 this,
690 R.string.contact_list_integration_not_available,
691 Toast.LENGTH_SHORT)
692 .show();
693 }
694 }
695
696 public void onBackendConnected() {
697 if (accountJid != null && contactJid != null) {
698 Account account = xmppConnectionService.findAccountByJid(accountJid);
699 if (account == null) {
700 return;
701 }
702 this.contact = account.getRoster().getContact(contactJid);
703 if (mPendingFingerprintVerificationUri != null) {
704 processFingerprintVerification(mPendingFingerprintVerificationUri);
705 mPendingFingerprintVerificationUri = null;
706 }
707
708 if (Compatibility.hasStoragePermission(this)) {
709 final int limit = GridManager.getCurrentColumnCount(this.binding.media);
710 xmppConnectionService.getAttachments(account, contact.getJid().asBareJid(), limit, this);
711 this.binding.showMedia.setOnClickListener((v) -> MediaBrowserActivity.launch(this, contact));
712 }
713
714 final VcardAdapter items = new VcardAdapter();
715 binding.profileItems.setAdapter(items);
716 binding.profileItems.setOnItemClickListener((a0, v, pos, a3) -> {
717 final Uri uri = items.getUri(pos);
718 if (uri == null) return;
719 new FixedURLSpan(uri.toString()).onClick(v);
720 });
721 binding.profileItems.setOnItemLongClickListener((a0, v, pos, a3) -> {
722 String toCopy = null;
723 final Uri uri = items.getUri(pos);
724 if (uri != null) toCopy = uri.toString();
725 if (toCopy == null) {
726 toCopy = items.getItem(pos).findChildContent("text", Namespace.VCARD4);
727 }
728
729 if (toCopy == null) return false;
730 if (ShareUtil.copyTextToClipboard(ContactDetailsActivity.this, toCopy, R.string.message)) {
731 Toast.makeText(ContactDetailsActivity.this, R.string.message_copied_to_clipboard, Toast.LENGTH_SHORT).show();
732 }
733 return true;
734 });
735 xmppConnectionService.fetchVcard4(account, contact, (vcard4) -> {
736 if (vcard4 == null) return;
737
738 runOnUiThread(() -> {
739 for (Element el : vcard4.getChildren()) {
740 if (el.findChildEnsureSingle("uri", Namespace.VCARD4) != null || el.findChildEnsureSingle("text", Namespace.VCARD4) != null) {
741 items.add(el);
742 }
743 }
744 Util.justifyListViewHeightBasedOnChildren(binding.profileItems);
745 });
746 });
747
748 final var conversation = xmppConnectionService.findOrCreateConversation(account, contact.getJid(), false, true);
749 binding.storeInCache.setChecked(conversation.storeInCache());
750 binding.storeInCache.setOnCheckedChangeListener((v, checked) -> {
751 conversation.setStoreInCache(checked);
752 xmppConnectionService.updateConversation(conversation);
753 });
754
755 populateView();
756 }
757 }
758
759 @Override
760 public void onKeyStatusUpdated(AxolotlService.FetchStatus report) {
761 refreshUi();
762 }
763
764 @Override
765 protected void processFingerprintVerification(XmppUri uri) {
766 if (contact != null && contact.getJid().asBareJid().equals(uri.getJid()) && uri.hasFingerprints()) {
767 if (xmppConnectionService.verifyFingerprints(contact, uri.getFingerprints())) {
768 Toast.makeText(this, R.string.verified_fingerprints, Toast.LENGTH_SHORT).show();
769 }
770 } else {
771 Toast.makeText(this, R.string.invalid_barcode, Toast.LENGTH_SHORT).show();
772 }
773 }
774
775 @Override
776 public void onMediaLoaded(List<Attachment> attachments) {
777 runOnUiThread(() -> {
778 int limit = GridManager.getCurrentColumnCount(binding.media);
779 mMediaAdapter.setAttachments(attachments.subList(0, Math.min(limit, attachments.size())));
780 binding.mediaWrapper.setVisibility(attachments.size() > 0 ? View.VISIBLE : View.GONE);
781 });
782
783 }
784
785 class VcardAdapter extends ArrayAdapter<Element> {
786 VcardAdapter() { super(ContactDetailsActivity.this, 0); }
787
788 private Drawable getDrawable(int d) {
789 return ContactDetailsActivity.this.getDrawable(d);
790 }
791
792 @Override
793 public View getView(int position, View view, @NonNull ViewGroup parent) {
794 final CommandRowBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.command_row, parent, false);
795 final Element item = getItem(position);
796
797 if (item.getName().equals("org")) {
798 binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.drawable.ic_business_24dp), null, null, null);
799 binding.command.setCompoundDrawablePadding(20);
800 } else if (item.getName().equals("impp")) {
801 binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.drawable.ic_chat_black_24dp), null, null, null);
802 binding.command.setCompoundDrawablePadding(20);
803 } else if (item.getName().equals("url")) {
804 binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.drawable.ic_link_24dp), null, null, null);
805 binding.command.setCompoundDrawablePadding(20);
806 }
807
808 final Uri uri = getUri(position);
809 if (uri != null && uri.getScheme() != null) {
810 if (uri.getScheme().equals("xmpp")) {
811 binding.command.setText(uri.getSchemeSpecificPart());
812 binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.drawable.jabber), null, null, null);
813 binding.command.setCompoundDrawablePadding(20);
814 } else if (uri.getScheme().equals("tel")) {
815 binding.command.setText(uri.getSchemeSpecificPart());
816 binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.drawable.ic_call_24dp), null, null, null);
817 binding.command.setCompoundDrawablePadding(20);
818 } else if (uri.getScheme().equals("mailto")) {
819 binding.command.setText(uri.getSchemeSpecificPart());
820 binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.drawable.ic_email_24dp), null, null, null);
821 binding.command.setCompoundDrawablePadding(20);
822 } else if (uri.getScheme().equals("bitcoin")) {
823 binding.command.setText(uri.getSchemeSpecificPart());
824 binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.drawable.bitcoin_24dp), null, null, null);
825 binding.command.setCompoundDrawablePadding(20);
826 } else if (uri.getScheme().equals("bitcoincash")) {
827 binding.command.setText(uri.getSchemeSpecificPart());
828 binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.drawable.bitcoin_cash_24dp), null, null, null);
829 binding.command.setCompoundDrawablePadding(20);
830 } else if (uri.getScheme().equals("ethereum")) {
831 binding.command.setText(uri.getSchemeSpecificPart());
832 binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.drawable.eth_24dp), null, null, null);
833 binding.command.setCompoundDrawablePadding(20);
834 } else if (uri.getScheme().equals("monero")) {
835 binding.command.setText(uri.getSchemeSpecificPart());
836 binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.drawable.monero_24dp), null, null, null);
837 binding.command.setCompoundDrawablePadding(20);
838 } else if (uri.getScheme().equals("wownero")) {
839 binding.command.setText(uri.getSchemeSpecificPart());
840 binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.drawable.wownero_24dp), null, null, null);
841 binding.command.setCompoundDrawablePadding(20);
842 } else if (uri.getScheme().equals("https") && "liberapay.com".equals(uri.getHost())) {
843 binding.command.setText(uri.getPath().substring(1));
844 binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.drawable.liberapay), null, null, null);
845 binding.command.setCompoundDrawablePadding(20);
846 } else if (uri.getScheme().equals("https") && ("www.patreon.com".equals(uri.getHost()) || "patreon.com".equals(uri.getHost()))) {
847 binding.command.setText(uri.getPath().replaceAll("^/(?:c/)?", ""));
848 binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.drawable.patreon), null, null, null);
849 binding.command.setCompoundDrawablePadding(20);
850 } else if (uri.getScheme().equals("http") || uri.getScheme().equals("https")) {
851 binding.command.setText(uri.toString());
852 binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.drawable.ic_link_24dp), null, null, null);
853 binding.command.setCompoundDrawablePadding(20);
854 } else {
855 binding.command.setText(uri.toString());
856 }
857 } else {
858 final String text = item.findChildContent("text", Namespace.VCARD4);
859 binding.command.setText(text);
860 }
861
862 return binding.getRoot();
863 }
864
865 public Uri getUri(int pos) {
866 final Element item = getItem(pos);
867 final String uriS = item.findChildContent("uri", Namespace.VCARD4);
868 if (uriS != null) return Uri.parse(uriS).normalizeScheme();
869 if (item.getName().equals("email")) return Uri.parse("mailto:" + item.findChildContent("text", Namespace.VCARD4));
870 return null;
871 }
872 }
873}