package ch.threema.app.fragments;

import static android.view.MenuItem.SHOW_AS_ACTION_ALWAYS;
import static android.view.MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW;
import static android.view.MenuItem.SHOW_AS_ACTION_NEVER;
import static ch.threema.android.ToastKt.showToast;
import static ch.threema.app.asynctasks.ContactSyncPolicy.EXCLUDE;
import static ch.threema.app.asynctasks.ContactSyncPolicy.INCLUDE;
import static ch.threema.common.JavaCompat.isNullOrEmpty;
import static kotlinx.coroutines.flow.FlowKt.drop;

import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.ActionMode;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.FrameLayout;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.appcompat.widget.SearchView;
import androidx.core.util.Pair;
import androidx.core.view.MenuItemCompat;
import androidx.lifecycle.Lifecycle;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;

import com.bumptech.glide.Glide;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
import com.google.android.material.tabs.TabLayout;

import org.koin.java.KoinJavaComponent;
import org.slf4j.Logger;

import java.lang.ref.WeakReference;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import ch.threema.android.FlowJavaCompat;
import ch.threema.android.LifecycleAwareAsyncTask;
import ch.threema.android.ToastDuration;
import ch.threema.app.AppConstants;
import ch.threema.app.R;
import ch.threema.app.activities.AddContactActivity;
import ch.threema.app.activities.ComposeMessageActivity;
import ch.threema.app.contactdetails.ContactDetailActivity;
import ch.threema.app.activities.ThreemaActivity;
import ch.threema.app.adapters.ContactListAdapter;
import ch.threema.app.asynctasks.AndroidContactLinkPolicy;
import ch.threema.app.asynctasks.ContactSyncPolicy;
import ch.threema.app.asynctasks.DeleteContactServices;
import ch.threema.app.asynctasks.DialogMarkContactAsDeletedBackgroundTask;
import ch.threema.app.asynctasks.EmptyOrDeleteConversationsAsyncTask;
import ch.threema.app.dialogs.BottomSheetAbstractDialog;
import ch.threema.app.dialogs.BottomSheetGridDialog;
import ch.threema.app.dialogs.GenericAlertDialog;
import ch.threema.app.dialogs.SelectorDialog;
import ch.threema.app.dialogs.TextWithCheckboxDialog;
import ch.threema.app.dialogs.ThreemaDialogFragment;
import ch.threema.app.emojis.EmojiTextView;
import ch.threema.app.eventbus.GlobalEventBuses;
import ch.threema.app.eventbus.GlobalEventFlows;
import ch.threema.app.eventbus.events.ContactEvent;
import ch.threema.app.eventbus.events.ConversationEvent;
import ch.threema.app.home.HomeActivity;
import ch.threema.app.listeners.ContactSettingsListener;
import ch.threema.app.listeners.SynchronizeContactsListener;
import ch.threema.app.managers.ListenerManager;
import ch.threema.app.managers.ServiceManager;
import ch.threema.app.messagereceiver.MessageReceiver;
import ch.threema.app.preference.service.SynchronizedSettingsService;
import ch.threema.app.routines.SynchronizeContactsRoutine;
import ch.threema.app.services.avatarcache.AvatarCacheService;
import ch.threema.app.services.ContactService;
import ch.threema.app.services.LockAppService;
import ch.threema.app.preference.service.PreferenceService;
import ch.threema.app.services.SynchronizeContactsService;
import ch.threema.app.ui.EmptyView;
import ch.threema.app.ui.InsetSides;
import ch.threema.app.ui.LockingSwipeRefreshLayout;
import ch.threema.app.ui.ResumePauseHandler;
import ch.threema.app.ui.SelectorDialogItem;
import ch.threema.app.ui.SpacingValues;
import ch.threema.app.ui.ViewExtensionsKt;
import ch.threema.app.usecases.GetBottomSheetAppShareTargetsUseCase;
import ch.threema.app.usecases.GetInviteFriendIntentUseCase;
import ch.threema.app.usecases.ShareIdentityUseCase;
import ch.threema.app.usecases.conversations.EmptyOrDeleteConversationsUseCase;
import ch.threema.app.utils.ConfigUtils;
import ch.threema.app.utils.EditTextUtil;
import ch.threema.app.utils.IntentDataUtil;
import ch.threema.app.utils.NameUtil;
import ch.threema.app.utils.executor.BackgroundExecutor;
import ch.threema.app.workers.ContactUpdateWorker;
import ch.threema.app.workers.WorkSyncWorker;
import ch.threema.base.ThreemaException;

import static ch.threema.base.utils.LoggingKt.getThreemaLogger;

import ch.threema.data.datatypes.ContactConversationId;
import ch.threema.data.datatypes.ContactNameFormat;
import ch.threema.domain.models.Contact;
import ch.threema.domain.models.VerificationLevel;
import ch.threema.domain.taskmanager.TriggerSource;
import ch.threema.storage.factories.ContactModelFactory;
import ch.threema.storage.models.ContactModel;
import kotlin.Lazy;

/**
 * This is one of the tabs in the home screen. It shows the contact list.
 */
public class ContactsSectionFragment
    extends MainFragment
    implements
    SwipeRefreshLayout.OnRefreshListener,
    ListView.OnItemClickListener,
    ContactListAdapter.AvatarListener,
    SelectorDialog.SelectorDialogClickListener,
    BottomSheetAbstractDialog.BottomSheetDialogCallback,
    TextWithCheckboxDialog.TextWithCheckboxDialogClickListener,
    GenericAlertDialog.DialogClickListener {
    private static final Logger logger = getThreemaLogger("ContactsSectionFragment");

    private static final int PERMISSION_REQUEST_REFRESH_CONTACTS = 1;
    private static final String DIALOG_TAG_SHARE_WITH = "wsw";
    private static final String DIALOG_TAG_RECENTLY_ADDED_SELECTOR = "ras";
    private static final String DIALOG_TAG_REALLY_DELETE_CONTACTS = "rdc";
    private static final String DIALOG_TAG_REPORT_SPAM = "spam";

    private static final String RUN_ON_ACTIVE_SHOW_LOADING = "show_loading";
    private static final String RUN_ON_ACTIVE_HIDE_LOADING = "hide_loading";
    private static final String RUN_ON_ACTIVE_UPDATE_LIST = "update_list";
    private static final String RUN_ON_ACTIVE_REFRESH_LIST = "refresh_list";
    private static final String RUN_ON_ACTIVE_REFRESH_PULL_TO_REFRESH = "pull_to_refresh";

    private static final String BUNDLE_SEARCH_QUERY = "search-query";
    private static final String BUNDLE_SELECTED_TAB = "tabpos";

    private static final int TAB_ALL_CONTACTS = 0;
    private static final int TAB_WORK_ONLY = 1;

    private static final int SELECTOR_TAG_CHAT = 0;
    private static final int SELECTOR_TAG_SHOW_CONTACT = 1;
    private static final int SELECTOR_TAG_REPORT_SPAM = 2;
    private static final int SELECTOR_TAG_BLOCK = 3;
    private static final int SELECTOR_TAG_DELETE = 4;

    private ResumePauseHandler resumePauseHandler;
    private ListView listView;
    private TextView contactsCounter;
    private LockingSwipeRefreshLayout swipeRefreshLayout;
    private ServiceManager serviceManager;
    private SearchView searchView;
    private MenuItem searchMenuItem;
    private ContactListAdapter contactListAdapter;
    private ActionMode actionMode = null;
    private ExtendedFloatingActionButton floatingButtonView;
    private EmojiTextView stickyInitialView;
    private FrameLayout stickyInitialLayout;
    private TabLayout workTabLayout;

    private SynchronizeContactsService synchronizeContactsService;
    private ContactService contactService;
    @Nullable
    private PreferenceService preferenceService;
    @Nullable
    private SynchronizedSettingsService synchronizedSettingsService;
    private LockAppService lockAppService;
    @NonNull
    private final GlobalEventBuses globalEventBuses = KoinJavaComponent.get(GlobalEventBuses.class);
    @NonNull
    private final GlobalEventFlows globalEventFlows = KoinJavaComponent.get(GlobalEventBuses.class);
    @NonNull
    private final Lazy<ShareIdentityUseCase> shareIdentityUseCase = KoinJavaComponent.inject(ShareIdentityUseCase.class);

    private final BackgroundExecutor backgroundExecutor = new BackgroundExecutor();

    private String searchQuery;
    @SuppressLint("StaticFieldLeak")
    private final TabLayout.OnTabSelectedListener onTabSelectedListener = new TabLayout.OnTabSelectedListener() {
        @Override
        public void onTabSelected(TabLayout.Tab tab) {
            if (swipeRefreshLayout != null && swipeRefreshLayout.isRefreshing()) {
                return;
            }

            if (actionMode != null) {
                actionMode.finish();
            }

            new FetchContactsTask(contactService, false, tab.getPosition(), true) {
                @Override
                protected void onPostExecute(Pair<List<ContactModel>, FetchResults> result) {
                    final List<ContactModel> contactModels = result.first;

                    if (contactModels != null && contactListAdapter != null) {
                        contactListAdapter.updateData(contactModels);
                        if (!isNullOrEmpty(searchQuery)) {
                            contactListAdapter.getFilter().filter(searchQuery);
                        }
                    }
                }
            }.execute(ContactsSectionFragment.this, null);
        }

        @Override
        public void onTabUnselected(TabLayout.Tab tab) {
        }

        @Override
        public void onTabReselected(TabLayout.Tab tab) {
        }
    };

    /**
     * Simple POJO to hold the number of contacts that were added in the last 24h / 30d.
     */
    private static class FetchResults {
        int last24h = 0;
        int last30d = 0;
        int workCount = 0;
    }

    // Contacts changed receiver
    private final BroadcastReceiver contactsChangedReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (resumePauseHandler != null) {
                resumePauseHandler.runOnActive(RUN_ON_ACTIVE_REFRESH_LIST, runIfActiveUpdateList);
            }
        }
    };

    private void startSwipeRefresh() {
        if (swipeRefreshLayout != null) {
            swipeRefreshLayout.setRefreshing(true);
        }
    }

    private void stopSwipeRefresh() {
        if (swipeRefreshLayout != null) {
            swipeRefreshLayout.setRefreshing(false);
        }
    }

    private final ResumePauseHandler.RunIfActive runIfActiveShowLoading = () -> {
        // do nothing
    };

    private final ResumePauseHandler.RunIfActive runIfActiveClearCacheAndRefresh = new ResumePauseHandler.RunIfActive() {
        @Override
        public void runOnUiThread() {
            if (synchronizeContactsService != null && !synchronizeContactsService.isSynchronizationInProgress()) {
                stopSwipeRefresh();

                if (serviceManager != null) {
                    AvatarCacheService avatarCacheService = serviceManager.getAvatarCacheService();
                    //clear the cache
                    avatarCacheService.clear();
                }
                updateList();
            }
        }
    };

    private final ResumePauseHandler.RunIfActive runIfActiveUpdateList = new ResumePauseHandler.RunIfActive() {
        @Override
        public void runOnUiThread() {
            if (synchronizeContactsService == null || !synchronizeContactsService.isSynchronizationInProgress()) {
                updateList();
            }
        }
    };

    private final ResumePauseHandler.RunIfActive runIfActiveUpdatePullToRefresh = new ResumePauseHandler.RunIfActive() {
        @Override
        public void runOnUiThread() {
            if (swipeRefreshLayout != null && preferenceService != null) {
                swipeRefreshLayout.setEnabled(true);
            }
        }
    };

    private final ResumePauseHandler.RunIfActive runIfActiveCreateList = () -> createListAdapter(null);

    private final SynchronizeContactsListener synchronizeContactsListener = new SynchronizeContactsListener() {
        @Override
        public void onStarted(SynchronizeContactsRoutine startedRoutine) {
            //only show loading on "full sync"
            if (resumePauseHandler != null && swipeRefreshLayout != null && startedRoutine.isFullSync()) {
                resumePauseHandler.runOnActive(RUN_ON_ACTIVE_SHOW_LOADING, runIfActiveShowLoading);
            }
        }

        @Override
        public void onFinished(SynchronizeContactsRoutine finishedRoutine) {
            if (resumePauseHandler != null && swipeRefreshLayout != null) {
                resumePauseHandler.runOnActive(RUN_ON_ACTIVE_HIDE_LOADING, runIfActiveClearCacheAndRefresh);
            }
        }

        @Override
        public void onError(SynchronizeContactsRoutine finishedRoutine) {
            if (resumePauseHandler != null && swipeRefreshLayout != null) {
                resumePauseHandler.runOnActive(RUN_ON_ACTIVE_HIDE_LOADING, runIfActiveClearCacheAndRefresh);
            }
        }
    };

    private final ContactSettingsListener contactSettingsListener = new ContactSettingsListener() {
        @Override
        public void onSortingChanged() {
            if (resumePauseHandler != null) {
                resumePauseHandler.runOnActive(RUN_ON_ACTIVE_REFRESH_LIST, runIfActiveCreateList);
            }
        }

        @Override
        public void onNameFormatChanged(@NonNull ContactNameFormat nameFormat) {
            if (resumePauseHandler != null) {
                resumePauseHandler.runOnActive(RUN_ON_ACTIVE_REFRESH_LIST, runIfActiveUpdateList);
            }
        }

        @Override
        public void onIsDefaultContactPictureColoredChanged(boolean isColored) {
            if (resumePauseHandler != null) {
                resumePauseHandler.runOnActive(RUN_ON_ACTIVE_REFRESH_LIST, runIfActiveUpdateList);
            }
        }

        @Override
        public void onShowContactDefinedAvatarsChanged(boolean shouldShow) {
            if (resumePauseHandler != null) {
                resumePauseHandler.runOnActive(RUN_ON_ACTIVE_REFRESH_LIST, runIfActiveUpdateList);
            }
        }

        @Override
        public void onInactiveContactsSettingChanged() {
            if (resumePauseHandler != null) {
                resumePauseHandler.runOnActive(RUN_ON_ACTIVE_REFRESH_LIST, runIfActiveUpdateList);
            }
        }
    };

    /**
     * An AsyncTask that fetches contacts and add counts in the background.
     * <p>
     * (and maybe other code) to separate files, to simplify this 1500+-LOC class.
     */
    private static class FetchContactsTask extends LifecycleAwareAsyncTask<Void, Pair<List<ContactModel>, FetchResults>> {
        ContactService contactService;
        boolean isOnLaunch, forceWork;
        int selectedTab;

        FetchContactsTask(ContactService contactService, boolean isOnLaunch, int selectedTab, boolean forceWork) {
            this.contactService = contactService;
            this.isOnLaunch = isOnLaunch;
            this.selectedTab = selectedTab;
            this.forceWork = forceWork;
        }

        @Override
        protected Pair<List<ContactModel>, FetchResults> doInBackground(Void params) {
            List<ContactModel> allContacts = null;

            // Count new contacts
            final FetchResults results = new FetchResults();

            if (ConfigUtils.isWorkBuild()) {
                results.workCount = contactService.countIsWork();
                if (selectedTab == TAB_WORK_ONLY) {
                    if (results.workCount > 0 || forceWork) {
                        allContacts = contactService.getAllDisplayedWork(ContactService.ContactSelection.INCLUDE_INVALID);
                    }
                }
            }

            if (allContacts == null) {
                allContacts = contactService.getAllDisplayed(ContactService.ContactSelection.INCLUDE_INVALID);
            }

            if (!ConfigUtils.isWorkBuild()) {
                long now = System.currentTimeMillis();
                long delta24h = 1000L * 3600 * 24;
                long delta30d = delta24h * 30;
                for (ContactModel contact : allContacts) {
                    final Instant dateCreated = contact.getDateCreated();
                    if (dateCreated == null) {
                        continue;
                    }
                    if (now - dateCreated.toEpochMilli() < delta24h) {
                        results.last24h += 1;
                    }
                    if (now - dateCreated.toEpochMilli() < delta30d) {
                        results.last30d += 1;
                    }
                }
            }
            return new Pair<>(allContacts, results);
        }
    }

    @Override
    public void onResume() {
        logger.debug("onResume");
        if (this.resumePauseHandler != null) {
            this.resumePauseHandler.onResume();
        }

        if (this.swipeRefreshLayout != null) {
            this.swipeRefreshLayout.setEnabled(this.listView != null && this.listView.getFirstVisiblePosition() == 0);
            stopSwipeRefresh();
        }
        super.onResume();
    }

    @Override
    public void onPause() {
        super.onPause();
        logger.debug("onPause");

        if (this.resumePauseHandler != null) {
            this.resumePauseHandler.onPause();
        }
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        logger.debug("onCreate");

        setRetainInstance(true);
        setHasOptionsMenu(true);

        setupListeners();

        this.resumePauseHandler = ResumePauseHandler.getByActivity(this, this.getActivity());

        this.resumePauseHandler.runOnActive(RUN_ON_ACTIVE_REFRESH_PULL_TO_REFRESH, runIfActiveUpdatePullToRefresh);

        FlowJavaCompat.collect(this, Lifecycle.State.CREATED, globalEventFlows.getContacts(), this::handleContactEvent);
    }

    @Override
    public void onAttach(@NonNull Activity activity) {
        super.onAttach(activity);
        logger.debug("onAttach");
    }

    @Override
    public void onDestroy() {
        logger.debug("onDestroy");

        removeListeners();

        if (this.resumePauseHandler != null) {
            this.resumePauseHandler.onDestroy(this);
        }

        super.onDestroy();
    }

    @Override
    public void onHiddenChanged(boolean hidden) {
        logger.debug("onHiddenChanged: {}", hidden);
        if (hidden) {
            if (actionMode != null) {
                actionMode.finish();
            }

            if (this.searchView != null && this.searchView.isShown() && this.searchMenuItem != null) {
                this.searchMenuItem.collapseActionView();
            }
            if (this.resumePauseHandler != null) {
                this.resumePauseHandler.onPause();
            }
        } else {
            if (this.resumePauseHandler != null) {
                this.resumePauseHandler.onResume();
            }
        }
    }

    @Override
    public void onPrepareOptionsMenu(@NonNull Menu menu) {
        super.onPrepareOptionsMenu(menu);

        // move search item to popup if the lock item is visible
        if (this.searchMenuItem != null) {
            if (lockAppService != null && lockAppService.isLockingEnabled()) {
                this.searchMenuItem.setShowAsAction(SHOW_AS_ACTION_NEVER | SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW);
            } else {
                this.searchMenuItem.setShowAsAction(SHOW_AS_ACTION_ALWAYS | SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW);
            }
        }
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, @NonNull MenuInflater inflater) {
        logger.debug("onCreateOptionsMenu");
        searchMenuItem = menu.findItem(R.id.menu_search_contacts);

        if (searchMenuItem == null) {
            inflater.inflate(R.menu.fragment_contacts, menu);

            if (getActivity() != null && this.isAdded()) {
                this.searchMenuItem = menu.findItem(R.id.menu_search_contacts);
                this.searchView = (SearchView) searchMenuItem.getActionView();

                if (this.searchView != null) {
                    if (!isNullOrEmpty(searchQuery)) {
                        // restore search
                        MenuItemCompat.expandActionView(searchMenuItem);
                        this.searchView.post(() -> {
                            searchView.setQuery(searchQuery, true);
                            searchView.clearFocus();
                        });
                    }
                    this.searchView.setQueryHint(getString(R.string.hint_search_list));
                    this.searchView.setOnQueryTextListener(queryTextListener);
                }
            }
        }
        super.onCreateOptionsMenu(menu, inflater);
    }

    final SearchView.OnQueryTextListener queryTextListener = new SearchView.OnQueryTextListener() {
        @Override
        public boolean onQueryTextChange(String searchQuery) {
            if (contactListAdapter != null) {
                ContactsSectionFragment.this.searchQuery = searchQuery;
                contactListAdapter.getFilter().filter(searchQuery);
            }
            return true;
        }

        @Override
        public boolean onQueryTextSubmit(String query) {
            return true;
        }
    };

    private int getDesiredWorkTab(boolean isOnFirstLaunch, Bundle savedInstanceState) {
        if (ConfigUtils.isWorkBuild()) {
            if (!isOnFirstLaunch) {
                if (savedInstanceState != null) {
                    return savedInstanceState.getInt(BUNDLE_SELECTED_TAB, TAB_ALL_CONTACTS);
                } else if (workTabLayout != null) {
                    return workTabLayout.getSelectedTabPosition();
                }
            }
        }
        return TAB_ALL_CONTACTS;
    }

    @SuppressLint("StaticFieldLeak")
    protected void createListAdapter(final Bundle savedInstanceState) {
        if (getActivity() == null) {
            return;
        }

        if (!this.requiredInstances()) {
            return;
        }

        final int[] desiredTabPosition = {getDesiredWorkTab(savedInstanceState == null, savedInstanceState)};

        new FetchContactsTask(contactService, savedInstanceState == null, desiredTabPosition[0], false) {
            @Override
            protected void onPostExecute(Pair<List<ContactModel>, FetchResults> result) {
                final List<ContactModel> contactModels = result.first;
                final FetchResults counts = result.second;
                if (contactModels != null) {
                    updateContactsCounter(contactModels.size(), counts);
                    if (!contactModels.isEmpty()) {
                        ((EmptyView) listView.getEmptyView()).setup(R.string.no_matching_contacts);
                    }

                    if (isAdded() && getContext() != null) {
                        contactListAdapter = new ContactListAdapter(
                            getContext(),
                            contactModels,
                            contactService,
                            serviceManager.getPreferenceService(),
                            serviceManager.getBlockedIdentitiesService(),
                            ContactsSectionFragment.this,
                            Glide.with(getContext())
                        );
                        listView.setAdapter(contactListAdapter);
                    }

                    if (ConfigUtils.isWorkBuild()) {
                        if (savedInstanceState == null && desiredTabPosition[0] == TAB_WORK_ONLY && counts.workCount == 0) {
                            // fix selected tab as there is now work contact
                            desiredTabPosition[0] = TAB_ALL_CONTACTS;
                        }

                        if (desiredTabPosition[0] != workTabLayout.getSelectedTabPosition()) {
                            workTabLayout.removeOnTabSelectedListener(onTabSelectedListener);
                            workTabLayout.selectTab(workTabLayout.getTabAt(selectedTab));
                            workTabLayout.addOnTabSelectedListener(onTabSelectedListener);
                        }
                    }
                }
            }
        }.execute(this, null);
    }

    @SuppressLint("StaticFieldLeak")
    private void updateList() {
        if (!this.requiredInstances()) {
            logger.error("could not instantiate required objects");
            return;
        }

        int desiredTab = getDesiredWorkTab(false, null);

        if (contactListAdapter != null) {
            new FetchContactsTask(contactService, false, desiredTab, false) {
                @Override
                protected void onPostExecute(Pair<List<ContactModel>, FetchResults> result) {
                    final List<ContactModel> contactModels = result.first;
                    final FetchResults counts = result.second;

                    if (contactModels != null && contactListAdapter != null && isAdded()) {
                        updateContactsCounter(contactModels.size(), counts);
                        contactListAdapter.updateData(contactModels);
                    }
                }
            }.execute(this, null);
        }
    }

    private void updateContactsCounter(int numContacts, @Nullable FetchResults counts) {
        var activity = getActivity();
        if (activity != null && listView != null && isAdded()) {
            if (contactsCounter != null) {
                if (counts != null && preferenceService != null) {
                    ((HomeActivity) activity).setContactsBadgeVisible(preferenceService.getShowUnreadBadge() && counts.last24h > 0);
                }
                if (numContacts > 1) {
                    final StringBuilder builder = new StringBuilder();
                    builder.append(
                        getResources().getQuantityString(R.plurals.contacts_counter_label, numContacts, numContacts)
                    );
                    if (counts != null) {
                        builder.append(" (+").append(counts.last30d).append(" / ").append(getString(R.string.thirty_days_abbrev)).append(")");
                    }
                    contactsCounter.setText(builder.toString());
                    contactsCounter.setVisibility(View.VISIBLE);
                } else {
                    contactsCounter.setVisibility(View.GONE);
                }
            }
            if (ConfigUtils.isWorkBuild() && counts != null) {
                if (counts.workCount > 0) {
                    showWorkTabs();
                } else {
                    hideWorkTabs();
                }
            }
        }
    }

    private void showWorkTabs() {
        if (workTabLayout != null && listView != null) {

            ViewExtensionsKt.applyDeviceInsetsAsPadding(
                (View) workTabLayout.getParent(),
                InsetSides.horizontal()
            );

            FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) listView.getLayoutParams();
            ((ViewGroup) workTabLayout.getParent()).setVisibility(View.VISIBLE);
            layoutParams.topMargin = getResources().getDimensionPixelSize(R.dimen.header_contact_section_work_height);
            listView.setLayoutParams(layoutParams);
            setStickyInitialLayoutTopMargin(layoutParams.topMargin);
        }
    }

    private void hideWorkTabs() {
        if (workTabLayout != null && listView != null) {
            FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) listView.getLayoutParams();
            if (workTabLayout.getSelectedTabPosition() != 0) {
                workTabLayout.selectTab(workTabLayout.getTabAt(0));
            }
            ((ViewGroup) workTabLayout.getParent()).setVisibility(View.GONE);
            layoutParams.topMargin = 0;
            listView.setLayoutParams(layoutParams);

            setStickyInitialLayoutTopMargin(layoutParams.topMargin);
        }
    }

    private void setStickyInitialLayoutTopMargin(int margin) {
        if (stickyInitialLayout != null) {
            ViewGroup.MarginLayoutParams stickyInitialLayoutLayoutParams = (ViewGroup.MarginLayoutParams) stickyInitialLayout.getLayoutParams();
            stickyInitialLayoutLayoutParams.topMargin = margin;
            stickyInitialLayout.setLayoutParams(stickyInitialLayoutLayoutParams);
        }
    }

    final protected boolean requiredInstances() {
        if (!this.checkInstances()) {
            this.instantiate();
        }
        return this.checkInstances();
    }

    protected boolean checkInstances() {
        return serviceManager != null
            && preferenceService != null
            && synchronizedSettingsService != null
            && synchronizeContactsService != null
            && lockAppService != null;
    }

    protected void instantiate() {
        this.serviceManager = ServiceManager.get();

        if (this.serviceManager != null) {
            this.contactService = this.serviceManager.getContactService();
            this.preferenceService = this.serviceManager.getPreferenceService();
            this.synchronizedSettingsService = this.serviceManager.getSynchronizedSettingsService();
            this.synchronizeContactsService = this.serviceManager.getSynchronizeContactsService();
            this.lockAppService = this.serviceManager.getLockAppService();

            FlowJavaCompat.collect(
                this,
                Lifecycle.State.STARTED,
                drop(preferenceService.watchIsContactSyncEnabled(), 1),
                value -> onContactSyncChanged()
            );
        }
    }

    private void onContactSyncChanged() {
        if (isAdded() && !isDetached()) {
            if (resumePauseHandler != null) {
                resumePauseHandler.runOnActive(RUN_ON_ACTIVE_REFRESH_PULL_TO_REFRESH, runIfActiveUpdatePullToRefresh);
            }
            updateEmptyView();
        }
    }

    private void onFABClicked(View v) {
        logger.info("FAB clicked");
        Intent intent = new Intent(getActivity(), AddContactActivity.class);
        intent.putExtra(AddContactActivity.EXTRA_ADD_BY_ID, true);
        startActivity(intent);
    }

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View headerView, fragmentView = getView();

        logger.debug("onCreateView");
        if (fragmentView == null) {
            fragmentView = inflater.inflate(R.layout.fragment_contacts, container, false);

            if (!this.requiredInstances()) {
                logger.error("could not instantiate required objects");
            }

            listView = fragmentView.findViewById(android.R.id.list);
            ViewExtensionsKt.applyDeviceInsetsAsPadding(
                listView,
                InsetSides.horizontal(),
                new SpacingValues(null, null, R.dimen.grid_unit_x10, null)
            );
            listView.setOnItemClickListener(this);
            listView.setDividerHeight(0);
            listView.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE_MODAL);
            listView.setMultiChoiceModeListener(new AbsListView.MultiChoiceModeListener() {
                MenuItem shareItem;

                @Override
                public void onItemCheckedStateChanged(android.view.ActionMode mode, int position, long id, boolean checked) {
                    if (shareItem != null) {
                        final int count = listView.getCheckedItemCount();
                        if (count > 0) {
                            mode.setTitle(Integer.toString(count));
                            shareItem.setVisible(count == 1);
                        }
                    }
                }

                @Override
                public boolean onCreateActionMode(android.view.ActionMode mode, Menu menu) {
                    mode.getMenuInflater().inflate(R.menu.action_contacts_section, menu);
                    actionMode = mode;

                    ConfigUtils.tintMenuIcons(menu, ConfigUtils.getColorFromAttribute(getContext(), R.attr.colorPrimary));

                    return true;
                }

                @Override
                public boolean onPrepareActionMode(android.view.ActionMode mode, Menu menu) {
                    shareItem = menu.findItem(R.id.menu_contacts_share);
                    mode.setTitle(Integer.toString(listView.getCheckedItemCount()));

                    return true;
                }

                @Override
                public boolean onActionItemClicked(android.view.ActionMode mode, MenuItem item) {
                    int id = item.getItemId();
                    if (id == R.id.menu_contacts_remove) {
                        deleteContacts(contactListAdapter.getCheckedItems());
                        return true;
                    } else if (id == R.id.menu_contacts_share) {
                        HashSet<ContactModel> contactModels = contactListAdapter.getCheckedItems();
                        if (contactModels.size() == 1) {
                            shareContact(contactModels.iterator().next());
                        }
                        return true;
                    }
                    return false;
                }

                @Override
                public void onDestroyActionMode(android.view.ActionMode mode) {
                    actionMode = null;
                }
            });

            this.stickyInitialView = fragmentView.findViewById(R.id.initial_sticky);
            this.stickyInitialLayout = fragmentView.findViewById(R.id.initial_sticky_layout);
            this.stickyInitialLayout.setVisibility(View.GONE);

            if (!ConfigUtils.isWorkBuild()) {
                headerView = View.inflate(getActivity(), R.layout.header_contact_section, null);
                listView.addHeaderView(headerView, null, false);

                View footerView = View.inflate(getActivity(), R.layout.footer_contact_section, null);
                ViewExtensionsKt.applyDeviceInsetsAsPadding(
                    footerView,
                    InsetSides.horizontal(),
                    new SpacingValues(R.dimen.grid_unit_x1, null, R.dimen.grid_unit_x1_5, null)
                );
                this.contactsCounter = footerView.findViewById(R.id.contact_counter_text);
                listView.addFooterView(footerView, null, false);

                final View invitePeopleContainer = headerView.findViewById(R.id.invite_people_container);
                final MaterialButton invitePeopleButton = headerView.findViewById(R.id.invite_people_button);
                invitePeopleButton.setOnClickListener(view -> shareInvite());

                ViewExtensionsKt.applyDeviceInsetsAsPadding(
                    invitePeopleContainer,
                    InsetSides.horizontal(),
                    SpacingValues.symmetric(R.dimen.listitem_contacts_margin_top_bottom, R.dimen.listitem_contacts_margin_left_right)
                );
            } else {
                workTabLayout = fragmentView.findViewById(R.id.work_contacts_tab_layout);
                workTabLayout.addOnTabSelectedListener(onTabSelectedListener);
                showWorkTabs();
            }

            this.swipeRefreshLayout = fragmentView.findViewById(R.id.swipe_container);
            this.swipeRefreshLayout.setOnRefreshListener(this);
            this.swipeRefreshLayout.setDistanceToTriggerSync(getResources().getConfiguration().screenHeightDp / 3);
            this.swipeRefreshLayout.setColorSchemeResources(R.color.md_theme_light_primary);
            this.swipeRefreshLayout.setSize(SwipeRefreshLayout.LARGE);

            this.floatingButtonView = fragmentView.findViewById(R.id.floating);
            this.floatingButtonView.setOnClickListener(this::onFABClicked);
            ViewExtensionsKt.applyDeviceInsetsAsMargin(
                this.floatingButtonView,
                InsetSides.horizontal(),
                SpacingValues.all(R.dimen.floating_button_margin)
            );
        }
        return fragmentView;
    }

    private void shareContact(ContactModel contact) {
        var name = NameUtil.getContactDisplayName(contact, preferenceService.getContactNameFormat());
        var result = shareIdentityUseCase.getValue().call(contact.getIdentity(), name);
        if (result instanceof ShareIdentityUseCase.Result.Error) {
            showToast(this, R.string.no_activity_for_mime_type, ToastDuration.LONG);
        }
    }

    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        logger.debug("onViewCreated");

        if (getActivity() != null && listView != null) {
            EmptyView emptyView = new EmptyView(getActivity());
            ((ViewGroup) listView.getParent()).addView(emptyView);
            listView.setEmptyView(emptyView);
            updateEmptyView();
            listView.setOnScrollListener(new AbsListView.OnScrollListener() {
                private int previousFirstVisibleItem = -1;

                @Override
                public void onScrollStateChanged(AbsListView view, int scrollState) {
                }

                @Override
                public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
                    if (swipeRefreshLayout != null) {
                        if (view != null && view.getChildCount() == 0) {
                            swipeRefreshLayout.setEnabled(true);
                        } else if (view != null && view.getChildCount() > 0) {
                            swipeRefreshLayout.setEnabled(firstVisibleItem == 0 && view.getChildAt(0).getTop() == 0);
                        } else {
                            swipeRefreshLayout.setEnabled(false);
                        }
                    }

                    if (view != null) {
                        if (contactListAdapter != null) {
                            int direction = 0;

                            if (floatingButtonView != null) {
                                if (firstVisibleItem == 0) {
                                    floatingButtonView.extend();
                                } else {
                                    floatingButtonView.shrink();
                                }
                            }

                            int headerCount = listView.getHeaderViewsCount();
                            firstVisibleItem -= headerCount;

                            if (firstVisibleItem != previousFirstVisibleItem) {
                                if (previousFirstVisibleItem != -1 && firstVisibleItem != -1) {
                                    if (previousFirstVisibleItem < firstVisibleItem) {
                                        // Scroll Down
                                        direction = 1;
                                    }
                                    if (previousFirstVisibleItem > firstVisibleItem) {
                                        // Scroll Up
                                        direction = -1;
                                    }


                                    stickyInitialView.setText(contactListAdapter.getInitial(firstVisibleItem));

                                    String currentInitial = contactListAdapter.getInitial(firstVisibleItem);
                                    String previousInitial = contactListAdapter.getInitial(previousFirstVisibleItem);
                                    String nextInitial = "";

                                    if (ContactListAdapter.RECENTLY_ADDED_SIGN.equals(currentInitial)) {
                                        stickyInitialLayout.setVisibility(View.GONE);
                                    } else {
                                        if (direction == 1 && firstVisibleItem < contactListAdapter.getCount()) {
                                            nextInitial = contactListAdapter.getInitial(firstVisibleItem + 1);
                                        } else if (direction == -1 && firstVisibleItem > 0) {
                                            nextInitial = contactListAdapter.getInitial(firstVisibleItem - 1);
                                        }

                                        if (direction == 1) {
                                            stickyInitialLayout.setVisibility(nextInitial.equals(currentInitial) ? View.VISIBLE : View.GONE);
                                        } else {
                                            stickyInitialLayout.setVisibility(previousInitial.equals(currentInitial) ? View.VISIBLE : View.GONE);
                                        }
                                    }
                                } else {
                                    stickyInitialLayout.setVisibility(View.GONE);
                                }
                            }
                            previousFirstVisibleItem = firstVisibleItem;
                        }
                    }
                }
            });
        }

        if (savedInstanceState != null) {
            if (isNullOrEmpty(this.searchQuery)) {
                this.searchQuery = savedInstanceState.getString(BUNDLE_SEARCH_QUERY);
            }
        }

        // fill adapter with data
        createListAdapter(savedInstanceState);

        // register a receiver that will receive info about changed contacts from contact sync
        IntentFilter filter = new IntentFilter();
        filter.addAction(IntentDataUtil.ACTION_CONTACTS_CHANGED);
        LocalBroadcastManager.getInstance(getContext()).registerReceiver(contactsChangedReceiver, filter);
    }

    private void updateEmptyView() {
        if (listView == null) {
            return;
        }
        EmptyView emptyView = (EmptyView) listView.getEmptyView();
        emptyView.setup(
            synchronizedSettingsService != null && synchronizedSettingsService.isSyncContacts()
                ? R.string.no_contacts_sync_on
                : R.string.no_contacts
        );
    }

    @Override
    public void onDestroyView() {
        LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(contactsChangedReceiver);

        searchView = null;
        searchMenuItem = null;
        contactListAdapter = null;

        super.onDestroyView();
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        switch (requestCode) {
            case ThreemaActivity.ACTIVITY_ID_ADD_CONTACT:
                if (actionMode != null) {
                    actionMode.finish();
                }
                break;
            case ThreemaActivity.ACTIVITY_ID_CONTACT_DETAIL:
                break;
            default:
                super.onActivityResult(requestCode, resultCode, data);
        }
    }

    @Override
    public void onRefresh() {
        if (actionMode != null) {
            actionMode.finish();
        }

        startSwipeRefresh();

        new Handler(Looper.getMainLooper()).postDelayed(this::stopSwipeRefresh, 2000);

        ContactUpdateWorker.Scheduler contactUpdateWorkerScheduler = KoinJavaComponent.get(ContactUpdateWorker.Scheduler.class);
        contactUpdateWorkerScheduler.performOneTimeSync();

        if (this.synchronizedSettingsService.isSyncContacts() && ConfigUtils.requestContactPermissions(getActivity(), this, PERMISSION_REQUEST_REFRESH_CONTACTS)) {
            if (this.synchronizeContactsService != null) {
                // we force a contact sync even if the grace time has not yet been reached
                preferenceService.setTimeOfLastContactSync(null);
                synchronizeContactsService.instantiateSynchronizationAndRun();
            }
        }

        if (ConfigUtils.isWorkBuild()) {
            try {
                WorkSyncWorker.Scheduler workSyncWorkerScheduler = KoinJavaComponent.get(WorkSyncWorker.Scheduler.class);
                workSyncWorkerScheduler.performOneTimeWorkSync(
                    true,
                    "WorkContactSync"
                );
            } catch (IllegalStateException e) {
                logger.error("Unable to schedule work sync one time work", e);
            }
        }
    }

    private void openConversationForIdentity(@Nullable View v, String identity) {
        // Close keyboard if search view is expanded
        if (searchView != null && !searchView.isIconified()) {
            EditTextUtil.hideSoftKeyboard(searchView);
        }
        Intent intent = ComposeMessageActivity.createIntent(
            getActivity(),
            new ContactConversationId(identity),
            null,
            true
        );
        getActivity().startActivityForResult(intent, ThreemaActivity.ACTIVITY_ID_COMPOSE_MESSAGE);
    }

    private void openContact(@Nullable View view, String identity) {
        Intent intent = new Intent(getActivity(), ContactDetailActivity.class);
        intent.putExtra(AppConstants.INTENT_DATA_CONTACT, identity);

        getActivity().startActivityForResult(intent, ThreemaActivity.ACTIVITY_ID_CONTACT_DETAIL);
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        logger.info("saveInstance");

        if (!isNullOrEmpty(searchQuery)) {
            outState.putString(BUNDLE_SEARCH_QUERY, searchQuery);
        }
        if (ConfigUtils.isWorkBuild() && workTabLayout != null) {
            outState.putInt(BUNDLE_SELECTED_TAB, workTabLayout.getSelectedTabPosition());
        }
        super.onSaveInstanceState(outState);
    }

    @Override
    public boolean onBackPressed() {
        if (actionMode != null) {
            actionMode.finish();
            return true;
        }
        if (this.searchView != null && this.searchView.isShown() && this.searchMenuItem != null) {
            MenuItemCompat.collapseActionView(this.searchMenuItem);
            return true;
        }
        return false;
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
        ContactModel contactModel = contactListAdapter.getClickedItem(v);

        if (contactModel != null) {
            String identity = contactModel.getIdentity();
            if (identity != null) {
                logger.info("Contact clicked, opening conversation");
                openConversationForIdentity(v, identity);
            }
        }
    }

    @Override
    public void onAvatarClick(View view, int position) {
        if (contactListAdapter == null) {
            return;
        }

        View listItemView = (View) view.getParent();

        if (contactListAdapter.getCheckedItemCount() > 0) {
            // forward click on avatar to relevant list item
            position += listView.getHeaderViewsCount();
            listView.setItemChecked(position, !listView.isItemChecked(position));
            return;
        }

        final @Nullable ContactModel clickedContactModel = contactListAdapter.getClickedItem(listItemView);
        if (clickedContactModel != null) {
            logger.info("Contact avatar clicked");
            openContact(view, clickedContactModel.getIdentity());
        }
    }

    @Override
    public boolean onAvatarLongClick(View view, int position) {
        return true;
    }

    @NonNull
    private ContactNameFormat getContactNameFormatDefault() {
        @NonNull ContactNameFormat contactNameFormat = ContactNameFormat.DEFAULT;
        if (preferenceService != null) {
            contactNameFormat = preferenceService.getContactNameFormat();
        }
        return contactNameFormat;
    }

    @Override
    public void onRecentlyAddedClick(ContactModel contactModel) {
        final @NonNull String contactName = NameUtil.getContactDisplayNameOrNickname(contactModel, true, getContactNameFormatDefault());

        ArrayList<SelectorDialogItem> items = new ArrayList<>();
        ArrayList<Integer> tags = new ArrayList<>();

        items.add(new SelectorDialogItem(getString(R.string.chat_with, contactName), R.drawable.ic_chat_bubble));
        tags.add(SELECTOR_TAG_CHAT);

        items.add(new SelectorDialogItem(getString(R.string.show_contact), R.drawable.ic_person_outline));
        tags.add(SELECTOR_TAG_SHOW_CONTACT);

        if (!ConfigUtils.isOnPremBuild()) {
            if (
                !contactModel.isLinkedToAndroidContact() &&
                    isNullOrEmpty(contactModel.getFirstName()) &&
                    isNullOrEmpty(contactModel.getLastName()) &&
                    contactModel.verificationLevel == VerificationLevel.UNVERIFIED
            ) {
                MessageReceiver messageReceiver = contactService.createReceiver(contactModel);
                if (messageReceiver.getMessagesCount() > 0) {
                    items.add(new SelectorDialogItem(getString(R.string.spam_report), R.drawable.ic_outline_report_24));
                    tags.add(SELECTOR_TAG_REPORT_SPAM);
                }
            }
        }

        if (serviceManager.getBlockedIdentitiesService().isBlocked(contactModel.getIdentity())) {
            items.add(new SelectorDialogItem(getString(R.string.unblock_contact), R.drawable.ic_block));
        } else {
            items.add(new SelectorDialogItem(getString(R.string.block_contact), R.drawable.ic_block));
        }
        tags.add(SELECTOR_TAG_BLOCK);

        items.add(new SelectorDialogItem(getString(R.string.delete_contact_action), R.drawable.ic_delete_outline));
        tags.add(SELECTOR_TAG_DELETE);

        SelectorDialog selectorDialog = SelectorDialog.newInstance(getString(R.string.last_added_contact), items, tags, getString(R.string.cancel), null);
        selectorDialog.setData(contactModel);
        selectorDialog.setTargetFragment(this, 0);
        selectorDialog.show(getParentFragmentManager(), DIALOG_TAG_RECENTLY_ADDED_SELECTOR);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode == PERMISSION_REQUEST_REFRESH_CONTACTS) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                this.onRefresh();
            } else if (!shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS)) {
                ConfigUtils.showPermissionRationale(getContext(), getView(), R.string.permission_contacts_sync_required);
            }
        }
    }

    private void setupListeners() {
        ListenerManager.contactSettingsListeners.add(this.contactSettingsListener);
        ListenerManager.synchronizeContactsListeners.add(this.synchronizeContactsListener);
    }

    private void removeListeners() {
        ListenerManager.contactSettingsListeners.remove(this.contactSettingsListener);
        ListenerManager.synchronizeContactsListeners.remove(this.synchronizeContactsListener);
    }

    @UiThread
    private void handleContactEvent(@NonNull ContactEvent contactEvent) {
        if (
            contactEvent instanceof ContactEvent.NewContact ||
                contactEvent instanceof ContactEvent.ContactUpdated ||
                contactEvent instanceof ContactEvent.ContactProfilePictureUpdated
        ) {
            resumePauseHandler.runOnActive(RUN_ON_ACTIVE_UPDATE_LIST, runIfActiveUpdateList);
        } else if (contactEvent instanceof ContactEvent.ContactRemoved) {
            if (searchView != null && searchMenuItem != null && searchMenuItem.isActionViewExpanded()) {
                searchQuery = null;
                searchMenuItem.collapseActionView();
            }

            resumePauseHandler.runOnActive(RUN_ON_ACTIVE_UPDATE_LIST, runIfActiveUpdateList);
        }
    }

    private boolean showExcludeFromContactSync(Set<ContactModel> contacts) {
        if (synchronizedSettingsService == null || !synchronizedSettingsService.isSyncContacts()) {
            return false;
        }

        for (ContactModel contactModel : contacts) {
            if (contactModel.isLinkedToAndroidContact()) {
                return true;
            }
        }

        return false;
    }

    private void deleteContacts(@NonNull Set<ContactModel> contacts) {
        int contactsSelectedToDelete = contacts.size();
        final String deleteContactTitle = getString(
            contactsSelectedToDelete > 1
                ? R.string.delete_multiple_contact_action
                : R.string.delete_contact_action
        );
        final String message = getResources().getQuantityString(
            R.plurals.really_delete_contacts_message,
            contactsSelectedToDelete,
            contactsSelectedToDelete
        );

        ThreemaDialogFragment dialog;
        if (showExcludeFromContactSync(contacts)) {
            dialog = TextWithCheckboxDialog.newInstance(
                deleteContactTitle,
                R.drawable.ic_contact,
                message,
                R.string.exclude_contact,
                R.string.ok,
                R.string.cancel);
        } else {
            logger.info("Showing delete contact dialog");
            dialog = GenericAlertDialog.newInstance(
                deleteContactTitle,
                message,
                R.string.ok,
                R.string.cancel);
        }
        dialog.setTargetFragment(this, 0);
        dialog.setData(contacts);
        dialog.show(getFragmentManager(), DIALOG_TAG_REALLY_DELETE_CONTACTS);
    }

    private void reallyDeleteContacts(@NonNull Set<ContactModel> contactModels, boolean excludeFromSync) throws ThreemaException {
        Set<String> identities = contactModels.stream()
            .map(Contact::getIdentity)
            .collect(Collectors.toSet());

        ContactSyncPolicy syncPolicy = excludeFromSync ? EXCLUDE : INCLUDE;

        DialogMarkContactAsDeletedBackgroundTask task = getDialogDeleteContactBackgroundTask(
            identities, syncPolicy
        );

        backgroundExecutor.execute(task);
    }

    @NonNull
    private DialogMarkContactAsDeletedBackgroundTask getDialogDeleteContactBackgroundTask(
        @NonNull Set<String> identities,
        @NonNull ContactSyncPolicy syncPolicy
    ) {
        DeleteContactServices deleteServices = new DeleteContactServices(
            serviceManager.getUserService(),
            contactService,
            serviceManager.getConversationService(),
            serviceManager.getRingtoneService(),
            serviceManager.getConversationCategoryService(),
            serviceManager.getProfilePicRecipientsService(),
            serviceManager.getWallpaperService(),
            serviceManager.getFileService(),
            serviceManager.getExcludedSyncIdentitiesService(),
            serviceManager.getDhSessionStore(),
            serviceManager.getNotificationService(),
            KoinJavaComponent.get(ContactModelFactory.class)
        );

        return new DialogMarkContactAsDeletedBackgroundTask(
            getParentFragmentManager(),
            new WeakReference<>(getContext()),
            identities,
            serviceManager.getModelRepositories().getContacts(),
            deleteServices,
            syncPolicy,
            AndroidContactLinkPolicy.REMOVE_LINK
        ) {
            @Override
            protected void onFinished() {
                if (actionMode != null) {
                    actionMode.finish();
                }
            }
        };
    }

    @Override
    public void onSelected(String tag, String data) {
        if (!isNullOrEmpty(tag)) {
            sendInvite(tag);
        }
    }

    public void shareInvite() {
        GetBottomSheetAppShareTargetsUseCase useCase = KoinJavaComponent.get(GetBottomSheetAppShareTargetsUseCase.class);
        var bottomSheetItems = useCase.call();
        if (bottomSheetItems.isEmpty()) {
            return;
        }
        BottomSheetGridDialog dialog = BottomSheetGridDialog.newInstance(R.string.invite_via, new ArrayList<>(bottomSheetItems));
        dialog.setTargetFragment(this, 0);
        dialog.show(getParentFragmentManager(), DIALOG_TAG_SHARE_WITH);
    }

    private void sendInvite(String packageName) {
        GetInviteFriendIntentUseCase useCase = KoinJavaComponent.get(GetInviteFriendIntentUseCase.class);

        try {
            var intent = useCase.call(packageName);
            startActivity(intent);
        } catch (Exception e) {
            logger.error("Failed to send invite", e);
            showToast(requireActivity(), R.string.no_activity_for_mime_type, ToastDuration.LONG);
        }
    }

    public void onLogoClicked() {
        logger.info("Logo clicked, scrolling to top");
        if (this.listView != null) {
            // this stops the fling
            this.listView.smoothScrollBy(0, 0);
            this.listView.setSelection(0);
        }
    }

    /* selector dialog callbacks */

    @Override
    public void onClick(String tag, int which, Object data) {
        if (data == null) {
            return;
        }

        ContactModel contactModel = (ContactModel) data;

        switch (which) {
            case SELECTOR_TAG_CHAT:
                openConversationForIdentity(null, contactModel.getIdentity());
                break;
            case SELECTOR_TAG_SHOW_CONTACT:
                openContact(null, contactModel.getIdentity());
                break;
            case SELECTOR_TAG_REPORT_SPAM:
                logger.info("Showing report for spam dialog");
                TextWithCheckboxDialog sdialog = TextWithCheckboxDialog.newInstance(
                    requireContext().getString(
                        R.string.spam_report_dialog_title,
                        NameUtil.getContactDisplayNameOrNickname(contactModel, true, getContactNameFormatDefault())
                    ),
                    R.string.spam_report_dialog_explain,
                    R.string.spam_report_dialog_block_checkbox,
                    R.string.spam_report_short,
                    R.string.cancel
                );
                sdialog.setData(contactModel);
                sdialog.setTargetFragment(this, 0);
                sdialog.show(getParentFragmentManager(), DIALOG_TAG_REPORT_SPAM);
                break;
            case SELECTOR_TAG_BLOCK:
                logger.info("Block contact clicked");
                serviceManager.getBlockedIdentitiesService().toggleBlocked(contactModel.getIdentity(), getContext());
                break;
            case SELECTOR_TAG_DELETE:
                logger.info("Delete contact clicked");
                deleteContacts(Set.of(contactModel));
                break;
        }
    }

    /* callback from TextWithCheckboxDialog */
    @Override
    public void onYes(String tag, Object data, boolean checked) {
        switch (tag) {
            case DIALOG_TAG_REALLY_DELETE_CONTACTS:
                try {
                    logger.info("Contact deletion confirmed");
                    reallyDeleteContacts((Set<ContactModel>) data, checked);
                } catch (ThreemaException e) {
                    logger.error("Could not delete contacts", e);
                }
                break;
            case DIALOG_TAG_REPORT_SPAM:
                logger.info("Reporting contact for spam confirmed");
                ContactModel contactModel = (ContactModel) data;

                contactService.reportSpam(contactModel.getIdentity(),
                    unused -> {
                        if (isAdded()) {
                            Toast.makeText(getContext(), R.string.spam_successfully_reported, Toast.LENGTH_LONG).show();
                        }

                        final String spammerIdentity = contactModel.getIdentity();
                        if (checked) {
                            serviceManager.getBlockedIdentitiesService()
                                .blockIdentity(spammerIdentity, null);
                            serviceManager.getExcludedSyncIdentitiesService()
                                .excludeFromSync(spammerIdentity, TriggerSource.LOCAL);

                            try {
                                new EmptyOrDeleteConversationsAsyncTask(
                                    EmptyOrDeleteConversationsUseCase.Mode.DELETE,
                                    new MessageReceiver[]{contactService.createReceiver(contactModel)},
                                    serviceManager.getConversationService(),
                                    serviceManager.getDistributionListService(),
                                    serviceManager.getModelRepositories().getGroups(),
                                    serviceManager.getGroupFlowDispatcher(),
                                    serviceManager.getUserService().getIdentity(),
                                    null,
                                    null,
                                    () -> {
                                        globalEventBuses.getConversations().emit(ConversationEvent.AllConversationsUpdated.INSTANCE);
                                        globalEventBuses.getContacts().emit(ContactEvent.ContactUpdated.javaCreate(spammerIdentity));
                                    }).execute();
                            } catch (Exception e) {
                                logger.error("Unable to empty chat", e);
                            }
                        } else {
                            globalEventBuses.getContacts().emit(ContactEvent.ContactUpdated.javaCreate(spammerIdentity));
                        }
                    },
                    message -> {
                        if (isAdded()) {
                            Toast.makeText(getContext(), requireContext().getString(R.string.spam_error_reporting, message), Toast.LENGTH_LONG).show();
                        }
                    }
                );
                break;
            default:
                break;
        }
    }

    /**
     * Callbacks from GenericAlertDialog
     */
    @Override
    public void onYes(@Nullable String tag, @Nullable Object data) {
        if (tag != null && tag.equals(DIALOG_TAG_REALLY_DELETE_CONTACTS) && data != null) {
            try {
                reallyDeleteContacts((Set<ContactModel>) data, false);
            } catch (ThreemaException e) {
                logger.error("Could not delete contacts", e);
            }
        }
    }
}
