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