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