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