1package eu.siacs.conversations.ui;
  2
  3import android.Manifest;
  4import android.content.ActivityNotFoundException;
  5import android.content.DialogInterface;
  6import android.content.Intent;
  7import android.content.SharedPreferences;
  8import android.content.pm.PackageManager;
  9import android.content.res.ColorStateList;
 10import android.net.Uri;
 11import android.os.Build;
 12import android.os.Bundle;
 13import android.preference.PreferenceManager;
 14import android.provider.ContactsContract.CommonDataKinds;
 15import android.provider.ContactsContract.Contacts;
 16import android.provider.ContactsContract.Intents;
 17import android.text.Spannable;
 18import android.text.SpannableString;
 19import android.text.style.RelativeSizeSpan;
 20import android.view.LayoutInflater;
 21import android.view.Menu;
 22import android.view.MenuItem;
 23import android.view.View;
 24import android.view.View.OnClickListener;
 25import android.widget.CompoundButton;
 26import android.widget.CompoundButton.OnCheckedChangeListener;
 27import android.widget.TextView;
 28import android.widget.Toast;
 29import androidx.annotation.NonNull;
 30import androidx.core.content.ContextCompat;
 31import androidx.core.view.ViewCompat;
 32import androidx.databinding.DataBindingUtil;
 33import com.google.android.material.color.MaterialColors;
 34import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 35import com.google.common.base.Joiner;
 36import com.google.common.collect.ImmutableList;
 37import com.google.common.collect.Iterables;
 38import com.google.common.primitives.Ints;
 39import eu.siacs.conversations.AppSettings;
 40import eu.siacs.conversations.Config;
 41import eu.siacs.conversations.R;
 42import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 43import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
 44import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
 45import eu.siacs.conversations.databinding.ActivityContactDetailsBinding;
 46import eu.siacs.conversations.entities.Account;
 47import eu.siacs.conversations.entities.Contact;
 48import eu.siacs.conversations.entities.ListItem;
 49import eu.siacs.conversations.services.AbstractQuickConversationsService;
 50import eu.siacs.conversations.services.QuickConversationsService;
 51import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
 52import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
 53import eu.siacs.conversations.ui.adapter.MediaAdapter;
 54import eu.siacs.conversations.ui.interfaces.OnMediaLoaded;
 55import eu.siacs.conversations.ui.util.Attachment;
 56import eu.siacs.conversations.ui.util.AvatarWorkerTask;
 57import eu.siacs.conversations.ui.util.GridManager;
 58import eu.siacs.conversations.ui.util.JidDialog;
 59import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
 60import eu.siacs.conversations.utils.AccountUtils;
 61import eu.siacs.conversations.utils.Compatibility;
 62import eu.siacs.conversations.utils.Emoticons;
 63import eu.siacs.conversations.utils.IrregularUnicodeDetector;
 64import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
 65import eu.siacs.conversations.utils.UIHelper;
 66import eu.siacs.conversations.utils.XEP0392Helper;
 67import eu.siacs.conversations.utils.XmppUri;
 68import eu.siacs.conversations.xml.Namespace;
 69import eu.siacs.conversations.xmpp.Jid;
 70import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
 71import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
 72import eu.siacs.conversations.xmpp.XmppConnection;
 73import eu.siacs.conversations.xmpp.manager.PresenceManager;
 74import eu.siacs.conversations.xmpp.manager.RosterManager;
 75import im.conversations.android.xmpp.model.stanza.Presence;
 76import java.util.Collection;
 77import java.util.Collections;
 78import java.util.List;
 79import org.openintents.openpgp.util.OpenPgpUtils;
 80
 81public class ContactDetailsActivity extends OmemoActivity
 82        implements OnAccountUpdate,
 83                OnRosterUpdate,
 84                OnUpdateBlocklist,
 85                OnKeyStatusUpdated,
 86                OnMediaLoaded {
 87    public static final String ACTION_VIEW_CONTACT = "view_contact";
 88    private final int REQUEST_SYNC_CONTACTS = 0x28cf;
 89    ActivityContactDetailsBinding binding;
 90    private MediaAdapter mMediaAdapter;
 91
 92    private Contact contact;
 93    private final DialogInterface.OnClickListener removeFromRoster =
 94            new DialogInterface.OnClickListener() {
 95
 96                @Override
 97                public void onClick(DialogInterface dialog, int which) {
 98                    xmppConnectionService.deleteContactOnServer(contact);
 99                }
100            };
101    private final OnCheckedChangeListener mOnSendCheckedChange =
102            new OnCheckedChangeListener() {
103
104                @Override
105                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
106                    if (isChecked) {
107                        if (contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
108                            xmppConnectionService.stopPresenceUpdatesTo(contact);
109                        } else {
110                            contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
111                        }
112                    } else {
113                        contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
114                        final var connection = contact.getAccount().getXmppConnection();
115                        connection
116                                .getManager(PresenceManager.class)
117                                .unsubscribed(contact.getJid().asBareJid());
118                    }
119                }
120            };
121    private final OnCheckedChangeListener mOnReceiveCheckedChange =
122            new OnCheckedChangeListener() {
123
124                @Override
125                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
126                    final var connection = contact.getAccount().getXmppConnection();
127                    if (isChecked) {
128                        connection
129                                .getManager(PresenceManager.class)
130                                .subscribe(contact.getJid().asBareJid());
131                    } else {
132                        connection
133                                .getManager(PresenceManager.class)
134                                .unsubscribe(contact.getJid().asBareJid());
135                    }
136                }
137            };
138    private Jid accountJid;
139    private Jid contactJid;
140    private boolean showDynamicTags = false;
141    private boolean showLastSeen = false;
142    private boolean showInactiveOmemo = false;
143    private String messageFingerprint;
144
145    private void checkContactPermissionAndShowAddDialog() {
146        if (hasContactsPermission()) {
147            showAddToPhoneBookDialog();
148        } else if (QuickConversationsService.isContactListIntegration(this)
149                && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
150            requestPermissions(
151                    new String[] {Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
152        }
153    }
154
155    private boolean hasContactsPermission() {
156        if (QuickConversationsService.isContactListIntegration(this)
157                && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
158            return checkSelfPermission(Manifest.permission.READ_CONTACTS)
159                    == PackageManager.PERMISSION_GRANTED;
160        } else {
161            return true;
162        }
163    }
164
165    private void showAddToPhoneBookDialog() {
166        final Jid jid = contact.getJid();
167        final boolean quicksyContact =
168                AbstractQuickConversationsService.isQuicksy()
169                        && Config.QUICKSY_DOMAIN.equals(jid.getDomain())
170                        && jid.getLocal() != null;
171        final String value;
172        if (quicksyContact) {
173            value = PhoneNumberUtilWrapper.toFormattedPhoneNumber(this, jid);
174        } else {
175            value = jid.toString();
176        }
177        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
178        builder.setTitle(getString(R.string.action_add_phone_book));
179        builder.setMessage(getString(R.string.add_phone_book_text, value));
180        builder.setNegativeButton(getString(R.string.cancel), null);
181        builder.setPositiveButton(
182                getString(R.string.add),
183                (dialog, which) -> {
184                    final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
185                    intent.setType(Contacts.CONTENT_ITEM_TYPE);
186                    if (quicksyContact) {
187                        intent.putExtra(Intents.Insert.PHONE, value);
188                    } else {
189                        intent.putExtra(Intents.Insert.IM_HANDLE, value);
190                        intent.putExtra(
191                                Intents.Insert.IM_PROTOCOL, CommonDataKinds.Im.PROTOCOL_JABBER);
192                        // TODO for modern use we want PROTOCOL_CUSTOM and an extra field with a
193                        // value of 'XMPP'
194                        // however we don’t have such a field and thus have to use the legacy
195                        // PROTOCOL_JABBER
196                    }
197                    intent.putExtra("finishActivityOnSaveCompleted", true);
198                    try {
199                        startActivityForResult(intent, 0);
200                    } catch (ActivityNotFoundException e) {
201                        Toast.makeText(
202                                        ContactDetailsActivity.this,
203                                        R.string.no_application_found_to_view_contact,
204                                        Toast.LENGTH_SHORT)
205                                .show();
206                    }
207                });
208        builder.create().show();
209    }
210
211    @Override
212    public void onRosterUpdate() {
213        refreshUi();
214    }
215
216    @Override
217    public void onAccountUpdate() {
218        refreshUi();
219    }
220
221    @Override
222    public void OnUpdateBlocklist(final Status status) {
223        refreshUi();
224    }
225
226    @Override
227    protected void refreshUiReal() {
228        invalidateOptionsMenu();
229        populateView();
230    }
231
232    @Override
233    protected String getShareableUri(boolean http) {
234        if (http) {
235            return "https://conversations.im/i/"
236                    + XmppUri.lameUrlEncode(contact.getJid().asBareJid().toString());
237        } else {
238            return "xmpp:" + contact.getJid().asBareJid().toString();
239        }
240    }
241
242    @Override
243    protected void onCreate(final Bundle savedInstanceState) {
244        super.onCreate(savedInstanceState);
245        showInactiveOmemo =
246                savedInstanceState != null
247                        && savedInstanceState.getBoolean("show_inactive_omemo", false);
248        if (getIntent().getAction().equals(ACTION_VIEW_CONTACT)) {
249            try {
250                this.accountJid = Jid.of(getIntent().getExtras().getString(EXTRA_ACCOUNT));
251            } catch (final IllegalArgumentException ignored) {
252            }
253            try {
254                this.contactJid = Jid.of(getIntent().getExtras().getString("contact"));
255            } catch (final IllegalArgumentException ignored) {
256            }
257        }
258        this.messageFingerprint = getIntent().getStringExtra("fingerprint");
259        this.binding = DataBindingUtil.setContentView(this, R.layout.activity_contact_details);
260        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
261
262        setSupportActionBar(binding.toolbar);
263        configureActionBar(getSupportActionBar());
264        binding.showInactiveDevices.setOnClickListener(
265                v -> {
266                    showInactiveOmemo = !showInactiveOmemo;
267                    populateView();
268                });
269        binding.addContactButton.setOnClickListener(v -> showAddToRosterDialog(contact));
270
271        mMediaAdapter = new MediaAdapter(this, R.dimen.media_size);
272        this.binding.media.setAdapter(mMediaAdapter);
273        GridManager.setupLayoutManager(this, this.binding.media, R.dimen.media_size);
274    }
275
276    @Override
277    public void onSaveInstanceState(final Bundle savedInstanceState) {
278        savedInstanceState.putBoolean("show_inactive_omemo", showInactiveOmemo);
279        super.onSaveInstanceState(savedInstanceState);
280    }
281
282    @Override
283    public void onStart() {
284        super.onStart();
285        final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
286        this.showDynamicTags = preferences.getBoolean(AppSettings.SHOW_DYNAMIC_TAGS, false);
287        this.showLastSeen = preferences.getBoolean("last_activity", false);
288        binding.mediaWrapper.setVisibility(
289                Compatibility.hasStoragePermission(this) ? View.VISIBLE : View.GONE);
290        mMediaAdapter.setAttachments(Collections.emptyList());
291    }
292
293    @Override
294    public void onRequestPermissionsResult(
295            int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
296        // TODO check for Camera / Scan permission
297        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
298        if (grantResults.length > 0)
299            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
300                if (requestCode == REQUEST_SYNC_CONTACTS && xmppConnectionServiceBound) {
301                    showAddToPhoneBookDialog();
302                    xmppConnectionService.loadPhoneContacts();
303                    xmppConnectionService.startContactObserver();
304                }
305            }
306    }
307
308    @Override
309    public boolean onOptionsItemSelected(final MenuItem menuItem) {
310        if (MenuDoubleTabUtil.shouldIgnoreTap()) {
311            return false;
312        }
313        switch (menuItem.getItemId()) {
314            case android.R.id.home:
315                finish();
316                break;
317            case R.id.action_share_http:
318                shareLink(true);
319                break;
320            case R.id.action_share_uri:
321                shareLink(false);
322                break;
323            case R.id.action_delete_contact:
324                final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
325                builder.setNegativeButton(getString(R.string.cancel), null);
326                builder.setTitle(getString(R.string.action_delete_contact))
327                        .setMessage(
328                                JidDialog.style(
329                                        this,
330                                        R.string.remove_contact_text,
331                                        contact.getJid().toString()))
332                        .setPositiveButton(getString(R.string.delete), removeFromRoster)
333                        .create()
334                        .show();
335                break;
336            case R.id.action_edit_contact:
337                final Uri systemAccount = contact.getSystemAccount();
338                if (systemAccount == null) {
339                    quickEdit(
340                            contact.getServerName(),
341                            R.string.contact_name,
342                            value -> {
343                                contact.setServerName(value);
344                                final var connection = contact.getAccount().getXmppConnection();
345                                connection
346                                        .getManager(RosterManager.class)
347                                        .addRosterItem(contact, null);
348                                populateView();
349                                return null;
350                            },
351                            true);
352                } else {
353                    Intent intent = new Intent(Intent.ACTION_EDIT);
354                    intent.setDataAndType(systemAccount, Contacts.CONTENT_ITEM_TYPE);
355                    intent.putExtra("finishActivityOnSaveCompleted", true);
356                    try {
357                        startActivity(intent);
358                    } catch (ActivityNotFoundException e) {
359                        Toast.makeText(
360                                        ContactDetailsActivity.this,
361                                        R.string.no_application_found_to_view_contact,
362                                        Toast.LENGTH_SHORT)
363                                .show();
364                    }
365                }
366                break;
367            case R.id.action_block, R.id.action_unblock:
368                BlockContactDialog.show(this, contact);
369                break;
370            case R.id.action_custom_notifications:
371                configureCustomNotifications(contact);
372                break;
373        }
374        return super.onOptionsItemSelected(menuItem);
375    }
376
377    private void configureCustomNotifications(final Contact contact) {
378        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
379            return;
380        }
381        final var shortcut = xmppConnectionService.getShortcutService().getShortcutInfo(contact);
382        configureCustomNotification(shortcut);
383    }
384
385    @Override
386    public boolean onCreateOptionsMenu(final Menu menu) {
387        getMenuInflater().inflate(R.menu.contact_details, menu);
388        AccountUtils.showHideMenuItems(menu);
389        final MenuItem block = menu.findItem(R.id.action_block);
390        final MenuItem unblock = menu.findItem(R.id.action_unblock);
391        final MenuItem edit = menu.findItem(R.id.action_edit_contact);
392        final MenuItem delete = menu.findItem(R.id.action_delete_contact);
393        final MenuItem customNotifications = menu.findItem(R.id.action_custom_notifications);
394        customNotifications.setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R);
395        if (contact == null) {
396            return true;
397        }
398        final XmppConnection connection = contact.getAccount().getXmppConnection();
399        if (connection != null && connection.getFeatures().blocking()) {
400            if (this.contact.isBlocked()) {
401                block.setVisible(false);
402            } else {
403                unblock.setVisible(false);
404            }
405        } else {
406            unblock.setVisible(false);
407            block.setVisible(false);
408        }
409        if (!contact.showInRoster()) {
410            edit.setVisible(false);
411            delete.setVisible(false);
412        }
413        return super.onCreateOptionsMenu(menu);
414    }
415
416    private void populateView() {
417        if (contact == null) {
418            return;
419        }
420        invalidateOptionsMenu();
421        setTitle(contact.getDisplayName());
422        if (contact.showInRoster()) {
423            binding.detailsSendPresence.setVisibility(View.VISIBLE);
424            binding.detailsReceivePresence.setVisibility(View.VISIBLE);
425            binding.addContactButton.setVisibility(View.GONE);
426            binding.detailsSendPresence.setOnCheckedChangeListener(null);
427            binding.detailsReceivePresence.setOnCheckedChangeListener(null);
428
429            Collection<String> statusMessages = contact.getPresences().getStatusMessages();
430            if (statusMessages.isEmpty()) {
431                binding.statusMessage.setVisibility(View.GONE);
432            } else if (statusMessages.size() == 1) {
433                final String message = Iterables.getOnlyElement(statusMessages);
434                binding.statusMessage.setVisibility(View.VISIBLE);
435                final Spannable span = new SpannableString(message);
436                if (Emoticons.isOnlyEmoji(message)) {
437                    span.setSpan(
438                            new RelativeSizeSpan(2.0f),
439                            0,
440                            message.length(),
441                            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
442                }
443                binding.statusMessage.setText(span);
444            } else {
445                binding.statusMessage.setText(Joiner.on('\n').join(statusMessages));
446            }
447
448            if (contact.getOption(Contact.Options.FROM)) {
449                binding.detailsSendPresence.setText(R.string.send_presence_updates);
450                binding.detailsSendPresence.setChecked(true);
451            } else if (contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
452                binding.detailsSendPresence.setChecked(false);
453                binding.detailsSendPresence.setText(R.string.send_presence_updates);
454            } else {
455                binding.detailsSendPresence.setText(R.string.preemptively_grant);
456                binding.detailsSendPresence.setChecked(
457                        contact.getOption(Contact.Options.PREEMPTIVE_GRANT));
458            }
459            if (contact.getOption(Contact.Options.TO)) {
460                binding.detailsReceivePresence.setText(R.string.receive_presence_updates);
461                binding.detailsReceivePresence.setChecked(true);
462            } else {
463                binding.detailsReceivePresence.setText(R.string.ask_for_presence_updates);
464                binding.detailsReceivePresence.setChecked(
465                        contact.getOption(Contact.Options.ASKING));
466            }
467            if (contact.getAccount().isOnlineAndConnected()) {
468                binding.detailsReceivePresence.setEnabled(true);
469                binding.detailsSendPresence.setEnabled(true);
470            } else {
471                binding.detailsReceivePresence.setEnabled(false);
472                binding.detailsSendPresence.setEnabled(false);
473            }
474            binding.detailsSendPresence.setOnCheckedChangeListener(this.mOnSendCheckedChange);
475            binding.detailsReceivePresence.setOnCheckedChangeListener(this.mOnReceiveCheckedChange);
476        } else {
477            binding.addContactButton.setVisibility(View.VISIBLE);
478            binding.detailsSendPresence.setVisibility(View.GONE);
479            binding.detailsReceivePresence.setVisibility(View.GONE);
480            binding.statusMessage.setVisibility(View.GONE);
481        }
482
483        if (contact.isBlocked() && !this.showDynamicTags) {
484            binding.detailsLastseen.setVisibility(View.VISIBLE);
485            binding.detailsLastseen.setText(R.string.contact_blocked);
486        } else {
487            if (showLastSeen
488                    && contact.getLastseen() > 0
489                    && contact.getPresences().allOrNonSupport(Namespace.IDLE)) {
490                binding.detailsLastseen.setVisibility(View.VISIBLE);
491                binding.detailsLastseen.setText(
492                        UIHelper.lastseen(
493                                getApplicationContext(),
494                                contact.isActive(),
495                                contact.getLastseen()));
496            } else {
497                binding.detailsLastseen.setVisibility(View.GONE);
498            }
499        }
500
501        binding.detailsContactjid.setText(IrregularUnicodeDetector.style(this, contact.getJid()));
502        final String account = contact.getAccount().getJid().asBareJid().toString();
503        binding.detailsAccount.setText(getString(R.string.using_account, account));
504        AvatarWorkerTask.loadAvatar(
505                contact, binding.detailsContactBadge, R.dimen.avatar_on_details_screen_size);
506        binding.detailsContactBadge.setOnClickListener(this::onBadgeClick);
507
508        binding.detailsContactKeys.removeAllViews();
509        boolean hasKeys = false;
510        final LayoutInflater inflater = getLayoutInflater();
511        final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
512        if (Config.supportOmemo() && axolotlService != null) {
513            final Collection<XmppAxolotlSession> sessions =
514                    axolotlService.findSessionsForContact(contact);
515            boolean anyActive = false;
516            for (XmppAxolotlSession session : sessions) {
517                anyActive = session.getTrust().isActive();
518                if (anyActive) {
519                    break;
520                }
521            }
522            boolean skippedInactive = false;
523            boolean showsInactive = false;
524            boolean showUnverifiedWarning = false;
525            for (final XmppAxolotlSession session : sessions) {
526                final FingerprintStatus trust = session.getTrust();
527                hasKeys |= !trust.isCompromised();
528                if (!trust.isActive() && anyActive) {
529                    if (showInactiveOmemo) {
530                        showsInactive = true;
531                    } else {
532                        skippedInactive = true;
533                        continue;
534                    }
535                }
536                if (!trust.isCompromised()) {
537                    boolean highlight = session.getFingerprint().equals(messageFingerprint);
538                    addFingerprintRow(binding.detailsContactKeys, session, highlight);
539                }
540                if (trust.isUnverified()) {
541                    showUnverifiedWarning = true;
542                }
543            }
544            binding.unverifiedWarning.setVisibility(
545                    showUnverifiedWarning ? View.VISIBLE : View.GONE);
546            if (showsInactive || skippedInactive) {
547                binding.showInactiveDevices.setText(
548                        showsInactive
549                                ? R.string.hide_inactive_devices
550                                : R.string.show_inactive_devices);
551                binding.showInactiveDevices.setVisibility(View.VISIBLE);
552            } else {
553                binding.showInactiveDevices.setVisibility(View.GONE);
554            }
555        } else {
556            binding.showInactiveDevices.setVisibility(View.GONE);
557        }
558        final boolean isCameraFeatureAvailable = isCameraFeatureAvailable();
559        binding.scanButton.setVisibility(
560                hasKeys && isCameraFeatureAvailable ? View.VISIBLE : View.GONE);
561        if (hasKeys) {
562            binding.scanButton.setOnClickListener((v) -> ScanActivity.scan(this));
563        }
564        if (Config.supportOpenPgp() && contact.getPgpKeyId() != 0) {
565            hasKeys = true;
566            View view =
567                    inflater.inflate(
568                            R.layout.item_device_fingerprint, binding.detailsContactKeys, false);
569            TextView key = view.findViewById(R.id.key);
570            TextView keyType = view.findViewById(R.id.key_type);
571            keyType.setText(R.string.openpgp_key_id);
572            if ("pgp".equals(messageFingerprint)) {
573                keyType.setTextColor(
574                        MaterialColors.getColor(
575                                keyType, com.google.android.material.R.attr.colorPrimaryVariant));
576            }
577            key.setText(OpenPgpUtils.convertKeyIdToHex(contact.getPgpKeyId()));
578            final OnClickListener openKey = v -> launchOpenKeyChain(contact.getPgpKeyId());
579            view.setOnClickListener(openKey);
580            key.setOnClickListener(openKey);
581            keyType.setOnClickListener(openKey);
582            binding.detailsContactKeys.addView(view);
583        }
584        binding.keysWrapper.setVisibility(hasKeys ? View.VISIBLE : View.GONE);
585
586        final List<ListItem.Tag> tagList = contact.getTags(this);
587        final boolean hasMetaTags =
588                contact.isBlocked() || contact.getShownStatus() != Presence.Availability.OFFLINE;
589        if ((tagList.isEmpty() && !hasMetaTags) || !this.showDynamicTags) {
590            binding.tags.setVisibility(View.GONE);
591        } else {
592            binding.tags.setVisibility(View.VISIBLE);
593            binding.tags.removeViews(1, binding.tags.getChildCount() - 1);
594            final ImmutableList.Builder<Integer> viewIdBuilder = new ImmutableList.Builder<>();
595            for (final ListItem.Tag tag : tagList) {
596                final String name = tag.getName();
597                final TextView tv =
598                        (TextView) inflater.inflate(R.layout.item_tag, binding.tags, false);
599                tv.setText(name);
600                tv.setBackgroundTintList(
601                        ColorStateList.valueOf(
602                                MaterialColors.harmonizeWithPrimary(
603                                        this, XEP0392Helper.rgbFromNick(name))));
604                final int id = ViewCompat.generateViewId();
605                tv.setId(id);
606                viewIdBuilder.add(id);
607                binding.tags.addView(tv);
608            }
609            if (contact.isBlocked()) {
610                final TextView tv =
611                        (TextView) inflater.inflate(R.layout.item_tag, binding.tags, false);
612                tv.setText(R.string.blocked);
613                tv.setBackgroundTintList(
614                        ColorStateList.valueOf(
615                                MaterialColors.harmonizeWithPrimary(
616                                        tv.getContext(),
617                                        ContextCompat.getColor(
618                                                tv.getContext(), R.color.gray_800))));
619                final int id = ViewCompat.generateViewId();
620                tv.setId(id);
621                viewIdBuilder.add(id);
622                binding.tags.addView(tv);
623            } else {
624                final Presence.Availability status = contact.getShownStatus();
625                if (status != Presence.Availability.OFFLINE) {
626                    final TextView tv =
627                            (TextView) inflater.inflate(R.layout.item_tag, binding.tags, false);
628                    UIHelper.setStatus(tv, status);
629                    final int id = ViewCompat.generateViewId();
630                    tv.setId(id);
631                    viewIdBuilder.add(id);
632                    binding.tags.addView(tv);
633                }
634            }
635            binding.flowWidget.setReferencedIds(Ints.toArray(viewIdBuilder.build()));
636        }
637    }
638
639    private void onBadgeClick(final View view) {
640        final var intent = new Intent(this, ViewProfilePictureActivity.class);
641        intent.setData(Uri.fromParts("avatar", contact.getAvatar(), null));
642        intent.putExtra(ViewProfilePictureActivity.EXTRA_DISPLAY_NAME, contact.getDisplayName());
643        startActivity(intent);
644    }
645
646    private void onAddToAddressBookClick(final View view) {
647        if (QuickConversationsService.isContactListIntegration(this)) {
648            final Uri systemAccount = contact.getSystemAccount();
649            if (systemAccount == null) {
650                checkContactPermissionAndShowAddDialog();
651            } else {
652                final Intent intent = new Intent(Intent.ACTION_VIEW);
653                intent.setData(systemAccount);
654                try {
655                    startActivity(intent);
656                } catch (final ActivityNotFoundException e) {
657                    Toast.makeText(
658                                    this,
659                                    R.string.no_application_found_to_view_contact,
660                                    Toast.LENGTH_SHORT)
661                            .show();
662                }
663            }
664        } else {
665            Toast.makeText(
666                            this,
667                            R.string.contact_list_integration_not_available,
668                            Toast.LENGTH_SHORT)
669                    .show();
670        }
671    }
672
673    public void onBackendConnected() {
674        if (accountJid != null && contactJid != null) {
675            Account account = xmppConnectionService.findAccountByJid(accountJid);
676            if (account == null) {
677                return;
678            }
679            this.contact = account.getRoster().getContact(contactJid);
680            if (mPendingFingerprintVerificationUri != null) {
681                processFingerprintVerification(mPendingFingerprintVerificationUri);
682                mPendingFingerprintVerificationUri = null;
683            }
684
685            if (Compatibility.hasStoragePermission(this)) {
686                final int limit = GridManager.getCurrentColumnCount(this.binding.media);
687                xmppConnectionService.getAttachments(
688                        account, contact.getJid().asBareJid(), limit, this);
689                this.binding.showMedia.setOnClickListener(
690                        (v) -> MediaBrowserActivity.launch(this, contact));
691            }
692            populateView();
693        }
694    }
695
696    @Override
697    public void onKeyStatusUpdated(AxolotlService.FetchStatus report) {
698        refreshUi();
699    }
700
701    @Override
702    protected void processFingerprintVerification(XmppUri uri) {
703        if (contact != null
704                && contact.getJid().asBareJid().equals(uri.getJid())
705                && uri.hasFingerprints()) {
706            if (xmppConnectionService.verifyFingerprints(contact, uri.getFingerprints())) {
707                Toast.makeText(this, R.string.verified_fingerprints, Toast.LENGTH_SHORT).show();
708            }
709        } else {
710            Toast.makeText(this, R.string.invalid_barcode, Toast.LENGTH_SHORT).show();
711        }
712    }
713
714    @Override
715    public void onMediaLoaded(List<Attachment> attachments) {
716        runOnUiThread(
717                () -> {
718                    int limit = GridManager.getCurrentColumnCount(binding.media);
719                    mMediaAdapter.setAttachments(
720                            attachments.subList(0, Math.min(limit, attachments.size())));
721                    binding.mediaWrapper.setVisibility(
722                            attachments.size() > 0 ? View.VISIBLE : View.GONE);
723                });
724    }
725}