package ch.threema.app.activities;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.os.Bundle;
import android.text.Editable;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.Toast;

import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.CollapsingToolbarLayout;
import com.google.android.material.appbar.MaterialToolbar;
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;

import org.koin.android.compat.ViewModelCompat;
import org.koin.java.KoinJavaComponent;
import org.slf4j.Logger;

import java.io.File;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import androidx.annotation.AnyThread;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.annotation.UiThread;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.view.menu.MenuBuilder;
import androidx.core.app.ActivityCompat;
import androidx.core.app.ActivityOptionsCompat;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import ch.threema.android.FlowJavaCompat;
import ch.threema.app.AppConstants;
import ch.threema.app.R;
import ch.threema.app.adapters.GroupDetailAdapter;
import ch.threema.app.contactdetails.ContactDetailActivity;
import ch.threema.app.di.DependencyContainer;
import ch.threema.app.dialogs.GenericAlertDialog;
import ch.threema.app.dialogs.GroupDescEditDialog;
import ch.threema.app.dialogs.SelectorDialog;
import ch.threema.app.dialogs.SimpleStringAlertDialog;
import ch.threema.app.dialogs.TextEntryDialog;
import ch.threema.app.dialogs.loadingtimeout.LoadingWithTimeoutDialogXml;
import ch.threema.app.emojis.EmojiEditText;
import ch.threema.app.eventbus.GlobalEventFlows;
import ch.threema.app.eventbus.events.ContactEvent;
import ch.threema.app.eventbus.events.GroupEvent;
import ch.threema.app.groupflows.GroupChanges.ProfilePictureChange;
import ch.threema.app.groupflows.GroupFlowResult;
import ch.threema.app.groupflows.GroupChanges;
import ch.threema.app.groupflows.GroupCreateProperties;
import ch.threema.app.groupflows.GroupDisbandIntent;
import ch.threema.app.groupflows.GroupLeaveIntent;
import ch.threema.app.home.HomeActivity;
import ch.threema.app.listeners.ContactSettingsListener;
import ch.threema.app.managers.ListenerManager;
import ch.threema.app.mediagallery.MediaGalleryActivity;
import ch.threema.app.profilepicture.CheckedProfilePicture;
import ch.threema.app.services.ContactService;
import ch.threema.app.ui.AvatarEditView;
import ch.threema.app.ui.GroupDetailViewModel;
import ch.threema.app.ui.ResumePauseHandler;
import ch.threema.app.ui.SelectorDialogItem;
import ch.threema.android.textwatchers.SimpleTextWatcher;
import ch.threema.app.ui.ViewExtensionsKt;
import ch.threema.app.utils.ConfigUtils;
import ch.threema.app.utils.ContactUtil;
import ch.threema.app.utils.DisplayableGroupParticipant;
import ch.threema.app.utils.DisplayableGroupParticipants;
import ch.threema.app.utils.GroupCallUtil;
import ch.threema.app.utils.IntentDataUtil;
import ch.threema.app.utils.NameUtil;
import ch.threema.app.utils.RuntimeUtil;
import ch.threema.app.voip.groupcall.GroupCallDescription;
import ch.threema.app.voip.util.VoipUtil;
import ch.threema.base.utils.CoroutinesExtensionKt;

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

import ch.threema.data.datatypes.ContactConversationId;
import ch.threema.data.datatypes.GroupConversationId;
import ch.threema.data.models.GroupModel;
import ch.threema.data.models.GroupModelData;
import ch.threema.storage.models.ContactModel;
import ch.threema.storage.models.group.GroupModelOld;
import kotlin.Unit;
import kotlinx.coroutines.Deferred;

import static ch.threema.app.adapters.GroupDetailAdapter.GroupDescState.COLLAPSED;
import static ch.threema.app.adapters.GroupDetailAdapter.GroupDescState.NONE;
import static ch.threema.app.di.DIJavaCompat.isSessionScopeReady;
import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
import static ch.threema.app.groupflows.GroupFlowResultKt.GROUP_FLOWS_LOADING_DIALOG_TIMEOUT_SECONDS;
import static ch.threema.common.JavaCompat.areEqual;

public class GroupDetailActivity extends GroupEditActivity implements SelectorDialog.SelectorDialogClickListener,
    GenericAlertDialog.DialogClickListener,
    TextEntryDialog.TextEntryDialogClickListener,
    GroupDetailAdapter.OnGroupDetailsClickListener {
    private static final Logger logger = getThreemaLogger("GroupDetailActivity");

    @NonNull
    public static Intent createIntent(@NonNull Context context, long groupDatabaseId) {
        final @NonNull Intent intent = new Intent(context, GroupDetailActivity.class);
        intent.putExtra(AppConstants.INTENT_DATA_GROUP_DATABASE_ID, groupDatabaseId);
        return intent;
    }

    // static values
    private final int MODE_EDIT = 1;
    private final int MODE_READONLY = 2;

    private static final String DIALOG_TAG_LEAVE_GROUP = "leaveGroup";
    private static final String DIALOG_TAG_DISSOLVE_GROUP = "dissolveGroup";
    private static final String DIALOG_TAG_QUIT = "quit";
    private static final String DIALOG_TAG_CHOOSE_ACTION = "chooseAction";
    private static final String DIALOG_TAG_DELETE_GROUP = "delG";
    private static final String DIALOG_TAG_CLONE_GROUP = "cg";
    private static final String DIALOG_TAG_CHANGE_GROUP_DESC = "cgDesc";
    private static final String DIALOG_TAG_CLONE_GROUP_CONFIRM = "cgc";
    private static final String RUN_ON_ACTIVE_RELOAD = "reload";

    private static final int SELECTOR_OPTION_CONTACT_DETAIL = 0;
    private static final int SELECTOR_OPTION_CHAT = 1;
    private static final int SELECTOR_OPTION_CALL = 2;
    private static final int SELECTOR_OPTION_REMOVE = 3;

    @NonNull
    private final DependencyContainer dependencies = KoinJavaComponent.get(DependencyContainer.class);
    @NonNull
    private final GlobalEventFlows globalEventFlows = KoinJavaComponent.get(GlobalEventFlows.class);

    private GroupModel groupModel;
    private GroupDetailViewModel groupDetailViewModel;
    private GroupDetailAdapter groupDetailAdapter;

    private EmojiEditText groupNameEditText;
    private ResumePauseHandler resumePauseHandler;
    private AvatarEditView avatarEditView;
    private ExtendedFloatingActionButton floatingActionButton;

    private String myIdentity;
    private int operationMode;
    private long groupId;

    private final ResumePauseHandler.RunIfActive runIfActiveUpdate = new ResumePauseHandler.RunIfActive() {
        @Override
        public void runOnUiThread() {
            GroupModelData groupModelData = groupModel.getData();
            if (groupModelData == null) {
                logger.warn("Group has been deleted");
                finish();
                return;
            }

            groupDetailViewModel.setGroupName(groupModelData.name);
            groupDetailViewModel.setGroupIdentities(groupModelData.getAllMembers(myIdentity).toArray(new String[]{}));
            sortGroupMembers();
        }
    };

    private final AvatarEditView.AvatarEditListener avatarEditViewListener = new AvatarEditView.AvatarEditListener() {
        @Override
        public void onAvatarSet(File avatarFile1) {
            groupDetailViewModel.setAvatarFile(avatarFile1);
            groupDetailViewModel.setAvatarRemoved(false);
            updateFloatingActionButtonAndMenu();
        }

        @Override
        public void onAvatarRemoved() {
            groupDetailViewModel.setAvatarFile(null);
            groupDetailViewModel.setAvatarRemoved(true);
            avatarEditView.setDefaultGroupAvatar();
            updateFloatingActionButtonAndMenu();
        }
    };

    private static class SelectorInfo {
        public View view;
        public ContactModel contactModel;
        public ArrayList<Integer> optionsMap;
    }

    private final ContactSettingsListener contactSettingsListener = new ContactSettingsListener() {

        @Override
        public void onIsDefaultContactPictureColoredChanged(boolean isColored) {
            resumePauseHandler.runOnActive(RUN_ON_ACTIVE_RELOAD, runIfActiveUpdate);
        }

        @Override
        public void onShowContactDefinedAvatarsChanged(boolean shouldShow) {
            resumePauseHandler.runOnActive(RUN_ON_ACTIVE_RELOAD, runIfActiveUpdate);
        }
    };

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        logScreenVisibility(this, logger);

        if (!isSessionScopeReady()) {
            finish();
            return;
        }

        this.myIdentity = dependencies.getUserService().getIdentity();

        final ActionBar actionBar = getSupportActionBar();
        if (actionBar == null) {
            finish();
            return;
        }

        final @NonNull CollapsingToolbarLayout collapsingToolbar = findViewById(R.id.collapsing_toolbar);
        this.groupNameEditText = findViewById(R.id.group_title);
        final @Nullable ImageView avatarEditView = findViewById(R.id.avatar_edit);

        if (getAppBarLayout() != null) {
            getAppBarLayout().addOnOffsetChangedListener((view, verticalOffset) -> {
                float expandedPercent = 1f;
                final float offsetPixels = Math.abs(verticalOffset);
                final float offsetPixelsWhenCollapsed = (float) view.getTotalScrollRange();
                if (offsetPixelsWhenCollapsed > 0) {
                    expandedPercent = 1 - (offsetPixels / offsetPixelsWhenCollapsed);
                }

                // Fade out contents while collapsing
                groupNameEditText.setAlpha(expandedPercent);
                if (avatarEditView != null && avatarEditView.getVisibility() == View.VISIBLE) {
                    avatarEditView.setAlpha(expandedPercent);
                }

                // Show the group name as a toolbar title only of we are fully collapsed
                @NonNull String toolbarTitle = " ";
                if (groupDetailViewModel != null && expandedPercent == 0.0f) {
                    toolbarTitle = groupDetailViewModel.getGroupName();
                }
                collapsingToolbar.setTitle(toolbarTitle);
            });
        }

        this.resumePauseHandler = ResumePauseHandler.getByActivity(this, this);
        this.groupDetailViewModel = ViewModelCompat.getViewModel(this, GroupDetailViewModel.class);

        final MaterialToolbar toolbar = findViewById(R.id.toolbar);
        this.avatarEditView = findViewById(R.id.avatar_edit_view);

        this.floatingActionButton = findViewById(R.id.floating);
        RecyclerView groupDetailRecyclerView = findViewById(R.id.group_members_list);
        collapsingToolbar.setTitle(" ");

        groupId = getIntent().getLongExtra(AppConstants.INTENT_DATA_GROUP_DATABASE_ID, 0L);
        if (this.groupId == 0) {
            finish();
            return;
        }
        groupModel = dependencies.getGroupModelRepository().getByGroupDatabaseId(groupId);
        if (groupModel == null) {
            logger.warn("Group model couldn't be found");
            finish();
            return;
        }
        GroupModelData groupModelData = groupModel.getData();
        if (groupModelData == null) {
            logger.warn("Group model has been deleted");
            finish();
            return;
        }
        DisplayableGroupParticipants displayableGroupParticipants = DisplayableGroupParticipants.getDisplayableGroupParticipantsOfGroup(
            groupModel,
            dependencies.getContactModelRepository(),
            dependencies.getUserService(),
            dependencies.getPreferenceService()
        );
        if (displayableGroupParticipants == null) {
            logger.error("Cannot get displayable group participants as group seems to have been deleted in the meantime.");
            finish();
            return;
        }
        List<DisplayableGroupParticipant> displayableGroupParticipantList = new ArrayList<>(displayableGroupParticipants.getMembersWithoutCreator().size() + 1);
        displayableGroupParticipantList.add(displayableGroupParticipants.getCreator());
        displayableGroupParticipantList.addAll(displayableGroupParticipants.getMembersWithoutCreator());

        observeNewGroupModel();

        if (savedInstanceState == null) {
            // new instance
            this.groupDetailViewModel.setGroupParticipants(displayableGroupParticipantList);
            this.groupDetailViewModel.setGroupName(groupModelData.name);
            String groupDesc = groupModelData.groupDescription;
            if (groupDesc == null || groupDesc.isEmpty()) {
                this.groupDetailViewModel.setGroupDesc(null);
                this.groupDetailViewModel.setGroupDescState(NONE);
            } else {
                this.groupDetailViewModel.setGroupDesc(groupDesc);
                this.groupDetailViewModel.setGroupDescState(COLLAPSED);
            }
            this.groupDetailViewModel.setGroupDescTimestamp(groupModelData.groupDescriptionChangedAt);
        }

        this.avatarEditView.setHires(true);
        if (groupDetailViewModel.isAvatarRemoved()) {
            this.avatarEditView.setDefaultGroupAvatar();
        } else {
            if (groupDetailViewModel.getAvatarFile() != null) {
                this.avatarEditView.setAvatarFile(groupDetailViewModel.getAvatarFile());
            } else {
                this.avatarEditView.loadAvatarForGroupModel(groupModel);
            }
        }
        this.avatarEditView.setListener(this.avatarEditViewListener);

        ((AppBarLayout) findViewById(R.id.appbar)).addOnOffsetChangedListener((appBarLayout, verticalOffset) -> {
            if (verticalOffset == 0) {
                if (!floatingActionButton.isExtended()) {
                    floatingActionButton.extend();
                }
            } else if (floatingActionButton.isExtended()) {
                floatingActionButton.shrink();
            }
        });

        this.sortGroupMembers();
        setTitle();
        updateFloatingActionButtonAndMenu();

        if (groupModel.isCreator() && groupModel.isMember()) {
            operationMode = MODE_EDIT;
            actionBar.setHomeButtonEnabled(false);
            actionBar.setDisplayHomeAsUpEnabled(true);

            floatingActionButton.setOnClickListener(v -> {
                logger.info("FAB (save group settings) clicked");
                saveGroupSettings();
            });
            groupNameEditText.setMaxByteSize(GroupModelOld.GROUP_NAME_MAX_LENGTH_BYTES);
            groupNameEditText.addTextChangedListener(new SimpleTextWatcher() {
                @Override
                public void afterTextChanged(@NonNull Editable editable) {
                    updateFloatingActionButtonAndMenu();
                }
            });
        } else {
            operationMode = MODE_READONLY;
            actionBar.setHomeButtonEnabled(false);
            actionBar.setDisplayHomeAsUpEnabled(true);

            groupNameEditText.setFocusable(false);
            groupNameEditText.setClickable(false);
            groupNameEditText.setFocusableInTouchMode(false);
            groupNameEditText.setBackground(null);
            groupNameEditText.setPadding(0, 0, 0, 0);

            floatingActionButton.setVisibility(View.GONE);

            // If the user is not a member of the group, then display the group name with strike
            // through style
            if (!groupModel.isMember()) {
                // Get the paint flags and add the strike through flag
                int paintFlags = groupNameEditText.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG;
                groupNameEditText.setPaintFlags(paintFlags);
            }
        }

        groupDetailRecyclerView.setLayoutManager(new LinearLayoutManager(this));

        setupAdapter();

        Fragment dialogFragment = getSupportFragmentManager().findFragmentByTag(DIALOG_TAG_CHANGE_GROUP_DESC);
        if (dialogFragment instanceof GroupDescEditDialog) {
            GroupDescEditDialog dialog = (GroupDescEditDialog) dialogFragment;
            dialog.setCallback(newGroupDesc -> {
                hideKeyboard(); // is used for older devices
                onGroupDescChange(newGroupDesc);
            });
        }


        groupDetailRecyclerView.setAdapter(this.groupDetailAdapter);

        final Observer<List<DisplayableGroupParticipant>> groupMemberObserver = groupParticipants ->
            // Update the UI
            groupDetailAdapter.setGroupMembers(groupParticipants);

        // Observe the LiveData, passing in this activity as the LifecycleOwner and the observer.
        groupDetailViewModel.getGroupParticipants().observe(this, groupMemberObserver);
        groupDetailViewModel.onDataChanged();

        @ColorInt int color = dependencies.getGroupService().getGroupProfilePictureColor(groupModel);
        collapsingToolbar.setContentScrimColor(color);
        collapsingToolbar.setStatusBarScrimColor(color);

        updateFloatingActionButtonAndMenu();

        if (toolbar.getNavigationIcon() != null) {
            toolbar.getNavigationIcon().setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN);
        }

        ListenerManager.contactSettingsListeners.add(this.contactSettingsListener);
        FlowJavaCompat.collect(this, Lifecycle.State.CREATED, globalEventFlows.getContacts(), this::handleContactEvent);
        FlowJavaCompat.collect(this, Lifecycle.State.CREATED, globalEventFlows.getGroups(), this::handleGroupEvent);
    }

    @Override
    public void handleDeviceInsets() {
        // As the CollapsingToolbarLayout will consume the window insets internally, we have to apply the window insets to every our child views
        // with one inset listener from the root layout
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main_content), (view, windowInsets) -> {
            final Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());

            // Apply the window insets to our MaterialToolbar
            ViewExtensionsKt.setMargin(getToolbar(), insets.left, 0, insets.right, 0);

            // Apply horizontal insets to the group name edittext
            final int ownMarginHorizontal = getResources().getDimensionPixelSize(R.dimen.grid_unit_x2);
            final int ownMarginBottom = getResources().getDimensionPixelSize(R.dimen.grid_unit_x4);
            ViewExtensionsKt.setMargin(
                findViewById(R.id.group_title),
                insets.left + ownMarginHorizontal,
                0,
                insets.right + ownMarginHorizontal,
                ownMarginBottom
            );

            // Apply horizontal and bottom insets to the listview
            findViewById(R.id.group_members_list).setPadding(
                insets.left,
                0,
                insets.right,
                insets.bottom
            );

            // Place the floating action button
            final int ownMargin = getResources().getDimensionPixelSize(R.dimen.grid_unit_x2);
            ViewExtensionsKt.setMargin(
                findViewById(R.id.floating),
                insets.left + ownMargin,
                0,
                insets.right + ownMargin,
                insets.bottom + ownMargin
            );

            return windowInsets;
        });
    }

    private void setupAdapter() {
        this.groupDetailAdapter = new GroupDetailAdapter(
            this,
            this.groupModel,
            groupDetailViewModel,
            dependencies.getContactService()
        );

        this.groupDetailAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
            @Override
            public void onChanged() {
                super.onChanged();
                updateFloatingActionButtonAndMenu();
            }
        });
        this.groupDetailAdapter.setOnClickListener(this);
    }

    @UiThread
    private void handleContactEvent(@NonNull ContactEvent event) {
        if (
            (event instanceof ContactEvent.ContactUpdated || event instanceof ContactEvent.ContactProfilePictureUpdated) &&
                groupDetailViewModel.containsParticipant(event.getIdentityString())
        ) {
            resumePauseHandler.runOnActive(RUN_ON_ACTIVE_RELOAD, runIfActiveUpdate);
        }
    }

    @UiThread
    private void handleGroupEvent(@NonNull GroupEvent event) {
        if (event instanceof GroupEvent.MemberKicked) {
            if (((GroupEvent.MemberKicked) event).getIdentityString().equals(myIdentity)) {
                finish();
            } else {
                resumePauseHandler.runOnActive(RUN_ON_ACTIVE_RELOAD, runIfActiveUpdate);
            }
        } else if (event instanceof GroupEvent.MemberLeft) {
            if (((GroupEvent.MemberLeft) event).getIdentityString().equals(myIdentity)) {
                finish();
            } else {
                resumePauseHandler.runOnActive(RUN_ON_ACTIVE_RELOAD, runIfActiveUpdate);
            }
        } else if (
            event instanceof GroupEvent.NewMember ||
                event instanceof GroupEvent.NewGroup ||
                event instanceof GroupEvent.GroupRemoved ||
                event instanceof GroupEvent.GroupRenamed ||
                event instanceof GroupEvent.GroupProfilePictureUpdated
        ) {
            resumePauseHandler.runOnActive(RUN_ON_ACTIVE_RELOAD, runIfActiveUpdate);
        }
    }

    @Override
    public int getLayoutResource() {
        return R.layout.activity_group_detail;
    }

    private void setTitle() {
        this.groupNameEditText.setText(groupDetailViewModel.getGroupName());
    }

    private void launchContactDetail(View view, String identity) {
        if (!this.myIdentity.equals(identity)) {
            Intent intent = new Intent(GroupDetailActivity.this, ContactDetailActivity.class);
            intent.putExtra(AppConstants.INTENT_DATA_CONTACT, identity);
            intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
            ActivityOptionsCompat options = ActivityOptionsCompat.makeScaleUpAnimation(view, 0, 0, view.getWidth(), view.getHeight());
            ActivityCompat.startActivityForResult(this, intent, ThreemaActivity.ACTIVITY_ID_CONTACT_DETAIL, options.toBundle());
        }
    }

    private void sortGroupMembers() {
        List<DisplayableGroupParticipant> displayableGroupParticipants = groupDetailViewModel.getDisplayableGroupParticipants();
        displayableGroupParticipants.sort((o1, o2) -> {
            if (o1 instanceof DisplayableGroupParticipant.Creator) {
                return -1;
            } else if (o2 instanceof DisplayableGroupParticipant.Creator) {
                return 1;
            } else {
                return o1.getDisplayableContactOrUser().getDisplayName().compareTo(o2.getDisplayableContactOrUser().getDisplayName());
            }
        });
        groupDetailViewModel.setGroupParticipants(displayableGroupParticipants);
    }

    private void removeMemberFromGroup(@NonNull String identity) {
        this.groupDetailViewModel.removeGroupParticipant(identity);
        updateFloatingActionButtonAndMenu();
    }

    @Override
    public void onPause() {
        super.onPause();
        if (this.resumePauseHandler != null) {
            this.resumePauseHandler.onPause();
        }
    }

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

    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        MenuItem groupSyncMenu = menu.findItem(R.id.menu_resync);
        MenuItem leaveGroupMenu = menu.findItem(R.id.menu_leave_group);
        MenuItem dissolveGroupMenu = menu.findItem(R.id.menu_dissolve_group);
        MenuItem deleteGroupMenu = menu.findItem(R.id.menu_delete_group);
        MenuItem cloneMenu = menu.findItem(R.id.menu_clone_group);
        MenuItem mediaGalleryMenu = menu.findItem(R.id.menu_gallery);
        MenuItem groupCallMenu = menu.findItem(R.id.menu_group_call);

        if (dependencies.getAppRestrictions().isCreateGroupDisabled()) {
            cloneMenu.setVisible(false);
        }

        if (groupModel != null) {
            GroupCallDescription call = dependencies.getGroupCallManager().getCurrentChosenCall(groupModel);
            groupCallMenu.setVisible(GroupCallUtil.qualifiesForGroupCalls(groupModel) && !hasChanges() && call == null);

            boolean isMember = groupModel.isMember();
            boolean isCreator = groupModel.isCreator();
            GroupModelData groupModelData = groupModel.getData();
            boolean hasOtherMembers = groupModelData != null && !groupModel.getRecipients().isEmpty();

            // The clone menu only makes sense if at least one other member is present
            cloneMenu.setVisible(hasOtherMembers);

            // The leave option is only available for members
            leaveGroupMenu.setVisible(isMember && !isCreator);

            // The dissolve option is only available for the creator (if it is not yet dissolved)
            dissolveGroupMenu.setVisible(isMember && isCreator && hasOtherMembers);

            // The delete option is always available
            deleteGroupMenu.setVisible(true);

            // The group sync option is only available for the creator when other members are in the group
            groupSyncMenu.setVisible(isCreator && isMember && hasOtherMembers);

            mediaGalleryMenu.setVisible(
                !dependencies.getConversationCategoryService().isMarkedAsPrivate(
                    new GroupConversationId(groupModel.getDatabaseId())
                )
            );

            menu.findItem(R.id.action_send_message).setVisible(!hasChanges());
        }

        return super.onPrepareOptionsMenu(menu);
    }

    @SuppressLint("RestrictedApi")
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        getMenuInflater().inflate(R.menu.activity_group_detail, menu);

        try {
            MenuBuilder menuBuilder = (MenuBuilder) menu;
            menuBuilder.setOptionalIconsVisible(true);
        } catch (Exception ignored) {
        }

        return true;
    }

    @Override
    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
        int itemId = item.getItemId();
        if (itemId == android.R.id.home) {
            onBackPressed();
            return true;
        } else if (itemId == R.id.action_send_message) {
            if (groupModel != null) {
                Intent intent = ComposeMessageActivity.createIntent(
                    this,
                    new GroupConversationId(groupId),
                    null,
                    true
                );
                intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
                startActivity(intent);
                finish();
            }
        } else if (itemId == R.id.menu_resync) {
            this.syncGroup();
        } else if (itemId == R.id.menu_leave_group) {
            GenericAlertDialog.newInstance(
                R.string.action_leave_group,
                R.string.really_leave_group_message,
                R.string.ok,
                R.string.cancel
            ).show(getSupportFragmentManager(), DIALOG_TAG_LEAVE_GROUP);
        } else if (itemId == R.id.menu_dissolve_group) {
            GenericAlertDialog.newInstance(
                R.string.action_dissolve_group,
                getString(R.string.really_dissolve_group),
                R.string.ok,
                R.string.cancel
            ).show(getSupportFragmentManager(), DIALOG_TAG_DISSOLVE_GROUP);
        } else if (itemId == R.id.menu_delete_group) {
            @StringRes int title;
            @StringRes int description;
            boolean isGroupCreator = groupModel.isCreator();
            boolean isGroupMember = groupModel.isMember();
            if (isGroupCreator && isGroupMember) {
                // Group creator and still member
                title = R.string.action_dissolve_and_delete_group;
                description = R.string.delete_my_group_message;
            } else if (isGroupMember) {
                // Just a member
                title = R.string.action_leave_and_delete_group;
                description = R.string.delete_group_message;
            } else {
                // Not even a member anymore
                title = R.string.action_delete_group;
                description = R.string.delete_left_group_message;
            }

            GenericAlertDialog.newInstance(
                    title,
                    description,
                    R.string.ok,
                    R.string.cancel)
                .show(getSupportFragmentManager(), DIALOG_TAG_DELETE_GROUP);
        } else if (itemId == R.id.menu_clone_group) {
            GenericAlertDialog.newInstance(
                    R.string.action_clone_group,
                    R.string.clone_group_message,
                    R.string.yes,
                    R.string.no)
                .show(getSupportFragmentManager(), DIALOG_TAG_CLONE_GROUP_CONFIRM);
        } else if (itemId == R.id.menu_gallery) {
            if (groupId > 0 && !dependencies.getConversationCategoryService().isMarkedAsPrivate(new GroupConversationId(groupModel.getDatabaseId()))) {
                Intent mediaGalleryIntent = new Intent(this, MediaGalleryActivity.class);
                mediaGalleryIntent.putExtra(AppConstants.INTENT_DATA_GROUP_DATABASE_ID, groupId);
                startActivity(mediaGalleryIntent);
            }
        } else if (itemId == R.id.menu_group_call) {
            GroupModelOld oldGroupModel = dependencies.getGroupService().getById(groupModel.getDatabaseId());
            if (oldGroupModel != null) {
                GroupCallUtil.initiateCall(this, oldGroupModel);
            } else {
                logger.warn("Could not find old group model");
            }
        }
        return super.onOptionsItemSelected(item);
    }

    private void leaveGroupAndQuit() {
        leaveOrDeleteGroupAndQuit(GroupLeaveIntent.LEAVE);
    }

    private void dissolveGroupAndQuit() {
        disbandOrDeleteGroupAndQuit(GroupDisbandIntent.DISBAND);
    }

    private void deleteGroupAndQuit() {
        if (!groupModel.isMember()) {
            removeGroupAndQuit();
        } else if (groupModel.isCreator()) {
            disbandOrDeleteGroupAndQuit(GroupDisbandIntent.DISBAND_AND_REMOVE);
        } else {
            leaveOrDeleteGroupAndQuit(GroupLeaveIntent.LEAVE_AND_REMOVE);
        }
    }

    private void leaveOrDeleteGroupAndQuit(GroupLeaveIntent intent) {
        if (groupModel == null) {
            logger.error("Could not leave or delete group: group model missing");
            SimpleStringAlertDialog
                .newInstance(R.string.error, R.string.error_leaving_group_internal)
                .show(getSupportFragmentManager());
            return;
        }

        final @NonNull LoadingWithTimeoutDialogXml loadingDialog = LoadingWithTimeoutDialogXml.newInstance(
            GROUP_FLOWS_LOADING_DIALOG_TIMEOUT_SECONDS,
            R.string.leaving_group
        );
        loadingDialog.show(getSupportFragmentManager());

        Deferred<GroupFlowResult> leaveGroupFlowResultDeferred = dependencies.getGroupFlowDispatcher()
            .runLeaveGroupFlow(intent, groupModel);

        CoroutinesExtensionKt.onCompleted(
            leaveGroupFlowResultDeferred,
            exception -> {
                logger.error("leave-group-flow was completed exceptionally", exception);
                onLeaveGroupFailed(GroupFlowResult.Failure.Other.INSTANCE, loadingDialog);
                return Unit.INSTANCE;
            },
            result -> {
                if (result instanceof GroupFlowResult.Success) {
                    onLeaveGroupSuccess(loadingDialog);
                } else if (result instanceof GroupFlowResult.Failure) {
                    onLeaveGroupFailed((GroupFlowResult.Failure) result, loadingDialog);
                }
                return Unit.INSTANCE;
            }
        );
    }

    @AnyThread
    private void onLeaveGroupSuccess(@NonNull LoadingWithTimeoutDialogXml loadingDialog) {
        RuntimeUtil.runOnUiThread(() -> {
            loadingDialog.dismiss();
            navigateHome();
        });
    }

    @AnyThread
    private void onLeaveGroupFailed(
        @NonNull GroupFlowResult.Failure failureResult,
        @NonNull LoadingWithTimeoutDialogXml loadingDialog
    ) {
        RuntimeUtil.runOnUiThread(() -> {
            loadingDialog.dismiss();
            final @StringRes int errorMessageRes;
            if (failureResult instanceof GroupFlowResult.Failure.Network) {
                errorMessageRes = R.string.error_leaving_group_network;
            } else {
                errorMessageRes = R.string.error_leaving_group_internal;
            }
            SimpleStringAlertDialog
                .newInstance(R.string.error, errorMessageRes)
                .show(getSupportFragmentManager());
        });
    }

    private void disbandOrDeleteGroupAndQuit(GroupDisbandIntent intent) {
        if (groupModel == null) {
            logger.error("Could not disband or delete group: group model missing");
            SimpleStringAlertDialog
                .newInstance(R.string.error, R.string.error_disbanding_group_internal)
                .show(getSupportFragmentManager());
            return;
        }

        final @NonNull LoadingWithTimeoutDialogXml loadingDialog = LoadingWithTimeoutDialogXml.newInstance(
            GROUP_FLOWS_LOADING_DIALOG_TIMEOUT_SECONDS,
            R.string.disbanding_group
        );
        loadingDialog.show(getSupportFragmentManager());

        Deferred<GroupFlowResult> disbandGroupFlowResultDeferred = dependencies.getGroupFlowDispatcher()
            .runDisbandGroupFlow(intent, groupModel);

        CoroutinesExtensionKt.onCompleted(
            disbandGroupFlowResultDeferred,
            exception -> {
                logger.error("disband-group-flow was completed exceptionally", exception);
                onDisbandGroupFailed(GroupFlowResult.Failure.Other.INSTANCE, loadingDialog);
                return Unit.INSTANCE;
            },
            result -> {
                if (result instanceof GroupFlowResult.Success) {
                    RuntimeUtil.runOnUiThread(this::finish);
                } else if (result instanceof GroupFlowResult.Failure) {
                    onDisbandGroupFailed((GroupFlowResult.Failure) result, loadingDialog);
                }
                return Unit.INSTANCE;
            }
        );
    }

    @AnyThread
    private void onDisbandGroupFailed(
        @NonNull GroupFlowResult.Failure failureResult,
        @NonNull LoadingWithTimeoutDialogXml loadingDialog
    ) {
        RuntimeUtil.runOnUiThread(() -> {
            loadingDialog.dismiss();
            final @StringRes int errorMessageRes;
            if (failureResult instanceof GroupFlowResult.Failure.Network) {
                errorMessageRes = R.string.error_disbanding_group_network;
            } else {
                errorMessageRes = R.string.error_disbanding_group_internal;
            }
            SimpleStringAlertDialog
                .newInstance(R.string.error, errorMessageRes)
                .show(getSupportFragmentManager());
        });
    }

    private void removeGroupAndQuit() {
        if (groupModel == null) {
            logger.error("Cannot remove group: group model is null");
            SimpleStringAlertDialog
                .newInstance(R.string.error, R.string.error_removing_group_internal)
                .show(getSupportFragmentManager());
            return;
        }

        final @NonNull LoadingWithTimeoutDialogXml loadingDialog = LoadingWithTimeoutDialogXml.newInstance(
            GROUP_FLOWS_LOADING_DIALOG_TIMEOUT_SECONDS,
            R.string.removing_group
        );
        loadingDialog.show(getSupportFragmentManager());

        Deferred<GroupFlowResult> removeGroupFlowResultDeferred = dependencies.getGroupFlowDispatcher()
            .runRemoveGroupFlow(groupModel);

        CoroutinesExtensionKt.onCompleted(
            removeGroupFlowResultDeferred,
            exception -> {
                logger.error("remove-group-flow was completed exceptionally", exception);
                RuntimeUtil.runOnUiThread(this::finish);
                return Unit.INSTANCE;
            },
            groupFlowResult -> {
                if (groupFlowResult instanceof GroupFlowResult.Success) {
                    RuntimeUtil.runOnUiThread(this::finish);
                } else if (groupFlowResult instanceof GroupFlowResult.Failure) {
                    onRemoveGroupFailed(
                        (GroupFlowResult.Failure) groupFlowResult,
                        loadingDialog
                    );
                }
                return Unit.INSTANCE;
            }
        );
    }

    @AnyThread
    private void onRemoveGroupFailed(
        @NonNull GroupFlowResult.Failure removeGroupFlowResultFailure,
        @NonNull LoadingWithTimeoutDialogXml loadingDialog
    ) {
        RuntimeUtil.runOnUiThread(() -> {
            loadingDialog.dismiss();
            final @StringRes int errorMessageRes;
            if (removeGroupFlowResultFailure instanceof GroupFlowResult.Failure.Network) {
                errorMessageRes = R.string.error_removing_group_network;
            } else {
                errorMessageRes = R.string.error_removing_group_internal;
            }
            SimpleStringAlertDialog
                .newInstance(R.string.error, errorMessageRes)
                .show(getSupportFragmentManager());
        });
    }

    private void cloneGroup(final String newGroupName) {
        final @NonNull LoadingWithTimeoutDialogXml loadingDialog = LoadingWithTimeoutDialogXml.newInstance(
            GROUP_FLOWS_LOADING_DIALOG_TIMEOUT_SECONDS,
            R.string.creating_group
        );
        loadingDialog.show(getSupportFragmentManager(), null);

        GroupModelOld oldGroupModel = dependencies.getGroupService().getById(groupModel.getDatabaseId());
        if (oldGroupModel == null) {
            logger.error("Old group model is null");
            return;
        }

        Deferred<GroupFlowResult> cloneGroupFlowResultDeferred = dependencies.getGroupFlowDispatcher().runCreateGroupFlow(
            dependencies.getAppRestrictions(),
            new GroupCreateProperties(
                newGroupName,
                CheckedProfilePicture.getOrConvertFromBitmap(dependencies.getGroupService().getAvatar(oldGroupModel, true, false)),
                Set.of(dependencies.getGroupService().getGroupMemberIdentities(oldGroupModel))
            )
        );

        CoroutinesExtensionKt.onCompleted(
            cloneGroupFlowResultDeferred,
            exception -> {
                logger.error("The create-group-flow failed exceptionally", exception);
                onGroupCloneFailed(
                    GroupFlowResult.Failure.Other.INSTANCE,
                    loadingDialog
                );
                return Unit.INSTANCE;
            },
            groupFlowResult -> {
                if (groupFlowResult instanceof GroupFlowResult.Success) {
                    onGroupClonedSuccessfully(
                        ((GroupFlowResult.Success) groupFlowResult).getGroupModel(),
                        loadingDialog
                    );
                } else if (groupFlowResult instanceof GroupFlowResult.Failure) {
                    onGroupCloneFailed(
                        (GroupFlowResult.Failure) groupFlowResult,
                        loadingDialog
                    );
                }
                return Unit.INSTANCE;
            }
        );
    }

    @AnyThread
    private void onGroupClonedSuccessfully(
        @NonNull GroupModel groupModel,
        @NonNull LoadingWithTimeoutDialogXml loadingDialog
    ) {
        RuntimeUtil.runOnUiThread(() -> {
            loadingDialog.dismiss();
            Intent intent = ComposeMessageActivity.createIntent(
                GroupDetailActivity.this,
                new GroupConversationId(groupModel.getDatabaseId())
            );
            intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
            startActivity(intent);
            finish();
        });
    }

    @AnyThread
    private void onGroupCloneFailed(
        @NonNull GroupFlowResult.Failure createGroupFlowResultFailure,
        @NonNull LoadingWithTimeoutDialogXml loadingDialog
    ) {
        RuntimeUtil.runOnUiThread(() -> {
            loadingDialog.dismiss();
            final @StringRes int errorMessageRes;
            if (createGroupFlowResultFailure instanceof GroupFlowResult.Failure.Network) {
                errorMessageRes = R.string.error_cloning_group_network;
            } else {
                errorMessageRes = R.string.error_cloning_group_internal;
            }
            SimpleStringAlertDialog
                .newInstance(R.string.error, errorMessageRes)
                .show(getSupportFragmentManager());
        });
    }

    private void showConversation(String identity) {
        Intent intent = ComposeMessageActivity.createIntent(
            this,
            new ContactConversationId(identity)
        );
        intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        startActivity(intent);
    }

    private void addNewMembers() {
        Intent intent = new Intent(GroupDetailActivity.this, GroupAddActivity.class);
        IntentDataUtil.append(groupModel, intent);
        List<DisplayableGroupParticipant> groupMembers = groupDetailViewModel.getDisplayableGroupParticipants();
        List<ContactModel> contactModels = new ArrayList<>(groupMembers.size());
        ContactService contactService = dependencies.getContactService();
        for (DisplayableGroupParticipant displayableGroupMember : groupMembers) {
            ContactModel contactModel = contactService.getByIdentity(displayableGroupMember.getDisplayableContactOrUser().getIdentity());
            if (contactModel != null) {
                contactModels.add(contactModel);
            }
        }
        IntentDataUtil.append(contactModels, intent);
        startActivityForResult(intent, ThreemaActivity.ACTIVITY_ID_GROUP_ADD);
    }

    private void syncGroup() {
        if (groupModel == null) {
            logger.error("Failed to resync group: New group model is null");
            SimpleStringAlertDialog
                .newInstance(R.string.error, R.string.error_resyncing_group_internal)
                .show(getSupportFragmentManager());
            return;
        }

        Deferred<GroupFlowResult> resyncGroupFlowResultDeferred = dependencies.getGroupFlowDispatcher()
            .runGroupResyncFlow(groupModel);

        final @NonNull LoadingWithTimeoutDialogXml loadingDialog = LoadingWithTimeoutDialogXml.newInstance(
            GROUP_FLOWS_LOADING_DIALOG_TIMEOUT_SECONDS,
            R.string.resyncing_group
        );
        loadingDialog.show(getSupportFragmentManager());

        CoroutinesExtensionKt.onCompleted(
            resyncGroupFlowResultDeferred,
            exception -> {
                logger.error("resync-group-flow was completed exceptionally", exception);
                onResyncGroupFailed(GroupFlowResult.Failure.Other.INSTANCE, loadingDialog);
                return Unit.INSTANCE;
            },
            result -> {
                if (result instanceof GroupFlowResult.Success) {
                    RuntimeUtil.runOnUiThread(() -> {
                        loadingDialog.dismiss();
                        Toast.makeText(GroupDetailActivity.this, R.string.group_was_synchronized, Toast.LENGTH_SHORT).show();
                    });
                } else if (result instanceof GroupFlowResult.Failure) {
                    onResyncGroupFailed((GroupFlowResult.Failure) result, loadingDialog);
                }
                return Unit.INSTANCE;
            }
        );
    }

    @AnyThread
    private void onResyncGroupFailed(
        @NonNull GroupFlowResult.Failure failureResult,
        @NonNull LoadingWithTimeoutDialogXml loadingDialog
    ) {
        RuntimeUtil.runOnUiThread(() -> {
            loadingDialog.dismiss();
            final @StringRes int errorMessageRes;
            if (failureResult instanceof GroupFlowResult.Failure.Network) {
                errorMessageRes = R.string.error_resyncing_group_network;
            } else {
                errorMessageRes = R.string.error_resyncing_group_internal;
            }
            SimpleStringAlertDialog
                .newInstance(R.string.error, errorMessageRes)
                .show(getSupportFragmentManager());
        });
    }

    private void saveGroupSettings() {
        GroupModelData groupModelData = groupModel.getData();
        if (groupModelData == null) {
            logger.warn("Group model data is null");
            showGroupUpdateErrorInternal();
            return;
        }

        if (groupNameEditText.getText() != null) {
            this.groupDetailViewModel.setGroupName(groupNameEditText.getText().toString());
        } else {
            this.groupDetailViewModel.setGroupName("");
        }

        Set<String> updatedIdentities = new HashSet<>();
        Collections.addAll(updatedIdentities, groupDetailViewModel.getGroupIdentities());
        updatedIdentities.remove(myIdentity);

        GroupChanges groupChanges = new GroupChanges(
            getGroupNameChange(),
            getProfilePictureChange(groupModel),
            updatedIdentities,
            groupModelData
        );

        final @NonNull LoadingWithTimeoutDialogXml loadingDialog = LoadingWithTimeoutDialogXml.newInstance(
            GROUP_FLOWS_LOADING_DIALOG_TIMEOUT_SECONDS,
            R.string.updating_group
        );
        loadingDialog.show(getSupportFragmentManager());

        Deferred<GroupFlowResult> updateResultDeferred = dependencies.getGroupFlowDispatcher().runUpdateGroupFlow(groupChanges, groupModel);

        CoroutinesExtensionKt.onCompleted(
            updateResultDeferred,
            exception -> {
                logger.error("The update-group-flow failed exceptionally", exception);
                onGroupUpdateFailed(
                    GroupFlowResult.Failure.Other.INSTANCE,
                    loadingDialog
                );
                return Unit.INSTANCE;
            },
            groupFlowResult -> {
                if (groupFlowResult instanceof GroupFlowResult.Success) {
                    onGroupUpdateSucceeded(loadingDialog);
                } else if (groupFlowResult instanceof GroupFlowResult.Failure) {
                    onGroupUpdateFailed(
                        (GroupFlowResult.Failure) groupFlowResult,
                        loadingDialog
                    );
                }
                return Unit.INSTANCE;
            }
        );
    }

    @AnyThread
    private void onGroupUpdateSucceeded(@NonNull LoadingWithTimeoutDialogXml loadingDialog) {
        RuntimeUtil.runOnUiThread(() -> {
            loadingDialog.dismiss();
            finish();
        });
    }

    @AnyThread
    private void onGroupUpdateFailed(
        @NonNull GroupFlowResult.Failure resultFailed,
        @NonNull LoadingWithTimeoutDialogXml loadingDialog
    ) {
        RuntimeUtil.runOnUiThread(() -> {
            loadingDialog.dismiss();
            final @StringRes int errorMessageRes;
            if (resultFailed instanceof GroupFlowResult.Failure.Network) {
                errorMessageRes = R.string.error_updating_group_network;
            } else {
                errorMessageRes = R.string.error_updating_group_internal;
            }
            SimpleStringAlertDialog
                .newInstance(R.string.error, errorMessageRes)
                .show(getSupportFragmentManager());
        });
    }

    @UiThread
    private void showGroupUpdateErrorInternal() {
        SimpleStringAlertDialog
            .newInstance(R.string.error, R.string.error_updating_group_internal)
            .show(getSupportFragmentManager());
    }

    @Nullable
    private String getGroupNameChange() {
        @NonNull String newGroupName = groupDetailViewModel.getGroupName();
        GroupModelData groupModelData = groupModel.getData();
        @NonNull String oldGroupName = groupModelData != null ?
            (groupModelData.name != null ? groupModelData.name : "") : "";

        if (!newGroupName.equals(oldGroupName)) {
            return newGroupName;
        } else {
            return null;
        }
    }

    @NonNull
    private ProfilePictureChange getProfilePictureChange(GroupModel groupModel) {
        if (!groupDetailViewModel.hasAvatarChanges()) {
            return ProfilePictureChange.NoChange.INSTANCE;
        }

        CheckedProfilePicture profilePicture = CheckedProfilePicture.getOrConvertFromFile(groupDetailViewModel.getAvatarFile());
        byte[] newAvatarBytes = profilePicture != null ? profilePicture.getBytes() : null;

        byte[] oldAvatarBytes;
        try {
            oldAvatarBytes = dependencies.getFileService().getGroupProfilePictureBytes(groupModel.getDatabaseId());
        } catch (Exception e) {
            logger.error("Could not get group avatar", e);
            oldAvatarBytes = null;
        }

        if (Arrays.equals(newAvatarBytes, oldAvatarBytes)) {
            return ProfilePictureChange.NoChange.INSTANCE;
        } else if (newAvatarBytes != null) {
            return new ProfilePictureChange.Set(profilePicture);
        } else {
            return ProfilePictureChange.Remove.INSTANCE;
        }
    }

    private void showCloneDialog() {
        GroupModelData groupModelData = groupModel.getData();
        if (groupModelData == null) {
            logger.error("Group model data is null");
            return;
        }

        TextEntryDialog.newInstance(
            R.string.action_clone_group,
            R.string.name,
            R.string.ok,
            R.string.cancel,
            groupModelData.name,
            0,
            0
        ).show(getSupportFragmentManager(), DIALOG_TAG_CLONE_GROUP);
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (resultCode == Activity.RESULT_OK) {
            if (requestCode == ThreemaActivity.ACTIVITY_ID_GROUP_ADD) {
                // some users were added
                var newContactIdentities = IntentDataUtil.getContactIdentities(data);
                if (newContactIdentities != null) {
                    groupDetailViewModel.addGroupContacts(newContactIdentities);
                }
                sortGroupMembers();
            } else {
                if (this.avatarEditView != null) {
                    this.avatarEditView.onActivityResult(requestCode, resultCode, data);
                }
                super.onActivityResult(requestCode, resultCode, data);
            }
        }

        if (requestCode == ThreemaActivity.ACTIVITY_ID_CONTACT_DETAIL) {
            // contacts may have been edited
            sortGroupMembers();
        }
    }

    @Override
    public void onDestroy() {
        ListenerManager.contactSettingsListeners.remove(this.contactSettingsListener);

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

        super.onDestroy();
    }

    @Override
    public void onClick(String tag, int which, Object object) {
        SelectorInfo selectorInfo = (SelectorInfo) object;

        // click on selector
        if (selectorInfo.contactModel != null) {
            switch (selectorInfo.optionsMap.get(which)) {
                case SELECTOR_OPTION_CONTACT_DETAIL:
                    logger.info("Contact details button clicked");
                    launchContactDetail(selectorInfo.view, selectorInfo.contactModel.getIdentity());
                    break;
                case SELECTOR_OPTION_CHAT:
                    logger.info("Chat button clicked");
                    showConversation(selectorInfo.contactModel.getIdentity());
                    finish();
                    break;
                case SELECTOR_OPTION_REMOVE:
                    logger.info("Kick user button clicked");
                    if (selectorInfo.contactModel != null) {
                        removeMemberFromGroup(selectorInfo.contactModel.getIdentity());
                    }
                    break;
                case SELECTOR_OPTION_CALL:
                    logger.info("Call button clicked");
                    VoipUtil.initiateCall(this, selectorInfo.contactModel, false, null);
                    break;
                default:
                    break;
            }
        }
    }

    @Override
    public void onYes(@NonNull String tag, @NonNull String text) {
        // text entry dialog
        if (tag.equals(DIALOG_TAG_CLONE_GROUP)) {
            logger.info("Clone group dialog confirmed");
            cloneGroup(text);
        }
    }

    public void onGroupDescChange(@NonNull String newGroupDesc) {
        GroupModelData groupModelData = groupModel.getData();
        if (groupModelData == null) {
            return;
        }

        if (newGroupDesc.equals(groupModelData.groupDescription)) {
            return;
        }
        groupDetailViewModel.setGroupDescTimestamp(Instant.now());


        // delete group description
        if (newGroupDesc.isEmpty()) {
            removeGroupDescription();
            return;
        }

        // create or update description
        groupDetailViewModel.setGroupDesc(newGroupDesc);
        if (groupDetailViewModel.getGroupDescState() == NONE) {
            groupDetailViewModel.setGroupDescState(COLLAPSED);
        }
        groupDetailAdapter.updateGroupDescriptionLayout();
    }


    @Override
    public void onNo(String tag) {
        // do nothing
    }

    @Override
    public void onYes(@Nullable String tag, @Nullable Object data) {
        if (tag == null) {
            return;
        }
        switch (tag) {
            case DIALOG_TAG_LEAVE_GROUP:
                logger.info("Leave group dialog confirmed");
                leaveGroupAndQuit();
                break;
            case DIALOG_TAG_DISSOLVE_GROUP:
                logger.info("Dissolve group dialog confirmed");
                dissolveGroupAndQuit();
                break;
            case DIALOG_TAG_DELETE_GROUP:
                logger.info("Delete group dialog confirmed");
                deleteGroupAndQuit();
                break;
            case DIALOG_TAG_QUIT:
                logger.info("Save group changes dialog confirmed");
                saveGroupSettings();
                break;
            case DIALOG_TAG_CLONE_GROUP_CONFIRM:
                logger.info("Clone group info dialog confirmed");
                showCloneDialog();
                break;
            default:
                break;
        }
    }

    @Override
    protected boolean enableOnBackPressedCallback() {
        return true;
    }

    @Override
    public void handleOnBackPressed() {
        if (this.operationMode == MODE_EDIT && hasChanges()) {
            logger.info("Showing warning about unsaved changes");
            GenericAlertDialog.newInstance(
                    R.string.leave,
                    R.string.save_group_changes,
                    R.string.yes,
                    R.string.no,
                    R.string.cancel,
                    0)
                .show(getSupportFragmentManager(), DIALOG_TAG_QUIT);
        } else {
            finish();
        }
    }

    @Override
    public void onNo(@Nullable String tag, @Nullable Object data) {
        if (tag != null && tag.equals(DIALOG_TAG_QUIT)) {
            finish();
        }
    }

    private boolean hasChanges() {
        return groupDetailViewModel.hasMemberChanges() || hasGroupNameChanges() || groupDetailViewModel.hasAvatarChanges();
    }

    private boolean hasGroupNameChanges() {
        Editable groupNameEditable = groupNameEditText.getText();
        String editedGroupNameText = groupNameEditable != null ? groupNameEditable.toString() : "";

        GroupModelData groupModelData = groupModel.getData();
        if (groupModelData == null) {
            return false;
        }

        String currentGroupName = groupModelData.name != null ? groupModelData.name : "";

        return !editedGroupNameText.equals(currentGroupName);
    }

    private void updateFloatingActionButtonAndMenu() {
        if (this.groupDetailAdapter == null) {
            logger.debug("Required instances not available");
            return;
        }

        if (this.floatingActionButton == null) {
            return;
        }

        if (groupModel.isCreator() && hasChanges()) {
            this.floatingActionButton.show();
        } else {
            this.floatingActionButton.hide();
        }
        invalidateOptionsMenu();
    }

    private void navigateHome() {
        Intent intent = HomeActivity.createIntent(GroupDetailActivity.this);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
        startActivity(intent);
        ActivityCompat.finishAffinity(GroupDetailActivity.this);
        overridePendingTransition(0, 0);
    }

    /* callbacks from MyAvatarView */
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (this.avatarEditView != null) {
            this.avatarEditView.onRequestPermissionsResult(requestCode, permissions, grantResults);
        }
    }

    @Override
    public void onGroupMemberClick(View v, @NonNull String identity) {
        logger.info("Group member clicked");
        ContactModel contactModel = dependencies.getContactService().getByIdentity(identity);
        if (contactModel == null) {
            logger.error("Could not find contact model for the clicked identity");
            return;
        }
        String shortName = NameUtil.getShortName(contactModel, dependencies.getPreferenceService().getContactNameFormat());

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

        items.add(new SelectorDialogItem(getString(R.string.show_contact), R.drawable.ic_visibility));
        optionsMap.add(SELECTOR_OPTION_CONTACT_DETAIL);

        if (!areEqual(myIdentity, identity)) {
            items.add(new SelectorDialogItem(String.format(getString(R.string.chat_with), shortName), R.drawable.ic_chat_bubble));
            optionsMap.add(SELECTOR_OPTION_CHAT);

            if (ContactUtil.canReceiveVoipMessages(contactModel, dependencies.getBlockedIdentitiesService())
                && ConfigUtils.isCallsEnabled()
            ) {
                items.add(new SelectorDialogItem(String.format(getString(R.string.call_with), shortName), R.drawable.ic_phone_locked_outline));
                optionsMap.add(SELECTOR_OPTION_CALL);
            }

            if (operationMode == MODE_EDIT) {
                if (groupModel != null && !groupModel.getGroupIdentity().getCreatorIdentity().equals(identity)) {
                    items.add(new SelectorDialogItem(String.format(getString(R.string.kick_user_from_group), shortName), R.drawable.ic_person_remove_outline));
                    optionsMap.add(SELECTOR_OPTION_REMOVE);
                }
            }
            SelectorDialog selectorDialog = SelectorDialog.newInstance(null, items, null, (String) null);
            SelectorInfo selectorInfo = new SelectorInfo();
            selectorInfo.contactModel = contactModel;
            selectorInfo.view = v;
            selectorInfo.optionsMap = optionsMap;
            selectorDialog.setData(selectorInfo);
            try {
                selectorDialog.show(getSupportFragmentManager(), DIALOG_TAG_CHOOSE_ACTION);
            } catch (IllegalStateException e) {
                logger.error("Exception", e);
            }
        }
    }

    @Override
    public void onGroupDescriptionEditClick() {
        logger.info("Edit description button clicked");
        showGroupDescEditDialog();
    }

    @Override
    public void onAddMembersClick(View v) {
        logger.info("Add member button clicked");
        addNewMembers();
    }

    // hide keyboard on older devices after ok clicked when group description changed
    public void hideKeyboard() {
        getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
    }

    public void showGroupDescEditDialog() {
        String groupDescText = groupDetailViewModel.getGroupDesc();
        GroupDescEditDialog descriptionDialog = GroupDescEditDialog.newGroupDescriptionInstance(R.string.change_group_description, groupDescText, newGroupDesc -> {
            hideKeyboard(); // is used for older devices
            onGroupDescChange(newGroupDesc);
        });
        descriptionDialog.show(getSupportFragmentManager(), DIALOG_TAG_CHANGE_GROUP_DESC);
    }

    private void removeGroupDescription() {
        groupDetailViewModel.setGroupDesc(null);
        groupDetailViewModel.setGroupDescState(NONE);
        groupDetailAdapter.updateGroupDescriptionLayout();
    }

    private void observeNewGroupModel() {
        LiveData<GroupModelData> groupModelDataLiveData = groupDetailViewModel.getGroup();
        if (groupModelDataLiveData == null) {
            GroupModel newGroupModel =
                dependencies.getGroupModelRepository().getByGroupDatabaseId(this.groupId);

            if (newGroupModel == null) {
                logger.error("Group model is null");
                finish();
                return;
            }

            groupDetailViewModel.setGroup(newGroupModel);
            groupModelDataLiveData = groupDetailViewModel.getGroup();
            if (groupModelDataLiveData == null) {
                logger.error("Live data is null");
                finish();
                return;
            }
        }

        groupModelDataLiveData.observe(
            this,
            this::onGroupModelDataUpdate
        );
    }

    /**
     * Updates the view with the new provided data. If the data is null, the activity is finished.
     * Note that currently only the group name is updated.
     */
    private void onGroupModelDataUpdate(@Nullable GroupModelData data) {
        logger.debug("New group model data observed: {}", data);
        if (data == null) {
            finish();
            return;
        }

        // Set new group name
        groupDetailViewModel.setGroupName(data.name);
        setTitle();
    }

}
