ContactDetailsActivity.java

  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.appcompat.app.AlertDialog;
 39import androidx.core.content.ContextCompat;
 40import androidx.core.view.ViewCompat;
 41import androidx.databinding.DataBindingUtil;
 42
 43import com.cheogram.android.Util;
 44
 45import com.google.android.material.color.MaterialColors;
 46import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 47import com.google.common.collect.ImmutableList;
 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.entities.Presence;
 73import eu.siacs.conversations.services.AbstractQuickConversationsService;
 74import eu.siacs.conversations.services.QuickConversationsService;
 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() {
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.list_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.list_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.list_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            binding.flowWidget.setReferencedIds(Ints.toArray(viewIdBuilder.build()));
646        }
647    }
648
649    private void onBadgeClick(final View view) {
650        if (QuickConversationsService.isContactListIntegration(this)) {
651            final Uri systemAccount = contact.getSystemAccount();
652            if (systemAccount == null) {
653                checkContactPermissionAndShowAddDialog();
654            } else {
655                final Intent intent = new Intent(Intent.ACTION_VIEW);
656                intent.setData(systemAccount);
657                try {
658                    startActivity(intent);
659                } catch (final ActivityNotFoundException e) {
660                    Toast.makeText(
661                                    this,
662                                    R.string.no_application_found_to_view_contact,
663                                    Toast.LENGTH_SHORT)
664                            .show();
665                }
666            }
667        } else {
668            Toast.makeText(
669                            this,
670                            R.string.contact_list_integration_not_available,
671                            Toast.LENGTH_SHORT)
672                    .show();
673        }
674    }
675
676    public void onBackendConnected() {
677        if (accountJid != null && contactJid != null) {
678            Account account = xmppConnectionService.findAccountByJid(accountJid);
679            if (account == null) {
680                return;
681            }
682            this.contact = account.getRoster().getContact(contactJid);
683            if (mPendingFingerprintVerificationUri != null) {
684                processFingerprintVerification(mPendingFingerprintVerificationUri);
685                mPendingFingerprintVerificationUri = null;
686            }
687
688            if (Compatibility.hasStoragePermission(this)) {
689                final int limit = GridManager.getCurrentColumnCount(this.binding.media);
690                xmppConnectionService.getAttachments(account, contact.getJid().asBareJid(), limit, this);
691                this.binding.showMedia.setOnClickListener((v) -> MediaBrowserActivity.launch(this, contact));
692            }
693
694            final VcardAdapter items = new VcardAdapter();
695            binding.profileItems.setAdapter(items);
696            binding.profileItems.setOnItemClickListener((a0, v, pos, a3) -> {
697                final Uri uri = items.getUri(pos);
698                if (uri == null) return;
699
700                if ("xmpp".equals(uri.getScheme())) {
701                    switchToConversation(xmppConnectionService.findOrCreateConversation(account, Jid.of(uri.getSchemeSpecificPart()), false, true));
702                } else {
703                    Intent intent = new Intent(Intent.ACTION_VIEW, uri);
704                    try {
705                        startActivity(intent);
706                    } catch (ActivityNotFoundException e) {
707                        Toast.makeText(this, R.string.no_application_found_to_open_link, Toast.LENGTH_SHORT).show();
708                    }
709                }
710            });
711            binding.profileItems.setOnItemLongClickListener((a0, v, pos, a3) -> {
712                String toCopy = null;
713                final Uri uri = items.getUri(pos);
714                if (uri != null) toCopy = uri.toString();
715                if (toCopy == null) {
716                    toCopy = items.getItem(pos).findChildContent("text", Namespace.VCARD4);
717                }
718
719                if (toCopy == null) return false;
720                if (ShareUtil.copyTextToClipboard(ContactDetailsActivity.this, toCopy, R.string.message)) {
721                    Toast.makeText(ContactDetailsActivity.this, R.string.message_copied_to_clipboard, Toast.LENGTH_SHORT).show();
722                }
723                return true;
724            });
725            xmppConnectionService.fetchVcard4(account, contact, (vcard4) -> {
726                if (vcard4 == null) return;
727
728                runOnUiThread(() -> {
729                    for (Element el : vcard4.getChildren()) {
730                        if (el.findChildEnsureSingle("uri", Namespace.VCARD4) != null || el.findChildEnsureSingle("text", Namespace.VCARD4) != null) {
731                            items.add(el);
732                        }
733                    }
734                    Util.justifyListViewHeightBasedOnChildren(binding.profileItems);
735                });
736            });
737
738            final var conversation = xmppConnectionService.findOrCreateConversation(account, contact.getJid(), false, true);
739            binding.storeInCache.setChecked(conversation.storeInCache());
740            binding.storeInCache.setOnCheckedChangeListener((v, checked) -> {
741                conversation.setStoreInCache(checked);
742                xmppConnectionService.updateConversation(conversation);
743            });
744
745            populateView();
746        }
747    }
748
749    @Override
750    public void onKeyStatusUpdated(AxolotlService.FetchStatus report) {
751        refreshUi();
752    }
753
754    @Override
755    protected void processFingerprintVerification(XmppUri uri) {
756        if (contact != null && contact.getJid().asBareJid().equals(uri.getJid()) && uri.hasFingerprints()) {
757            if (xmppConnectionService.verifyFingerprints(contact, uri.getFingerprints())) {
758                Toast.makeText(this, R.string.verified_fingerprints, Toast.LENGTH_SHORT).show();
759            }
760        } else {
761            Toast.makeText(this, R.string.invalid_barcode, Toast.LENGTH_SHORT).show();
762        }
763    }
764
765    @Override
766    public void onMediaLoaded(List<Attachment> attachments) {
767        runOnUiThread(() -> {
768            int limit = GridManager.getCurrentColumnCount(binding.media);
769            mMediaAdapter.setAttachments(attachments.subList(0, Math.min(limit, attachments.size())));
770            binding.mediaWrapper.setVisibility(attachments.size() > 0 ? View.VISIBLE : View.GONE);
771        });
772
773    }
774
775    class VcardAdapter extends ArrayAdapter<Element> {
776        VcardAdapter() { super(ContactDetailsActivity.this, 0); }
777
778        private Drawable getDrawable(int d) {
779            return ContactDetailsActivity.this.getDrawable(d);
780        }
781
782        @Override
783        public View getView(int position, View view, @NonNull ViewGroup parent) {
784            final CommandRowBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.command_row, parent, false);
785            final Element item = getItem(position);
786
787            if (item.getName().equals("org")) {
788                binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.drawable.ic_business_24dp), null, null, null);
789                binding.command.setCompoundDrawablePadding(20);
790            } else if (item.getName().equals("impp")) {
791                binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.drawable.ic_chat_black_24dp), null, null, null);
792                binding.command.setCompoundDrawablePadding(20);
793            } else if (item.getName().equals("url")) {
794                binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.drawable.ic_link_24dp), null, null, null);
795                binding.command.setCompoundDrawablePadding(20);
796            }
797
798            final Uri uri = getUri(position);
799            if (uri != null && uri.getScheme() != null) {
800                if (uri.getScheme().equals("xmpp")) {
801                    binding.command.setText(uri.getSchemeSpecificPart());
802                    binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.drawable.jabber), null, null, null);
803                    binding.command.setCompoundDrawablePadding(20);
804                } else if (uri.getScheme().equals("tel")) {
805                    binding.command.setText(uri.getSchemeSpecificPart());
806                    binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.drawable.ic_call_24dp), null, null, null);
807                    binding.command.setCompoundDrawablePadding(20);
808                } else if (uri.getScheme().equals("mailto")) {
809                    binding.command.setText(uri.getSchemeSpecificPart());
810                    binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.drawable.ic_email_24dp), null, null, null);
811                    binding.command.setCompoundDrawablePadding(20);
812                } else if (uri.getScheme().equals("http") || uri.getScheme().equals("https")) {
813                    binding.command.setText(uri.toString());
814                    binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.drawable.ic_link_24dp), null, null, null);
815                    binding.command.setCompoundDrawablePadding(20);
816                } else {
817                    binding.command.setText(uri.toString());
818                }
819            } else {
820                final String text = item.findChildContent("text", Namespace.VCARD4);
821                binding.command.setText(text);
822            }
823
824            return binding.getRoot();
825        }
826
827        public Uri getUri(int pos) {
828            final Element item = getItem(pos);
829            final String uriS = item.findChildContent("uri", Namespace.VCARD4);
830            if (uriS != null) return Uri.parse(uriS).normalizeScheme();
831            if (item.getName().equals("email")) return Uri.parse("mailto:" + item.findChildContent("text", Namespace.VCARD4));
832            return null;
833        }
834    }
835}