com.ruesga.rview.fragments.ChangeDetailsFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.ruesga.rview.fragments.ChangeDetailsFragment.java

Source

/*
 * Copyright (C) 2016 Jorge Ruesga
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.ruesga.rview.fragments;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.databinding.DataBindingUtil;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.Keep;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;
import android.support.v4.widget.NestedScrollView;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.ListPopupWindow;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import com.google.gson.reflect.TypeToken;
import com.ruesga.rview.BaseActivity;
import com.ruesga.rview.R;
import com.ruesga.rview.adapters.PatchSetsAdapter;
import com.ruesga.rview.databinding.ChangeDetailsFragmentBinding;
import com.ruesga.rview.databinding.FileInfoItemBinding;
import com.ruesga.rview.databinding.MessageItemBinding;
import com.ruesga.rview.databinding.TotalAddedDeletedBinding;
import com.ruesga.rview.exceptions.OperationFailedException;
import com.ruesga.rview.gerrit.GerritApi;
import com.ruesga.rview.gerrit.filter.ChangeQuery;
import com.ruesga.rview.gerrit.model.AbandonInput;
import com.ruesga.rview.gerrit.model.AccountInfo;
import com.ruesga.rview.gerrit.model.ActionInfo;
import com.ruesga.rview.gerrit.model.AddReviewerResultInfo;
import com.ruesga.rview.gerrit.model.AddReviewerState;
import com.ruesga.rview.gerrit.model.ApprovalInfo;
import com.ruesga.rview.gerrit.model.AssigneeInfo;
import com.ruesga.rview.gerrit.model.AssigneeInput;
import com.ruesga.rview.gerrit.model.ChangeEditMessageInput;
import com.ruesga.rview.gerrit.model.ChangeInfo;
import com.ruesga.rview.gerrit.model.ChangeInput;
import com.ruesga.rview.gerrit.model.ChangeMessageInfo;
import com.ruesga.rview.gerrit.model.ChangeOptions;
import com.ruesga.rview.gerrit.model.ChangeStatus;
import com.ruesga.rview.gerrit.model.CherryPickInput;
import com.ruesga.rview.gerrit.model.CommentInfo;
import com.ruesga.rview.gerrit.model.ConfigInfo;
import com.ruesga.rview.gerrit.model.DeleteVoteInput;
import com.ruesga.rview.gerrit.model.DescriptionInput;
import com.ruesga.rview.gerrit.model.DraftActionType;
import com.ruesga.rview.gerrit.model.Features;
import com.ruesga.rview.gerrit.model.FileInfo;
import com.ruesga.rview.gerrit.model.HashtagsInput;
import com.ruesga.rview.gerrit.model.InitialChangeStatus;
import com.ruesga.rview.gerrit.model.MoveInput;
import com.ruesga.rview.gerrit.model.NotifyType;
import com.ruesga.rview.gerrit.model.RebaseInput;
import com.ruesga.rview.gerrit.model.RestoreInput;
import com.ruesga.rview.gerrit.model.RevertInput;
import com.ruesga.rview.gerrit.model.ReviewInfo;
import com.ruesga.rview.gerrit.model.ReviewInput;
import com.ruesga.rview.gerrit.model.ReviewerInput;
import com.ruesga.rview.gerrit.model.ReviewerStatus;
import com.ruesga.rview.gerrit.model.RevisionInfo;
import com.ruesga.rview.gerrit.model.SideType;
import com.ruesga.rview.gerrit.model.SubmitInput;
import com.ruesga.rview.gerrit.model.SubmitType;
import com.ruesga.rview.gerrit.model.TopicInput;
import com.ruesga.rview.misc.ActivityHelper;
import com.ruesga.rview.misc.AndroidHelper;
import com.ruesga.rview.misc.CacheHelper;
import com.ruesga.rview.misc.ExceptionHelper;
import com.ruesga.rview.misc.ModelHelper;
import com.ruesga.rview.misc.PicassoHelper;
import com.ruesga.rview.misc.SerializationManager;
import com.ruesga.rview.misc.StringHelper;
import com.ruesga.rview.model.Account;
import com.ruesga.rview.model.EmptyState;
import com.ruesga.rview.model.Repository;
import com.ruesga.rview.preferences.Constants;
import com.ruesga.rview.preferences.Preferences;
import com.ruesga.rview.widget.AccountChipView.OnAccountChipClickedListener;
import com.ruesga.rview.widget.AccountChipView.OnAccountChipRemovedListener;
import com.ruesga.rview.widget.DividerItemDecoration;
import com.ruesga.rview.widget.LinesWithCommentsView.OnLineClickListener;
import com.ruesga.rview.widget.TagEditTextView.Tag;
import com.squareup.picasso.Picasso;

import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;

import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import me.tatarka.rxloader2.RxLoader;
import me.tatarka.rxloader2.RxLoader1;
import me.tatarka.rxloader2.RxLoader2;
import me.tatarka.rxloader2.RxLoaderManager;
import me.tatarka.rxloader2.RxLoaderManagerCompat;
import me.tatarka.rxloader2.RxLoaderObserver;
import me.tatarka.rxloader2.safe.Empty;
import me.tatarka.rxloader2.safe.SafeObservable;

public class ChangeDetailsFragment extends Fragment
        implements AddReviewerDialogFragment.OnReviewerAdded, EditAssigneeDialogFragment.OnAssigneeSelected,
        FilterableDialogFragment.OnFilterSelectedListener, EditDialogFragment.OnEditChanged,
        ConfirmDialogFragment.OnActionConfirmed, TagEditDialogFragment.OnTagEditChanged {

    private static final String TAG = "ChangeDetailsFragment";

    private static final List<ChangeOptions> OPTIONS = new ArrayList<ChangeOptions>() {
        {
            add(ChangeOptions.DETAILED_ACCOUNTS);
            add(ChangeOptions.DETAILED_LABELS);
            add(ChangeOptions.ALL_REVISIONS);
            add(ChangeOptions.ALL_FILES);
            add(ChangeOptions.ALL_COMMITS);
            add(ChangeOptions.MESSAGES);
            add(ChangeOptions.REVIEWED);
            add(ChangeOptions.CHANGE_ACTIONS);
            add(ChangeOptions.CHECK);
            add(ChangeOptions.WEB_LINKS);
            add(ChangeOptions.DOWNLOAD_COMMANDS);
        }
    };
    private static final List<ChangeOptions> MESSAGES_OPTIONS = new ArrayList<ChangeOptions>() {
        {
            add(ChangeOptions.DETAILED_ACCOUNTS);
            add(ChangeOptions.MESSAGES);
        }
    };

    private static final Pattern COMMENTS_PATTERN = Pattern.compile("(^|\\s)(\\(\\d+ (inline )?comment(s)?\\))$",
            Pattern.MULTILINE);

    private static final int DIFF_REQUEST_CODE = 99;
    private static final int EDIT_REQUEST_CODE = 98;

    private static final int REQUEST_CODE_REBASE = 0;
    private static final int REQUEST_CODE_CHERRY_PICK = 1;
    private static final int REQUEST_CODE_MOVE_BRANCH = 2;
    private static final int REQUEST_CODE_EDIT_MESSAGE = 3;
    private static final int REQUEST_CODE_CHANGE_TOPIC = 4;
    private static final int REQUEST_CODE_ABANDON_CHANGE = 5;
    private static final int REQUEST_CODE_RESTORE_CHANGE = 6;
    private static final int REQUEST_CODE_REVERT_CHANGE = 7;
    private static final int REQUEST_CODE_FOLLOW_UP_CHANGE = 8;
    private static final int REQUEST_CODE_SUBMIT_CHANGE = 9;
    private static final int REQUEST_CODE_EDIT_REVISION_DESCRIPTION = 10;
    private static final int REQUEST_CODE_TAGS = 97;

    @Keep
    public static class ListModel {
        @StringRes
        public int header;
        public String selector;
        public boolean visible;
        public boolean empty;
        public int emptyText;
        public final Map<String, String> actions = new HashMap<>();

        private ListModel(int headerLabel, int emptyTextLabel) {
            header = headerLabel;
            selector = null;
            visible = false;
            empty = true;
            emptyText = emptyTextLabel;
        }
    }

    @Keep
    public static class Model {
        boolean isLocked = false;
        boolean isAuthenticated = false;
        public ListModel filesListModel = new ListModel(R.string.change_details_header_files,
                R.string.change_details_header_files_empty);
        public ListModel msgListModel = new ListModel(R.string.change_details_header_messages,
                R.string.change_details_header_messages_empty);
    }

    @Keep
    @SuppressWarnings({ "UnusedParameters", "unused" })
    public static class EventHandlers {
        private final ChangeDetailsFragment mFragment;

        public EventHandlers(ChangeDetailsFragment fragment) {
            mFragment = fragment;
        }

        public void onPatchSetPressed(View v) {
            mFragment.showPatchSetChooser(v);
        }

        public void onListHeaderSelectorPressed(View v) {
            String id = (String) v.getTag();
            if (!TextUtils.isEmpty(id)) {
                switch (id) {
                case "files":
                    mFragment.showDiffAgainstChooser(v);
                    break;
                }
            }
        }

        public void onListHeaderActionPressed(View v) {
            String id = (String) v.getTag();
            if (!TextUtils.isEmpty(id)) {
                switch (id) {
                case "files-action1":
                    mFragment.showEditChangeActivity();
                    break;

                case "messages-action1":
                    mFragment.toggleTaggedMessages();
                    break;
                case "messages-action2":
                    mFragment.toggleCIMessages();
                    break;
                }
            }
        }

        public void onStarredPressed(View v) {
            mFragment.performStarred(!v.isSelected());
        }

        public void onSharePressed(View v) {
            mFragment.performShare();
        }

        public void onEditAssigneePressed(View v) {
            mFragment.performShowEditAssigneeDialog(v);
        }

        public void onAddReviewerPressed(View v) {
            mFragment.performShowAddReviewerDialog(AddReviewerState.REVIEWER, v);
        }

        public void onAddCCPressed(View v) {
            mFragment.performShowAddReviewerDialog(AddReviewerState.CC, v);
        }

        public void onAddMeAsReviewerPressed(View v) {
            Account account = Preferences.getAccount(v.getContext());
            if (account != null) {
                mFragment.onReviewerAdded(String.valueOf(account.mAccount.accountId), AddReviewerState.REVIEWER);
            }
        }

        public void onBranchEditPressed(View v) {
            mFragment.performShowMoveBranchDialog(v);
        }

        public void onTopicEditPressed(View v) {
            mFragment.performShowChangeTopicDialog(v);
        }

        public void onTagsEditPressed(View v) {
            mFragment.performShowChangeTagsDialog(v);
        }

        public void onRelatedChangesPressed(View v) {
            mFragment.performOpenRelatedChanges();
        }

        public void onDownloadPatchSetPressed(View v) {
            mFragment.performOpenDownloadDialog();
        }

        public void onViewPatchSetPressed(View v) {
            mFragment.performViewPatchSet();
        }

        public void onEditMessagePressed(View v) {
            mFragment.performEditMessage(v);
        }

        public void onReviewPressed(View v) {
            mFragment.performReview();
        }

        public void onReplyCommentPressed(View v) {
            mFragment.performReplyComment((int) v.getTag());
        }

        public void onActionPressed(View v) {
            mFragment.performAction(v);
        }

        public void onWebLinkPressed(View v) {
            String url = (String) v.getTag();
            if (url != null) {
                ActivityHelper.openUriInCustomTabs(mFragment.getActivity(), url);
            }
        }

        public void onFileItemPressed(View v) {
            mFragment.performOpenFileDiff((String) v.getTag());
        }

        public void onApplyFilterPressed(View v) {
            mFragment.performApplyFilter(v);
        }

        public void onMessageAvatarPressed(View v) {
            int position = (int) v.getTag();
            AccountInfo account = mFragment.mResponse.mChange.messages[position].author;
            mFragment.performAccountClicked(account, null);
        }

        public void onMessagePressed(View v) {
            int position = (int) v.getTag();
            mFragment.performMessageClick(position);
        }

        private void onNavigateToComment(View v) {
            CommentInfo comment = (CommentInfo) v.getTag();
            mFragment.performNavigateToComment(comment);
        }

        public void onEditRevisionDescriptionPressed(View v) {
            mFragment.performEditRevisionDescription(v);
        }
    }

    @Keep
    public static class FileItemModel {
        public String file;
        public FileInfo info;
        public int totalAdded;
        public int totalDeleted;
        public boolean hasGraph = true;
        public int inlineComments;
        public int draftComments;
    }

    private static class FileInfoViewHolder extends RecyclerView.ViewHolder {
        private final FileInfoItemBinding mBinding;

        FileInfoViewHolder(FileInfoItemBinding binding) {
            super(binding.getRoot());
            mBinding = binding;
            binding.executePendingBindings();
        }
    }

    private static class TotalAddedDeletedViewHolder extends RecyclerView.ViewHolder {
        private final TotalAddedDeletedBinding mBinding;

        TotalAddedDeletedViewHolder(TotalAddedDeletedBinding binding) {
            super(binding.getRoot());
            mBinding = binding;
            binding.executePendingBindings();
        }
    }

    private static class MessageViewHolder extends RecyclerView.ViewHolder {
        private final MessageItemBinding mBinding;

        MessageViewHolder(MessageItemBinding binding, boolean isMessagesFolded) {
            super(binding.getRoot());
            mBinding = binding;
            mBinding.setFolded(isMessagesFolded);
            binding.executePendingBindings();
        }
    }

    private static class FileAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
        private final List<FileItemModel> mFiles = new ArrayList<>();
        private FileItemModel mTotals;
        private final EventHandlers mEventHandlers;
        private Boolean mIsShortFilenames;

        FileAdapter(EventHandlers handlers, boolean isShortFilenames) {
            mEventHandlers = handlers;
            mIsShortFilenames = isShortFilenames;
        }

        void update(ListModel listModel, Map<String, FileInfo> files, Map<String, Integer> inlineComments,
                Map<String, Integer> draftComments) {
            mFiles.clear();
            mTotals = null;
            if (files == null) {
                notifyDataSetChanged();
                return;
            }

            int added = 0;
            int deleted = 0;
            // Compute the added and deleted from revision instead from the change
            // to be accurate with the current revision info
            for (String key : files.keySet()) {
                // Do not compute commit message
                if (key.equals(Constants.COMMIT_MESSAGE)) {
                    continue;
                }

                FileInfo info = files.get(key);
                if (info.linesInserted != null) {
                    added += info.linesInserted;
                }
                if (info.linesDeleted != null) {
                    deleted += info.linesDeleted;
                }
            }

            // Create a model from each file
            for (String key : files.keySet()) {
                FileItemModel model = new FileItemModel();
                model.file = key;
                model.info = files.get(key);
                model.totalAdded = added;
                model.totalDeleted = deleted;
                model.inlineComments = inlineComments.containsKey(key) ? inlineComments.get(key) : 0;
                model.draftComments = draftComments.containsKey(key) ? draftComments.get(key) : 0;
                model.hasGraph = !key.equals(Constants.COMMIT_MESSAGE)
                        && ((model.info.linesInserted != null && model.info.linesInserted > 0)
                                || (model.info.linesDeleted != null && model.info.linesDeleted > 0)
                                || model.inlineComments > 0 || model.draftComments > 0);
                if (key.equals(Constants.COMMIT_MESSAGE)) {
                    mFiles.add(0, model);
                } else {
                    mFiles.add(model);
                }
            }

            // And add the total
            mTotals = new FileItemModel();
            mTotals.info = new FileInfo();
            if (added > 0) {
                mTotals.info.linesInserted = added;
            }
            if (deleted > 0) {
                mTotals.info.linesDeleted = deleted;
            }
            mTotals.totalAdded = added;
            mTotals.totalDeleted = deleted;

            listModel.empty = mFiles.isEmpty();
            notifyDataSetChanged();
        }

        @Override
        public int getItemCount() {
            return mFiles.size() + (mTotals != null ? 1 : 0);
        }

        @Override
        public int getItemViewType(int position) {
            return position == getItemCount() - 1 ? 1 : 0;
        }

        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            LayoutInflater inflater = LayoutInflater.from(parent.getContext());
            if (viewType == 1) {
                return new TotalAddedDeletedViewHolder(
                        DataBindingUtil.inflate(inflater, R.layout.total_added_deleted, parent, false));
            } else {
                return new FileInfoViewHolder(
                        DataBindingUtil.inflate(inflater, R.layout.file_info_item, parent, false));
            }
        }

        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            if (holder instanceof TotalAddedDeletedViewHolder) {
                TotalAddedDeletedBinding binding = ((TotalAddedDeletedViewHolder) holder).mBinding;
                binding.addedVsDeleted.with(mTotals);
                binding.setModel(mTotals);
            } else {
                FileItemModel model = mFiles.get(position);
                FileInfoItemBinding binding = ((FileInfoViewHolder) holder).mBinding;
                binding.addedVsDeleted.with(model);
                binding.setIsShortFileName(mIsShortFilenames);
                binding.setModel(model);
                binding.setHandlers(mEventHandlers);
            }
        }
    }

    private static class MessageAdapter extends RecyclerView.Adapter<MessageViewHolder> {
        private final AccountInfo mBuildBotSystemAccount;
        private final EventHandlers mEventHandlers;
        private ChangeMessageInfo[] mMessages;
        private Map<String, LinkedHashMap<String, List<CommentInfo>>> mMessagesWithComments;
        private boolean[] mFolded;
        private final boolean mIsAuthenticated;
        private final boolean mIsFolded;
        private final Picasso mPicasso;

        private boolean mIsHideTaggedMessages;
        private Repository mRepository;

        private final OnLineClickListener mLineClickListener = new OnLineClickListener() {
            @Override
            public void onLineClick(View v) {
                mEventHandlers.onNavigateToComment(v);
            }
        };

        MessageAdapter(ChangeDetailsFragment fragment, EventHandlers handlers, Picasso picasso,
                boolean isAuthenticated, boolean isFolded) {
            final Resources res = fragment.getResources();
            mEventHandlers = handlers;
            mIsAuthenticated = isAuthenticated;
            mIsFolded = isFolded;
            mPicasso = picasso;

            mBuildBotSystemAccount = new AccountInfo();
            mBuildBotSystemAccount.name = res.getString(R.string.account_build_bot_system_name);
        }

        void changeFoldedStatus(int position) {
            mFolded[position] = !mFolded[position];
            notifyItemChanged(position);
        }

        void updateHideTaggedMessages(boolean isHideTaggedMessages) {
            mIsHideTaggedMessages = isHideTaggedMessages;
        }

        void updateHideCIMessages(Repository repo) {
            mRepository = repo;
        }

        void update(ListModel listModel, ChangeMessageInfo[] messages,
                Map<String, LinkedHashMap<String, List<CommentInfo>>> messagesWithComments) {
            mMessages = filterTaggedMessages(filterCiAccountsMessages(messages));
            mMessagesWithComments = messagesWithComments;

            int count = mMessages.length;
            boolean[] old = mFolded;
            mFolded = new boolean[count];
            for (int i = 0; i < count; i++) {
                mFolded[i] = old != null && old.length > i ? old[i] : mIsFolded;
            }

            listModel.empty = count == 0;
            notifyDataSetChanged();
        }

        @Override
        public int getItemCount() {
            return mMessages != null ? mMessages.length : 0;
        }

        String getMessage(int position) {
            return mMessages[position].message;
        }

        @Override
        public MessageViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            LayoutInflater inflater = LayoutInflater.from(parent.getContext());
            return new MessageViewHolder(DataBindingUtil.inflate(inflater, R.layout.message_item, parent, false),
                    mIsFolded);
        }

        @Override
        public void onBindViewHolder(MessageViewHolder holder, int position) {
            final Context context = holder.mBinding.getRoot().getContext();
            ChangeMessageInfo message = mMessages[position];
            if (message.author == null) {
                message.author = mBuildBotSystemAccount;
            }
            Map<String, List<CommentInfo>> comments = mMessagesWithComments.get(message.id);

            PicassoHelper.bindAvatar(context, mPicasso, message.author, holder.mBinding.avatar,
                    PicassoHelper.getDefaultAvatar(context, R.color.primaryDarkForeground));
            holder.mBinding.setIsAuthenticated(mIsAuthenticated);
            holder.mBinding.setIndex(position);
            holder.mBinding.setModel(message);
            holder.mBinding.comments.listenOn(mLineClickListener).from(comments);
            holder.mBinding.setFolded(mFolded[position]);
            holder.mBinding.setHandlers(mEventHandlers);
            holder.mBinding.setFoldHandlers(mIsFolded ? mEventHandlers : null);
        }

        private ChangeMessageInfo[] filterTaggedMessages(ChangeMessageInfo[] messages) {
            if (!mIsHideTaggedMessages) {
                return messages;
            }

            ArrayList<ChangeMessageInfo> msgs = new ArrayList<>();
            for (ChangeMessageInfo msg : messages) {
                if (TextUtils.isEmpty(msg.tag)) {
                    msgs.add(msg);
                }
            }
            return msgs.toArray(new ChangeMessageInfo[msgs.size()]);
        }

        private ChangeMessageInfo[] filterCiAccountsMessages(ChangeMessageInfo[] messages) {
            if (mRepository == null || TextUtils.isEmpty(mRepository.mCiAccounts)) {
                return messages;
            }

            Pattern pattern = Pattern.compile(mRepository.mCiAccounts, Pattern.MULTILINE);
            ArrayList<ChangeMessageInfo> msgs = new ArrayList<>();
            for (ChangeMessageInfo msg : messages) {
                if (msg.author == null || msg.author.name == null || !pattern.matcher(msg.author.name).matches()) {
                    msgs.add(msg);
                }
            }
            return msgs.toArray(new ChangeMessageInfo[msgs.size()]);
        }
    }

    private static class DataResponse {
        ChangeInfo mChange;
        SubmitType mSubmitType;
        Map<String, FileInfo> mFiles;
        Map<String, ActionInfo> mActions;
        Map<String, Integer> mInlineComments;
        Map<String, Integer> mDraftComments;
        ConfigInfo mProjectConfig;
        Map<String, LinkedHashMap<String, List<CommentInfo>>> mMessagesWithComments = new HashMap<>();
    }

    private final RxLoaderObserver<DataResponse> mChangeObserver = new RxLoaderObserver<DataResponse>() {
        @Override
        public void onNext(DataResponse result) {
            mResponse = result;

            mModel.isLocked = false;
            updateLocked();

            updateAuthenticatedAndOwnerStatus();

            ChangeInfo change = null;
            mEmptyState.state = result != null ? EmptyState.NORMAL_STATE : EmptyState.NO_RESULTS_STATE;
            mBinding.setEmpty(mEmptyState);
            if (result != null) {
                change = result.mChange;
                if (mCurrentRevision == null || !change.revisions.containsKey(mCurrentRevision)) {
                    mCurrentRevision = change.currentRevision;
                }

                // Check supported features
                final GerritApi api = ModelHelper.getGerritApi(getActivity());
                boolean supportTaggedMessages = api != null && api.supportsFeature(Features.TAGGED_MESSAGES);
                Repository repo = ModelHelper.findRepositoryForAccount(getContext(), mAccount);
                boolean supportCIMessages = repo != null && !TextUtils.isEmpty(repo.mCiAccounts);

                sortRevisions(change);
                updatePatchSetInfo(result);
                updateChangeInfo(result);
                updateReviewInfo(result);

                mModel.filesListModel.selector = resolveDiffAgainstSelectorText();
                mModel.filesListModel.visible = result.mFiles != null && !result.mFiles.isEmpty();
                mModel.filesListModel.actions.clear();
                ChangeStatus status = result.mChange.status;
                if (mModel.isAuthenticated && !ChangeStatus.MERGED.equals(status)
                        && !ChangeStatus.ABANDONED.equals(status)
                        && mCurrentRevision.equals(result.mChange.currentRevision)) {
                    mModel.filesListModel.actions.put("action1", getString(R.string.action_edit));
                }
                mFileAdapter.update(mModel.filesListModel, result.mFiles, result.mInlineComments,
                        result.mDraftComments);
                mModel.msgListModel.visible = change.messages != null && change.messages.length > 0;
                if (supportTaggedMessages) {
                    mModel.msgListModel.actions.put("action1",
                            getString(mHideTaggedMessages ? R.string.change_details_action_show_tagged_messages
                                    : R.string.change_details_action_hide_tagged_messages));
                }
                if (supportCIMessages) {
                    mModel.msgListModel.actions.put("action2",
                            getString(mHideCIMessages ? R.string.change_details_action_show_ci_messages
                                    : R.string.change_details_action_hide_ci_messages));
                }
                mMessageAdapter.update(mModel.msgListModel, change.messages, result.mMessagesWithComments);
            }

            // Invalidate the diff cache. we have new data
            CacheHelper.removeAccountDiffCacheDir(getContext());

            mBinding.setModel(mModel);
            mBinding.setHandlers(mEventHandlers);
            showProgress(false, change);
        }

        @Override
        public void onError(Throwable error) {
            mModel.isLocked = false;
            updateLocked();

            mEmptyState.state = ExceptionHelper.resolveEmptyState(error);
            mBinding.setEmpty(mEmptyState);
            mChangeLoader.clear();
            ((BaseActivity) getActivity()).handleException(TAG, error, mEmptyHandlers);
            showProgress(false, null);
        }

        @Override
        public void onStarted() {
            mModel.isLocked = true;
            updateLocked();

            showProgress(true, null);
        }
    };

    private final RxLoaderObserver<Boolean> mStarredObserver = new RxLoaderObserver<Boolean>() {
        @Override
        public void onNext(Boolean value) {
            if (mResponse == null) {
                return;
            }

            mResponse.mChange.starred = value;
            updateChangeInfo(mResponse);

            mStarredLoader.clear();
        }

        @Override
        public void onError(Throwable error) {
            ((BaseActivity) getActivity()).handleException(TAG, error, mEmptyHandlers);

            mStarredLoader.clear();
        }
    };

    private final RxLoaderObserver<Boolean> mChangeEditMessageObserver = new RxLoaderObserver<Boolean>() {
        @Override
        public void onNext(Boolean value) {
            // Switch to the new revision
            mCurrentRevision = null;
            forceRefresh();

            mChangeEditMessageLoader.clear();
        }

        @Override
        public void onError(Throwable error) {
            ((BaseActivity) getActivity()).handleException(TAG, error, mEmptyHandlers);

            mChangeEditMessageLoader.clear();
        }
    };

    private final RxLoaderObserver<Boolean> mChangeEditRevisionDescriptionObserver = new RxLoaderObserver<Boolean>() {
        @Override
        public void onNext(Boolean value) {
            forceRefresh();

            mChangeEditRevisionDescriptionLoader.clear();
        }

        @Override
        public void onError(Throwable error) {
            ((BaseActivity) getActivity()).handleException(TAG, error, mEmptyHandlers);

            mChangeEditRevisionDescriptionLoader.clear();
        }
    };

    private final RxLoaderObserver<Pair<ReviewInput, ReviewInfo>> mReviewObserver = new RxLoaderObserver<Pair<ReviewInput, ReviewInfo>>() {

        private Runnable mUiDelayedProcessingNotification = () -> internalSetProcessing(true);

        @Override
        public void onNext(Pair<ReviewInput, ReviewInfo> review) {
            setProcessing(false);
            mReviewLoader.clear();

            // Clean the message box
            mBinding.reviewInfo.reviewComment.setText(null);
            AndroidHelper.hideSoftKeyboard(getContext(), getActivity().getWindow());

            // Update the messages (since it was update at server side, we can temporary
            // update the message list until a full refresh happens)
            ModelHelper.updateChangeMessageInfo(getActivity(), mAccount, mResponse.mChange, review.first);
            mMessageAdapter.update(mModel.msgListModel, mResponse.mChange.messages,
                    mResponse.mMessagesWithComments);

            // Fetch the whole change
            forceRefresh();
        }

        @Override
        public void onError(Throwable error) {
            setProcessing(false);

            ((BaseActivity) getActivity()).handleException(TAG, error, mEmptyHandlers);

            mReviewLoader.clear();
        }

        @Override
        public void onStarted() {
            setProcessing(true);
        }

        private void setProcessing(boolean locked) {
            mUiHandler.removeCallbacks(mUiDelayedProcessingNotification);
            if (locked) {
                mUiHandler.postDelayed(mUiDelayedProcessingNotification, 300L);
            } else {
                internalSetProcessing(false);
            }
        }

        private void internalSetProcessing(boolean locked) {
            mBinding.reviewInfo.setIsProcessing(locked);
            mModel.isLocked = locked;
            updateLocked();
        }
    };

    private final RxLoaderObserver<String> mChangeTopicObserver = new RxLoaderObserver<String>() {
        @Override
        public void onNext(String newTopic) {
            if (mResponse == null) {
                return;
            }

            mResponse.mChange.topic = newTopic;
            updateChangeInfo(mResponse);

            // Refresh messages
            performMessagesRefresh();

            mChangeTopicLoader.clear();
        }

        @Override
        public void onError(Throwable error) {
            ((BaseActivity) getActivity()).handleException(TAG, error, mEmptyHandlers);

            mChangeTopicLoader.clear();
        }
    };

    private final RxLoaderObserver<String[]> mChangeTagsObserver = new RxLoaderObserver<String[]>() {
        @Override
        public void onNext(String[] newTags) {
            if (mResponse == null) {
                return;
            }

            mResponse.mChange.hashtags = newTags;
            updateChangeInfo(mResponse);

            performMessagesRefresh();

            mChangeTagsLoader.clear();
        }

        @Override
        public void onError(Throwable error) {
            ((BaseActivity) getActivity()).handleException(TAG, error, mEmptyHandlers);

            mChangeTagsLoader.clear();
        }
    };

    private final RxLoaderObserver<ChangeMessageInfo[]> mMessagesRefreshObserver = new RxLoaderObserver<ChangeMessageInfo[]>() {
        @Override
        public void onNext(ChangeMessageInfo[] messages) {
            if (mResponse == null) {
                return;
            }

            // We don't fetch messages's comments from this observer, but this
            // only happens from topic change, which implies a partial refresh
            // and the possibility that we are out-of-sync is low, compared to
            // the effort of fetching messages and comments (a user refresh
            // will fix the out-of-sync problem).
            mResponse.mChange.messages = messages;
            mModel.msgListModel.visible = messages != null && messages.length > 0;
            mMessageAdapter.update(mModel.msgListModel, messages, mResponse.mMessagesWithComments);
            mBinding.setModel(mModel);
        }

        @Override
        public void onError(Throwable error) {
            ((BaseActivity) getActivity()).handleException(TAG, error, mEmptyHandlers);

            mMessagesRefreshLoader.clear();
        }
    };

    private final RxLoaderObserver<Map<String, Integer>> mDraftsRefreshObserver = new RxLoaderObserver<Map<String, Integer>>() {
        @Override
        public void onNext(Map<String, Integer> drafts) {
            if (mResponse == null) {
                return;
            }

            mResponse.mDraftComments = drafts;

            mModel.filesListModel.visible = mResponse.mFiles != null && !mResponse.mFiles.isEmpty();
            mFileAdapter.update(mModel.filesListModel, mResponse.mFiles, mResponse.mInlineComments,
                    mResponse.mDraftComments);
            mBinding.setModel(mModel);

            mDraftsRefreshLoader.clear();
        }

        @Override
        public void onError(Throwable error) {
            ((BaseActivity) getActivity()).handleException(TAG, error, mEmptyHandlers);

            mDraftsRefreshLoader.clear();
        }
    };

    private final RxLoaderObserver<AccountInfo> mRemoveReviewerObserver = new RxLoaderObserver<AccountInfo>() {
        @Override
        public void onNext(AccountInfo account) {
            if (mResponse == null) {
                return;
            }

            // Update internal objects
            if (mResponse.mChange.reviewers != null) {
                for (ReviewerStatus status : mResponse.mChange.reviewers.keySet()) {
                    AccountInfo[] reviewers = mResponse.mChange.reviewers.get(status);
                    mResponse.mChange.reviewers.put(status,
                            ModelHelper.removeAccount(getActivity(), account, reviewers));
                }
            }
            if (mResponse.mChange.labels != null) {
                for (String label : mResponse.mChange.labels.keySet()) {
                    ApprovalInfo[] approvals = mResponse.mChange.labels.get(label).all;
                    mResponse.mChange.labels.get(label).all = ModelHelper.removeApproval(account, approvals);
                }
            }
            mResponse.mChange.removableReviewers = ModelHelper.removeAccount(getActivity(), account,
                    mResponse.mChange.removableReviewers);

            updateChangeInfo(mResponse);

            performMessagesRefresh();

            mRemoveReviewerLoader.clear();
        }

        @Override
        public void onError(Throwable error) {
            ((BaseActivity) getActivity()).handleException(TAG, error, mEmptyHandlers);

            mRemoveReviewerLoader.clear();
        }
    };

    private final RxLoaderObserver<Pair<String, AccountInfo>> mRemoveReviewerVoteObserver = new RxLoaderObserver<Pair<String, AccountInfo>>() {
        @Override
        public void onNext(Pair<String, AccountInfo> vote) {
            if (mResponse == null) {
                return;
            }

            // Update internal objects
            if (mResponse.mChange.labels != null && mResponse.mChange.labels.containsKey(vote.first)) {
                mResponse.mChange.labels.get(vote.first).all = ModelHelper.removeApproval(vote.second,
                        mResponse.mChange.labels.get(vote.first).all);
            }
            updateChangeInfo(mResponse);

            performMessagesRefresh();

            mRemoveReviewerVoteLoader.clear();
        }

        @Override
        public void onError(Throwable error) {
            ((BaseActivity) getActivity()).handleException(TAG, error, mEmptyHandlers);

            mRemoveReviewerVoteLoader.clear();
        }
    };

    private final RxLoaderObserver<AddReviewerResultInfo> mAddReviewerObserver = new RxLoaderObserver<AddReviewerResultInfo>() {
        @Override
        public void onNext(AddReviewerResultInfo result) {
            if (mResponse == null) {
                return;
            }

            if (!TextUtils.isEmpty(result.error) || result.confirm) {
                onError(new OperationFailedException(String.format(Locale.US, "error: %s; confirm: %s",
                        result.error, String.valueOf(result.confirm))));
                return;
            }

            // Update internal objects

            // Update reviewers
            if (mResponse.mChange.reviewers == null) {
                mResponse.mChange.reviewers = new HashMap<>();
            }
            if (result.reviewers != null) {
                AccountInfo[] reviewers = mResponse.mChange.reviewers.get(ReviewerStatus.REVIEWER);
                if (reviewers != null) {
                    mResponse.mChange.reviewers.put(ReviewerStatus.REVIEWER,
                            ModelHelper.addReviewers(result.reviewers, reviewers));
                } else {
                    mResponse.mChange.reviewers.put(ReviewerStatus.REVIEWER, result.reviewers);
                }
            }
            if (result.ccs != null) {
                AccountInfo[] ccs = mResponse.mChange.reviewers.get(ReviewerStatus.CC);
                if (ccs != null) {
                    mResponse.mChange.reviewers.put(ReviewerStatus.CC, ModelHelper.addReviewers(result.ccs, ccs));
                } else {
                    mResponse.mChange.reviewers.put(ReviewerStatus.CC, result.ccs);
                }
            }

            // Update labels
            if (result.reviewers != null && mResponse.mChange.labels != null) {

                for (String label : mResponse.mChange.labels.keySet()) {
                    ApprovalInfo[] approvals = mResponse.mChange.labels.get(label).all;
                    mResponse.mChange.labels.get(label).all = ModelHelper.updateApprovals(result.reviewers, label,
                            approvals);
                }
            }
            ModelHelper.updateRemovableReviewers(getContext(), mResponse.mChange, result);

            updateChangeInfo(mResponse);

            mAddReviewerLoader.clear();
        }

        @Override
        public void onError(Throwable error) {
            ((BaseActivity) getActivity()).handleException(TAG, error, mEmptyHandlers);

            mAddReviewerLoader.clear();
        }
    };

    private final RxLoaderObserver<AssigneeInfo> mEditAssigneeObserver = new RxLoaderObserver<AssigneeInfo>() {
        @Override
        public void onNext(AssigneeInfo result) {
            if (mResponse == null) {
                return;
            }

            mResponse.mChange.assignee = result._new;
            if (result._new != null) {
                // Update internal objects
                if (mResponse.mChange.reviewers != null) {
                    // Update reviewers
                    AccountInfo[] reviewers = mResponse.mChange.reviewers.get(ReviewerStatus.REVIEWER);
                    mResponse.mChange.reviewers.put(ReviewerStatus.REVIEWER,
                            ModelHelper.addReviewers(new AccountInfo[] { result._new }, reviewers));
                }
                ModelHelper.addRemovableReviewer(mResponse.mChange, result._new);
            }

            updateChangeInfo(mResponse);

            performMessagesRefresh();

            mEditAssigneeLoader.clear();
        }

        @Override
        public void onError(Throwable error) {
            ((BaseActivity) getActivity()).handleException(TAG, error, mEmptyHandlers);

            mEditAssigneeLoader.clear();
        }
    };

    private final RxLoaderObserver<Object> mActionObserver = new RxLoaderObserver<Object>() {
        @Override
        public void onNext(Object value) {
            if (Empty.NULL.equals(value)) {
                // The change was deleted. Redirect to parent
                ActivityHelper.performFinishActivity(getActivity(), true);
                return;
            }

            if (value instanceof ChangeInfo) {
                // Move to the new change
                ActivityHelper.openChangeDetails(getContext(), (ChangeInfo) value, false, false);
                return;
            }

            // Refresh the change
            forceRefresh();

            mActionLoader.clear();
        }

        @Override
        public void onError(Throwable error) {
            ((BaseActivity) getActivity()).handleException(TAG, error, mEmptyHandlers);

            mActionLoader.clear();
        }
    };

    private final RxLoaderObserver<ChangeInfo> mMoveBranchObserver = new RxLoaderObserver<ChangeInfo>() {
        @Override
        public void onNext(ChangeInfo value) {
            // Refresh the change
            forceRefresh();

            mMoveBranchLoader.clear();
        }

        @Override
        public void onError(Throwable error) {
            ((BaseActivity) getActivity()).handleException(TAG, error, mEmptyHandlers);

            mMoveBranchLoader.clear();
        }
    };

    @Keep
    public static class EmptyEventHandlers extends EmptyState.EventHandlers {
        private ChangeDetailsFragment mFragment;

        EmptyEventHandlers(ChangeDetailsFragment fragment) {
            mFragment = fragment;
        }

        public void onRetry(View v) {
            mFragment.forceRefresh();
        }
    }

    private Handler mUiHandler;

    private final OnAccountChipClickedListener mOnAccountChipClickedListener = this::performAccountClicked;
    private final OnAccountChipRemovedListener mOnReviewerRemovedListener = this::performRemoveReviewer;
    private final OnAccountChipRemovedListener mOnReviewerRemovedVoteListener = this::performRemoveReviewerVote;
    private final OnAccountChipRemovedListener mOnAssigneeRemovedListener = (account,
            tag) -> onAssigneeSelected(null);

    private ChangeDetailsFragmentBinding mBinding;
    private Picasso mPicasso;

    private FileAdapter mFileAdapter;
    private MessageAdapter mMessageAdapter;

    private EventHandlers mEventHandlers;
    private final Model mModel = new Model();
    private final EmptyState mEmptyState = new EmptyState();
    private EmptyEventHandlers mEmptyHandlers;
    private String mCurrentRevision;
    private String mDiffAgainstRevision;
    private DataResponse mResponse;
    private final List<RevisionInfo> mAllRevisions = new ArrayList<>();
    private final List<RevisionInfo> mAllRevisionsWithBase = new ArrayList<>();

    private boolean mHideTaggedMessages;
    private boolean mHideCIMessages;

    private RxLoader1<String, DataResponse> mChangeLoader;
    private RxLoader1<Boolean, Boolean> mStarredLoader;
    private RxLoader1<ChangeEditMessageInput, Boolean> mChangeEditMessageLoader;
    private RxLoader1<DescriptionInput, Boolean> mChangeEditRevisionDescriptionLoader;
    private RxLoader1<ReviewInput, Pair<ReviewInput, ReviewInfo>> mReviewLoader;
    private RxLoader2<String, AddReviewerState, AddReviewerResultInfo> mAddReviewerLoader;
    private RxLoader1<String, AssigneeInfo> mEditAssigneeLoader;
    private RxLoader1<AccountInfo, AccountInfo> mRemoveReviewerLoader;
    private RxLoader1<Pair<String, AccountInfo>, Pair<String, AccountInfo>> mRemoveReviewerVoteLoader;
    private RxLoader1<String, String> mChangeTopicLoader;
    private RxLoader2<String[], String[], String[]> mChangeTagsLoader;
    private RxLoader<ChangeMessageInfo[]> mMessagesRefreshLoader;
    private RxLoader<Map<String, Integer>> mDraftsRefreshLoader;
    private RxLoader2<String, String[], Object> mActionLoader;
    private RxLoader1<String, ChangeInfo> mMoveBranchLoader;
    private int mLegacyChangeId;

    private Map<String, Integer> savedReview;

    private Account mAccount;

    private boolean mIsInlineCommentsInMessages;

    public static ChangeDetailsFragment newInstance(int changeId) {
        return newInstance(changeId, null, null);
    }

    public static ChangeDetailsFragment newInstance(int changeId, String revision, String base) {
        ChangeDetailsFragment fragment = new ChangeDetailsFragment();
        Bundle arguments = new Bundle();
        arguments.putInt(Constants.EXTRA_LEGACY_CHANGE_ID, changeId);
        arguments.putString(Constants.EXTRA_REVISION, revision);
        arguments.putString(Constants.EXTRA_BASE, base);
        fragment.setArguments(arguments);
        return fragment;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mUiHandler = new Handler();
        mLegacyChangeId = getArguments().getInt(Constants.EXTRA_LEGACY_CHANGE_ID, Constants.INVALID_CHANGE_ID);
        mCurrentRevision = getArguments().getString(Constants.EXTRA_REVISION);
        mDiffAgainstRevision = getArguments().getString(Constants.EXTRA_BASE);
        mPicasso = PicassoHelper.getPicassoClient(getContext());
        mEmptyHandlers = new EmptyEventHandlers(this);

        if (savedInstanceState != null) {
            mCurrentRevision = savedInstanceState.getString("current_revision", mCurrentRevision);
            mDiffAgainstRevision = savedInstanceState.getString("diff_against_revision", mDiffAgainstRevision);
        }
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
            @Nullable Bundle savedInstanceState) {
        mBinding = DataBindingUtil.inflate(inflater, R.layout.change_details_fragment, container, false);
        mBinding.setModel(mModel);
        mBinding.setEmpty(mEmptyState);
        mBinding.setEmptyHandlers(mEmptyHandlers);
        startLoadersWithValidContext(savedInstanceState);

        // Force HW acceleration in case the activity disabled it in minidrawer mode
        mBinding.getRoot().setLayerType(View.LAYER_TYPE_HARDWARE, null);
        return mBinding.getRoot();
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        startLoadersWithValidContext(savedInstanceState);
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        Map<String, Integer> review = mBinding.reviewInfo.reviewLabels.getReview(false);
        outState.putString("review", SerializationManager.getInstance().toJson(review));
        outState.putString("current_revision", mCurrentRevision);
        outState.putString("diff_against_revision", mDiffAgainstRevision);
        outState.putBoolean("hideTaggedMessages", mHideTaggedMessages);
        outState.putBoolean("hideCIMessages", mHideCIMessages);
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == DIFF_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
            if (data != null) {
                // Current revision
                String revisionId = data.getStringExtra(Constants.EXTRA_REVISION_ID);
                if (revisionId != null && !revisionId.equals(mCurrentRevision)) {
                    // Change to the current revision
                    mCurrentRevision = revisionId;
                    forceRefresh();
                    return;
                }

                // Diff against revision
                String diffAgainst = resolveDiffAgainstRevision(data.getStringExtra(Constants.EXTRA_BASE));
                boolean changed = (diffAgainst == null && mDiffAgainstRevision != null)
                        || (diffAgainst != null && mDiffAgainstRevision == null)
                        || (diffAgainst != null && !diffAgainst.equals(mDiffAgainstRevision));
                if (changed) {
                    // Change to the diff against revision revision
                    if (diffAgainst != null && diffAgainst.equals(mCurrentRevision)) {
                        mDiffAgainstRevision = null;
                    } else {
                        mDiffAgainstRevision = diffAgainst;
                    }
                    forceRefresh();
                    return;
                }

                // Drafts changed
                boolean dataChanged = data.getBooleanExtra(Constants.EXTRA_DATA_CHANGED, false);
                if (dataChanged) {
                    // Refresh drafts comments
                    performDraftsRefresh();
                }
            }
        } else if (requestCode == EDIT_REQUEST_CODE) {
            // Remove the cache, in case the user request to enter again in edit mode
            CacheHelper.removeAccountDiffCacheDir(getContext());

            // If the user publish the edit, then reload the whole change
            if (resultCode == Activity.RESULT_OK) {
                mCurrentRevision = null;
                mDiffAgainstRevision = null;
                forceRefresh();
            }
        }
    }

    private void startLoadersWithValidContext(Bundle savedInstanceState) {
        if (getActivity() == null) {
            return;
        }

        if (mFileAdapter == null) {
            mAccount = Preferences.getAccount(getContext());
            if (mAccount != null) {
                mModel.isAuthenticated = mAccount.hasAuthenticatedAccessMode();
            }
            updateAuthenticatedAndOwnerStatus();

            mHideTaggedMessages = Preferences.isAccountToggleTaggedMessages(getContext(), mAccount);
            mHideCIMessages = Preferences.isAccountToggleCIAccountsMessages(getContext(), mAccount);
            if (savedInstanceState != null) {
                mHideTaggedMessages = savedInstanceState.getBoolean("hideTaggedMessages", mHideTaggedMessages);
                mHideCIMessages = savedInstanceState.getBoolean("hideCIMessages", mHideCIMessages);
            }
            Repository repo = null;
            if (mHideCIMessages) {
                repo = ModelHelper.findRepositoryForAccount(getContext(), mAccount);
            }

            boolean isMessagesFolded = Preferences.isAccountMessagesFolded(getContext(), mAccount);
            mIsInlineCommentsInMessages = Preferences.isAccountInlineCommentInMessages(getContext(), mAccount);

            mEventHandlers = new EventHandlers(this);

            mFileAdapter = new FileAdapter(mEventHandlers,
                    Preferences.isAccountShortFilenames(getContext(), mAccount));
            mBinding.fileInfo.list
                    .setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false));
            mBinding.fileInfo.list.setNestedScrollingEnabled(false);
            mBinding.fileInfo.list.setAdapter(mFileAdapter);

            mMessageAdapter = new MessageAdapter(this, mEventHandlers, mPicasso, mModel.isAuthenticated,
                    isMessagesFolded);
            mMessageAdapter.updateHideTaggedMessages(mHideTaggedMessages);
            mMessageAdapter.updateHideCIMessages(repo);
            int leftPadding = getResources().getDimensionPixelSize(R.dimen.message_list_left_padding);
            DividerItemDecoration messageDivider = new DividerItemDecoration(getContext(),
                    LinearLayoutManager.VERTICAL);
            messageDivider.setMargins(leftPadding, 0);
            mBinding.messageInfo.list
                    .setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false));
            mBinding.messageInfo.list.addItemDecoration(messageDivider);
            mBinding.messageInfo.list.setNestedScrollingEnabled(false);
            mBinding.messageInfo.list.setAdapter(mMessageAdapter);

            mBinding.fastScroller.listenTo(() -> {
                mBinding.nestedScroll.fullScroll(View.FOCUS_DOWN);
                mBinding.fastScroller.hide();
            });

            mBinding.nestedScroll.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {
                @Override
                public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX,
                        int oldScrollY) {
                    float h = mBinding.nestedScroll.getHeight();
                    float h14 = h / 4;
                    float mt = mBinding.messageInfo.getRoot().getTop();
                    float mh = mBinding.messageInfo.getRoot().getHeight();
                    float rt = mBinding.reviewInfo.getRoot().getTop() == 0 ? mt + mh
                            : mBinding.reviewInfo.getRoot().getTop();

                    if ((mh / h) >= 2.5) {
                        if (scrollY < mt || (scrollY + h + h14) > rt) {
                            mBinding.fastScroller.hide();
                        } else if (scrollY > mt) {
                            mBinding.fastScroller.show(R.string.change_details_fast_scroll_msg);
                        }
                    }
                }
            });

            // Restore user temporary review state
            if (savedInstanceState != null) {
                String review = savedInstanceState.getString("review", null);
                if (review != null) {
                    Type type = new TypeToken<Map<String, Integer>>() {
                    }.getType();
                    savedReview = SerializationManager.getInstance().fromJson(review, type);
                }
            }

            // Configure the refresh
            setupSwipeToRefresh();

            // Fetch or join current loader
            RxLoaderManager loaderManager = RxLoaderManagerCompat.get(this);
            mChangeLoader = loaderManager.create("fetch", this::fetchChange, mChangeObserver);
            mStarredLoader = loaderManager.create("starred", this::starChange, mStarredObserver);
            mChangeEditMessageLoader = loaderManager.create("edit:message", this::editMessage,
                    mChangeEditMessageObserver);
            mChangeEditRevisionDescriptionLoader = loaderManager.create("edit:description",
                    this::editRevisionDescription, mChangeEditRevisionDescriptionObserver);
            mReviewLoader = loaderManager.create("review", this::reviewChange, mReviewObserver);
            mChangeTopicLoader = loaderManager.create("change_topic", this::changeTopic, mChangeTopicObserver);
            mChangeTagsLoader = loaderManager.create("change_tags", this::changeTags, mChangeTagsObserver);
            mAddReviewerLoader = loaderManager.create("add_reviewer", this::addReviewer, mAddReviewerObserver);
            mEditAssigneeLoader = loaderManager.create("edit_assignee", this::editAssignee, mEditAssigneeObserver);
            mRemoveReviewerLoader = loaderManager.create("remove_reviewer", this::removeReviewer,
                    mRemoveReviewerObserver);
            mRemoveReviewerVoteLoader = loaderManager.create("remove_reviewer_vote", this::removeReviewerVote,
                    mRemoveReviewerVoteObserver);
            mMessagesRefreshLoader = loaderManager.create("messages_refresh", fetchMessages(),
                    mMessagesRefreshObserver);
            mActionLoader = loaderManager.create("action", this::doAction, mActionObserver);
            mMoveBranchLoader = loaderManager.create("move_branch", this::doMoveBranch, mMoveBranchObserver);
            mDraftsRefreshLoader = loaderManager.create("drafts_refresh", fetchDrafts(), mDraftsRefreshObserver);
            mChangeLoader.start(String.valueOf(mLegacyChangeId));
        }
    }

    @Override
    public final void onDestroyView() {
        super.onDestroyView();
        mBinding.unbind();
        mUiHandler.removeCallbacksAndMessages(null);
    }

    private void updatePatchSetInfo(DataResponse response) {
        mBinding.patchSetInfo.setChangeId(response.mChange.changeId);
        mBinding.patchSetInfo.setRevision(mCurrentRevision);
        RevisionInfo revision = response.mChange.revisions.get(mCurrentRevision);
        mBinding.patchSetInfo.setChange(response.mChange);
        mBinding.patchSetInfo.setConfig(response.mProjectConfig);
        mBinding.patchSetInfo.setModel(revision);
        final int maxRevision = computeMaxRevisionNumber(response.mChange.revisions.values());
        final String patchSetText = getContext().getString(R.string.change_details_header_patchsets,
                revision.number, maxRevision);
        mBinding.patchSetInfo.setPatchset(patchSetText);
        mBinding.patchSetInfo.setHandlers(mEventHandlers);
        mBinding.patchSetInfo.parentCommits.with(mEventHandlers).from(revision.commit);
        mBinding.patchSetInfo.setIsCurrentRevision(mCurrentRevision.equals(response.mChange.currentRevision));
        mBinding.patchSetInfo.setHasData(true);
    }

    @SuppressWarnings("ConstantConditions")
    private void updateChangeInfo(DataResponse response) {
        final Context ctx = getActivity();
        if (ctx == null) {
            return;
        }
        boolean open = !ChangeStatus.MERGED.equals(response.mChange.status)
                && !ChangeStatus.ABANDONED.equals(response.mChange.status);
        final GerritApi api = ModelHelper.getGerritApi(ctx);
        boolean supportVotes = api.supportsFeature(Features.VOTES);
        boolean supportAssignee = api.supportsFeature(Features.ASSIGNEE);
        boolean canAssignee = response.mActions.containsKey(ModelHelper.ACTION_ASSIGNEE);
        boolean supportsCC = api.supportsFeature(Features.CC) && mAccount.getServerInfo() != null
                && mAccount.getServerInfo().noteDbEnabled;

        mBinding.changeInfo.owner.with(mPicasso).listenOn(mOnAccountChipClickedListener)
                .from(response.mChange.owner);
        mBinding.changeInfo.assignee.with(mPicasso)
                .removable(mAccount.hasAuthenticatedAccessMode() && supportAssignee && canAssignee)
                .listenOn(mOnAccountChipClickedListener).listenOn(mOnAssigneeRemovedListener)
                .from(response.mChange.assignee);
        mBinding.changeInfo.reviewers.with(mPicasso).listenOn(mOnAccountChipClickedListener)
                .listenOn(mOnReviewerRemovedListener).withRemovableReviewers(open)
                .withFilterCIAccounts(mHideCIMessages)
                .withReviewerStatus(supportsCC ? ReviewerStatus.REVIEWER : null).from(response.mChange);
        if (supportsCC) {
            mBinding.changeInfo.cc.with(mPicasso).listenOn(mOnAccountChipClickedListener)
                    .listenOn(mOnReviewerRemovedListener).withRemovableReviewers(open)
                    .withFilterCIAccounts(mHideCIMessages).withReviewerStatus(ReviewerStatus.CC)
                    .from(response.mChange);
        }
        mBinding.changeInfo.labels.with(mPicasso)
                .withRemovableReviewers(open && supportVotes, response.mChange.removableReviewers)
                .listenOn(mOnAccountChipClickedListener).listenOn(mOnReviewerRemovedVoteListener)
                .from(response.mChange);
        mBinding.changeInfo.setModel(response.mChange);
        mBinding.changeInfo.setSubmitType(response.mSubmitType);
        mBinding.changeInfo.setServerInfo(mAccount.getServerInfo());
        mBinding.changeInfo.setActions(response.mActions);
        mBinding.changeInfo.setHandlers(mEventHandlers);
        mBinding.changeInfo.setHasData(true);
        mBinding.changeInfo.setIsReviewer(ModelHelper.isReviewer(mAccount.mAccount, response.mChange));
        mBinding.changeInfo.setIsTwoPane(getResources().getBoolean(R.bool.config_is_two_pane));
        mBinding.changeInfo.setIsCurrentRevision(mCurrentRevision.equals(response.mChange.currentRevision));
    }

    private void updateReviewInfo(DataResponse response) {
        mBinding.reviewInfo.setHasData(true);
        mBinding.reviewInfo.setModel(response.mChange);
        mBinding.reviewInfo.setHandlers(mEventHandlers);
        mBinding.reviewInfo.setIsCurrentRevision(mCurrentRevision.equals(response.mChange.currentRevision));
        mBinding.reviewInfo.reviewLabels.from(response.mChange, savedReview);
        savedReview = null;
    }

    private int computeMaxRevisionNumber(Collection<RevisionInfo> revisions) {
        int max = 0;
        for (RevisionInfo revision : revisions) {
            max = Math.max(revision.number, max);
        }
        return max;
    }

    @SuppressWarnings("ConstantConditions")
    private Observable<DataResponse> fetchChange(String changeId) {
        final Context ctx = getActivity();
        final GerritApi api = ModelHelper.getGerritApi(ctx);
        final String revision = mCurrentRevision == null ? GerritApi.CURRENT_REVISION : mCurrentRevision;
        return Observable.zip(SafeObservable.fromNullCallable(() -> {
            DataResponse dataResponse = new DataResponse();
            dataResponse.mChange = api.getChange(changeId, OPTIONS).blockingFirst();

            // Obtain the project configuration
            if (dataResponse.mChange != null) {
                // Request project config
                dataResponse.mProjectConfig = api.getProjectConfig(dataResponse.mChange.project).blockingFirst();

                // Only request actions when we don't know which actions
                // the change could have for the user. In other case, we
                // have some logic to deal with basic actions.
                // Request actions could be a heavy operation in old and complex
                // changes, so just try to omit it.
                ChangeStatus status = dataResponse.mChange.status;
                if (mAccount.hasAuthenticatedAccessMode() && !ChangeStatus.MERGED.equals(status)
                        && !ChangeStatus.ABANDONED.equals(status)) {
                    dataResponse.mActions = api.getChangeRevisionActions(changeId, revision).blockingFirst();
                } else {
                    // At least a cherry-pick action should be present if user
                    // is authenticated
                    dataResponse.mActions = new HashMap<>();
                    if (mAccount.hasAuthenticatedAccessMode()) {
                        dataResponse.mActions.put(ModelHelper.ACTION_CHERRY_PICK, new ActionInfo());
                    }
                }
            }

            return dataResponse;
        }), api.getChangeRevisionFiles(changeId, revision, mDiffAgainstRevision, null),
                api.getChangeRevisionSubmitType(changeId, revision),
                api.getChangeRevisionComments(changeId, revision), SafeObservable.fromNullCallable(() -> {
                    if (mDiffAgainstRevision != null) {
                        return api.getChangeRevisionComments(changeId, mDiffAgainstRevision).blockingFirst();
                    }
                    return new HashMap<>();
                }), SafeObservable.fromNullCallable(() -> {
                    // Do no fetch drafts if the account is not authenticated
                    if (mAccount.hasAuthenticatedAccessMode()) {
                        return api.getChangeRevisionDrafts(changeId, revision).blockingFirst();
                    }
                    return new HashMap<>();
                }), SafeObservable.fromNullCallable(() -> {
                    // Do no fetch drafts if the account is not authenticated
                    if (mDiffAgainstRevision != null && mAccount.hasAuthenticatedAccessMode()) {
                        return api.getChangeRevisionDrafts(changeId, mDiffAgainstRevision).blockingFirst();
                    }
                    return new HashMap<>();
                }), this::combineResponse).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
    }

    @SuppressWarnings("ConstantConditions")
    private Observable<Boolean> starChange(final Boolean starred) {
        final Context ctx = getActivity();
        final GerritApi api = ModelHelper.getGerritApi(ctx);
        return SafeObservable.fromNullCallable(() -> {
            Observable<Void> call;
            if (starred) {
                call = api.putDefaultStarOnChange(GerritApi.SELF_ACCOUNT, String.valueOf(mLegacyChangeId));
            } else {
                call = api.deleteDefaultStarFromChange(GerritApi.SELF_ACCOUNT, String.valueOf(mLegacyChangeId));
            }
            call.blockingFirst();
            return starred;
        }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
    }

    @SuppressWarnings("ConstantConditions")
    private Observable<Boolean> editMessage(final ChangeEditMessageInput input) {
        final Context ctx = getActivity();
        final GerritApi api = ModelHelper.getGerritApi(ctx);
        return SafeObservable.fromNullCallable(() -> {
            api.setChangeEditMessage(String.valueOf(mLegacyChangeId), input).blockingFirst();
            api.publishChangeEdit(String.valueOf(mLegacyChangeId)).blockingFirst();
            return true;
        }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
    }

    @SuppressWarnings("ConstantConditions")
    private Observable<Boolean> editRevisionDescription(final DescriptionInput input) {
        final Context ctx = getActivity();
        final GerritApi api = ModelHelper.getGerritApi(ctx);
        return SafeObservable.fromNullCallable(() -> {
            api.setChangeRevisionDescription(String.valueOf(mLegacyChangeId), mCurrentRevision, input)
                    .blockingFirst();
            return true;
        }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
    }

    @SuppressWarnings("ConstantConditions")
    private Observable<Pair<ReviewInput, ReviewInfo>> reviewChange(final ReviewInput input) {
        final Context ctx = getActivity();
        final GerritApi api = ModelHelper.getGerritApi(ctx);
        return SafeObservable.fromNullCallable(() -> {
            ReviewInfo response = api
                    .setChangeRevisionReview(String.valueOf(mLegacyChangeId), mCurrentRevision, input)
                    .blockingFirst();
            return new Pair<>(input, response);
        }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
    }

    @SuppressWarnings("ConstantConditions")
    private Observable<String> changeTopic(final String newTopic) {
        final Context ctx = getActivity();
        final GerritApi api = ModelHelper.getGerritApi(ctx);
        return SafeObservable.fromNullCallable(() -> {
            if (!TextUtils.isEmpty(newTopic)) {
                TopicInput input = new TopicInput();
                input.topic = newTopic;
                api.setChangeTopic(String.valueOf(mLegacyChangeId), input).blockingFirst();
            } else {
                api.deleteChangeTopic(String.valueOf(mLegacyChangeId)).blockingFirst();
            }
            return newTopic;
        }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
    }

    @SuppressWarnings("ConstantConditions")
    private Observable<String[]> changeTags(final String[] add, final String[] remove) {
        final Context ctx = getActivity();
        final GerritApi api = ModelHelper.getGerritApi(ctx);
        return SafeObservable.fromNullCallable(() -> {
            HashtagsInput input = new HashtagsInput();
            input.add = Arrays.asList(add);
            input.remove = Arrays.asList(remove);
            return api.setChangeHashtags(String.valueOf(mLegacyChangeId), input).blockingFirst();
        }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
    }

    @SuppressWarnings("ConstantConditions")
    private Observable<AddReviewerResultInfo> addReviewer(final String reviewer, AddReviewerState state) {
        final Context ctx = getActivity();
        final GerritApi api = ModelHelper.getGerritApi(ctx);
        return SafeObservable.fromNullCallable(() -> {
            ReviewerInput input = new ReviewerInput();
            input.reviewerId = reviewer;
            input.state = state;
            return api.addChangeReviewer(String.valueOf(mLegacyChangeId), input).blockingFirst();
        }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
    }

    @SuppressWarnings("ConstantConditions")
    private Observable<AssigneeInfo> editAssignee(final String assignee) {
        final Context ctx = getActivity();
        final GerritApi api = ModelHelper.getGerritApi(ctx);
        return SafeObservable.fromNullCallable(() -> {
            AssigneeInfo info = new AssigneeInfo();
            info.old = mResponse.mChange.assignee;
            if (TextUtils.isEmpty(assignee)) {
                // Remove assignee
                api.deleteChangeAssignee(String.valueOf(mLegacyChangeId)).blockingFirst();
            } else {
                // Set assignee
                AssigneeInput input = new AssigneeInput();
                input.assignee = assignee;
                info._new = api.setChangeAssignee(String.valueOf(mLegacyChangeId), input).blockingFirst();
            }
            return info;
        }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
    }

    @SuppressWarnings("ConstantConditions")
    private Observable<AccountInfo> removeReviewer(final AccountInfo account) {
        final Context ctx = getActivity();
        final GerritApi api = ModelHelper.getGerritApi(ctx);
        return SafeObservable.fromNullCallable(() -> {
            api.deleteChangeReviewer(String.valueOf(mLegacyChangeId), String.valueOf(account.accountId))
                    .blockingFirst();
            return account;
        }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
    }

    @SuppressWarnings("ConstantConditions")
    private Observable<Pair<String, AccountInfo>> removeReviewerVote(final Pair<String, AccountInfo> vote) {
        // TODO Evaluate to use deleteChangeRevisionReviewersVote 2.14+'s method
        // for a safer deletion
        final Context ctx = getActivity();
        final GerritApi api = ModelHelper.getGerritApi(ctx);
        return SafeObservable.fromNullCallable(() -> {
            DeleteVoteInput input = new DeleteVoteInput();

            api.deleteChangeReviewerVote(String.valueOf(mLegacyChangeId), String.valueOf(vote.second.accountId),
                    String.valueOf(vote.first), input).blockingFirst();
            return vote;
        }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
    }

    @SuppressWarnings("ConstantConditions")
    private Observable<ChangeMessageInfo[]> fetchMessages() {
        final Context ctx = getActivity();
        final GerritApi api = ModelHelper.getGerritApi(ctx);
        return SafeObservable.fromNullCallable(() -> {
            final ChangeInfo change = api.getChange(String.valueOf(mLegacyChangeId), MESSAGES_OPTIONS)
                    .blockingFirst();
            return change.messages;
        }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
    }

    @SuppressWarnings("ConstantConditions")
    private Observable<Map<String, Integer>> fetchDrafts() {
        final Context ctx = getActivity();
        final GerritApi api = ModelHelper.getGerritApi(ctx);
        return SafeObservable.fromNullCallable(() -> {
            // Do no fetch drafts if the account is not authenticated
            if (mAccount.hasAuthenticatedAccessMode()) {
                Map<String, List<CommentInfo>> drafts = api
                        .getChangeRevisionDrafts(String.valueOf(mLegacyChangeId), mCurrentRevision).blockingFirst();
                Map<String, Integer> draftComments = new HashMap<>();
                if (drafts != null) {
                    for (String file : drafts.keySet()) {
                        draftComments.put(file, drafts.get(file).size());
                    }
                }
                return draftComments;
            }
            return new HashMap<String, Integer>();
        }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
    }

    @SuppressWarnings("ConstantConditions")
    private Observable<Object> doAction(final String action, final String[] params) {
        final Context ctx = getActivity();
        final GerritApi api = ModelHelper.getGerritApi(ctx);
        return SafeObservable.fromNullCallable(() -> {
            switch (action) {
            case ModelHelper.ACTION_CHERRY_PICK:
                return performCherryPickChange(api, params[0], params[1]);
            case ModelHelper.ACTION_REBASE:
                return performRebaseChange(api, params[0]);
            case ModelHelper.ACTION_ABANDON:
                performAbandonChange(api, params[0]);
                break;
            case ModelHelper.ACTION_RESTORE:
                performRestoreChange(api, params[0]);
                break;
            case ModelHelper.ACTION_REVERT:
                return performRevertChange(api, params[0]);
            case ModelHelper.ACTION_PUBLISH_DRAFT:
                return performPublishDraft(api);
            case ModelHelper.ACTION_DELETE_CHANGE:
                performDeleteChange(api);
                break;
            case ModelHelper.ACTION_FOLLOW_UP:
                return performFollowUp(api, params[0]);
            case ModelHelper.ACTION_SUBMIT:
                performSubmitChange(api);
                break;
            }
            return Empty.NULL;
        }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
    }

    @SuppressWarnings("ConstantConditions")
    private Observable<ChangeInfo> doMoveBranch(final String newBranch) {
        final Context ctx = getActivity();
        final GerritApi api = ModelHelper.getGerritApi(ctx);
        return SafeObservable.fromNullCallable(() -> {
            MoveInput input = new MoveInput();
            input.destinationBranch = newBranch;
            return api.moveChange(String.valueOf(mLegacyChangeId), input).blockingFirst();
        }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
    }

    private void setupSwipeToRefresh() {
        mBinding.refresh.setColorSchemeColors(ContextCompat.getColor(getContext(), R.color.accent));
        mBinding.refresh.setOnRefreshListener(this::forceRefresh);
    }

    private void forceRefresh() {
        startLoadersWithValidContext(null);

        // Check that activity was attached before refresh
        if (mChangeLoader != null) {
            mChangeLoader.clear();
            mChangeLoader.restart(String.valueOf(mLegacyChangeId));
        }
    }

    private void showProgress(boolean show, ChangeInfo change) {
        BaseActivity activity = (BaseActivity) getActivity();
        if (show) {
            activity.onRefreshStart(this);
        } else {
            activity.onRefreshEnd(this, change);
        }
        mBinding.refresh.setRefreshing(false);
    }

    private DataResponse combineResponse(DataResponse response, Map<String, FileInfo> files, SubmitType submitType,
            Map<String, List<CommentInfo>> revisionComments, Map<String, List<CommentInfo>> baseRevisionComments,
            Map<String, List<CommentInfo>> revisionDraftComments,
            Map<String, List<CommentInfo>> baseRevisionDraftComments) {
        // Map inline and draft comments
        Map<String, Integer> inlineComments = new HashMap<>();
        if (revisionComments != null) {
            for (String file : revisionComments.keySet()) {
                inlineComments.put(file, computeNumberOfComments(revisionComments.get(file)));
            }
        }
        Map<String, Integer> draftComments = new HashMap<>();
        if (revisionDraftComments != null) {
            for (String file : revisionDraftComments.keySet()) {
                draftComments.put(file, computeNumberOfComments(revisionDraftComments.get(file)));
            }
        }

        // Map inline and draft comments from diff against revision
        if (baseRevisionComments != null) {
            for (String file : baseRevisionComments.keySet()) {
                int count = computeNumberOfComments(baseRevisionComments.get(file));
                if (inlineComments.containsKey(file)) {
                    count += inlineComments.get(file);
                }
                inlineComments.put(file, count);
            }
        }
        if (baseRevisionDraftComments != null) {
            for (String file : baseRevisionDraftComments.keySet()) {
                int count = computeNumberOfComments(baseRevisionDraftComments.get(file));
                if (draftComments.containsKey(file)) {
                    count += draftComments.get(file);
                }
                draftComments.put(file, count);
            }
        }

        // Fetch revision comments
        fetchNeededRevisionComments(response);

        // Join the actions
        response.mFiles = files;
        if (response.mActions == null) {
            response.mActions = response.mChange.actions;
        } else {
            response.mActions.putAll(response.mChange.actions);
        }

        response.mSubmitType = submitType;
        response.mInlineComments = inlineComments;
        response.mDraftComments = draftComments;
        return response;
    }

    private void performOpenFileDiff(String file) {
        // Resolve base diff
        String base = null;
        if (mDiffAgainstRevision != null) {
            base = String.valueOf(mResponse.mChange.revisions.get(mDiffAgainstRevision).number);
        }
        String current = String.valueOf(mResponse.mChange.revisions.get(mCurrentRevision).number);

        ArrayList<String> files = new ArrayList<>(mResponse.mFiles.keySet());
        ActivityHelper.openDiffViewerActivity(this, mResponse.mChange, files, mResponse.mFiles, mCurrentRevision,
                base, current, file, null, DIFF_REQUEST_CODE);
    }

    private void performNavigateToComment(CommentInfo comment) {
        // Resolve base diff
        String base = null;
        if (mDiffAgainstRevision != null) {
            base = String.valueOf(mResponse.mChange.revisions.get(mDiffAgainstRevision).number);
        }
        String current = String.valueOf(comment.patchSet);
        String revision = mCurrentRevision;
        for (String rev : mResponse.mChange.revisions.keySet()) {
            if (mResponse.mChange.revisions.get(rev).number == comment.patchSet) {
                revision = rev;
                break;
            }
        }

        ActivityHelper.openDiffViewerActivity(this, mResponse.mChange, null, null, revision, base, current,
                comment.path, comment.id, DIFF_REQUEST_CODE);

    }

    private void sortRevisions(ChangeInfo change) {
        mAllRevisions.clear();
        for (String revision : change.revisions.keySet()) {
            RevisionInfo rev = change.revisions.get(revision);
            rev.commit.commit = revision;
            mAllRevisions.add(rev);
        }
        Collections.sort(mAllRevisions, (o1, o2) -> {
            if (o1.number > o2.number) {
                return -1;
            }
            if (o1.number < o2.number) {
                return 1;
            }
            return 0;
        });

        // All revisions + base - current revision
        mAllRevisionsWithBase.clear();
        mAllRevisionsWithBase.addAll(mAllRevisions);
        int count = mAllRevisionsWithBase.size();
        for (int i = 0; i < count; i++) {
            RevisionInfo revision = mAllRevisionsWithBase.get(i);
            if (revision.commit.commit.equals(mCurrentRevision)) {
                mAllRevisionsWithBase.remove(i);
                break;
            }
        }
        mAllRevisionsWithBase.add(new RevisionInfo());
    }

    private void showPatchSetChooser(View anchor) {
        if (isLocked()) {
            return;
        }

        final ListPopupWindow popupWindow = new ListPopupWindow(getContext());
        PatchSetsAdapter adapter = new PatchSetsAdapter(getContext(), mAllRevisions, mCurrentRevision);
        popupWindow.setAnchorView(anchor);
        popupWindow.setAdapter(adapter);
        popupWindow.setContentWidth(adapter.measureContentWidth());
        popupWindow.setOnItemClickListener((parent, view, position, id) -> {
            popupWindow.dismiss();
            mCurrentRevision = mAllRevisions.get(position).commit.commit;
            // Restore diff against to base
            mDiffAgainstRevision = null;
            forceRefresh();
        });
        popupWindow.setModal(true);
        popupWindow.show();
    }

    private void showDiffAgainstChooser(View anchor) {
        if (isLocked()) {
            return;
        }

        final ListPopupWindow popupWindow = new ListPopupWindow(getContext());
        PatchSetsAdapter adapter = new PatchSetsAdapter(getContext(), mAllRevisionsWithBase, mDiffAgainstRevision);
        popupWindow.setAnchorView(anchor);
        popupWindow.setAdapter(adapter);
        popupWindow.setContentWidth(adapter.measureContentWidth());
        popupWindow.setOnItemClickListener((parent, view, position, id) -> {
            popupWindow.dismiss();
            String commit = null;
            if (mAllRevisionsWithBase.get(position).commit != null) {
                commit = mAllRevisionsWithBase.get(position).commit.commit;
            }
            mDiffAgainstRevision = commit;
            forceRefresh();
        });
        popupWindow.setModal(true);
        popupWindow.show();
    }

    private void showEditChangeActivity() {
        if (!isLocked()) {
            ActivityHelper.editChange(this, mResponse.mChange.legacyChangeId, mResponse.mChange.changeId,
                    mCurrentRevision, EDIT_REQUEST_CODE);
        }
    }

    private void toggleTaggedMessages() {
        if (!isLocked()) {
            mModel.isLocked = true;
            if (mModel.msgListModel.actions.containsKey("action1")) {
                mHideTaggedMessages = !mHideTaggedMessages;
                mModel.msgListModel.actions.put("action1",
                        getString(mHideTaggedMessages ? R.string.change_details_action_show_tagged_messages
                                : R.string.change_details_action_hide_tagged_messages));

                mMessageAdapter.updateHideTaggedMessages(mHideTaggedMessages);
                mMessageAdapter.update(mModel.msgListModel, mResponse.mChange.messages,
                        mResponse.mMessagesWithComments);
                mBinding.setModel(mModel);
            }
            mModel.isLocked = false;
        }
    }

    private void toggleCIMessages() {
        if (!isLocked()) {
            mModel.isLocked = true;
            if (mModel.msgListModel.actions.containsKey("action2")) {
                mHideCIMessages = !mHideCIMessages;
                mModel.msgListModel.actions.put("action2",
                        getString(mHideCIMessages ? R.string.change_details_action_show_ci_messages
                                : R.string.change_details_action_hide_ci_messages));

                Repository repo = null;
                if (mHideCIMessages) {
                    repo = ModelHelper.findRepositoryForAccount(getContext(), mAccount);
                }
                mMessageAdapter.updateHideCIMessages(repo);
                mMessageAdapter.update(mModel.msgListModel, mResponse.mChange.messages,
                        mResponse.mMessagesWithComments);
                updateChangeInfo(mResponse);
                mBinding.setModel(mModel);
            }
            mModel.isLocked = false;
        }
    }

    private void performStarred(boolean starred) {
        if (!isLocked()) {
            mStarredLoader.clear();
            mStarredLoader.restart(starred);
        }
    }

    private void performAccountClicked(AccountInfo account, Object tag) {
        ChangeQuery filter = new ChangeQuery().owner(ModelHelper.getSafeAccountOwner(account));
        String title = getString(R.string.account_details);
        String displayName = ModelHelper.getAccountDisplayName(account);
        String extra = SerializationManager.getInstance().toJson(account);
        ActivityHelper.openStatsActivity(getContext(), title, displayName, StatsFragment.ACCOUNT_STATS,
                String.valueOf(account.accountId), filter, extra);
    }

    private void performRemoveReviewer(AccountInfo account, Object tag) {
        if (!isLocked()) {
            mRemoveReviewerLoader.clear();
            mRemoveReviewerLoader.restart(account);
        }
    }

    private void performRemoveReviewerVote(AccountInfo account, Object tag) {
        if (!isLocked()) {
            mRemoveReviewerVoteLoader.clear();
            mRemoveReviewerVoteLoader.restart(new Pair<>((String) tag, account));
        }
    }

    private void performMessagesRefresh() {
        if (!isLocked()) {
            mMessagesRefreshLoader.clear();
            mMessagesRefreshLoader.restart();
        }
    }

    private void performDraftsRefresh() {
        if (!isLocked()) {
            mDraftsRefreshLoader.clear();
            mDraftsRefreshLoader.restart();
        }
    }

    private void performReview() {
        if (!isLocked()) {
            String message = StringHelper
                    .obtainQuoteFromMessage(mBinding.reviewInfo.reviewComment.getText().toString());
            Map<String, Integer> review = mBinding.reviewInfo.reviewLabels.getReview(false);

            ReviewInput input = new ReviewInput();
            input.drafts = DraftActionType.PUBLISH_ALL_REVISIONS;
            input.strictLabels = true;
            if (!review.isEmpty()) {
                input.labels = review;
            }
            if (!TextUtils.isEmpty(message)) {
                input.message = message;
            }
            input.omitDuplicateComments = true;
            input.notify = NotifyType.ALL;

            mReviewLoader.clear();
            mReviewLoader.restart(input);
        }
    }

    private void updateLocked() {
        mBinding.patchSetInfo.setIsLocked(isLocked());
        mBinding.changeInfo.setIsLocked(isLocked());
        mBinding.reviewInfo.setIsLocked(isLocked());
        mBinding.executePendingBindings();
    }

    private void updateAuthenticatedAndOwnerStatus() {
        mBinding.patchSetInfo.setIsAuthenticated(mModel.isAuthenticated);
        mBinding.changeInfo.setIsAuthenticated(mModel.isAuthenticated);
        mBinding.reviewInfo.setIsAuthenticated(mModel.isAuthenticated);

        final boolean isOwner = mModel.isAuthenticated && mResponse != null
                && mResponse.mChange.owner.accountId == mAccount.mAccount.accountId;
        mBinding.patchSetInfo.setIsOwner(isOwner);
        mBinding.changeInfo.setIsOwner(isOwner);
        mBinding.executePendingBindings();
    }

    private void performOpenRelatedChanges() {
        ActivityHelper.openRelatedChangesActivity(getContext(), mResponse.mChange, mCurrentRevision);
    }

    @SuppressWarnings("ConstantConditions")
    private void performViewPatchSet() {
        final Context ctx = getActivity();
        final GerritApi api = ModelHelper.getGerritApi(ctx);
        RevisionInfo revision = mResponse.mChange.revisions.get(mCurrentRevision);
        Uri uri = api.getRevisionUri(String.valueOf(mLegacyChangeId), String.valueOf(revision.number));

        ActivityHelper.openUriInCustomTabs(getActivity(), uri, true);
    }

    @SuppressWarnings("ConstantConditions")
    private void performEditMessage(View v) {
        String title = getString(R.string.change_edit_message_title);
        String action = getString(R.string.action_edit);
        String hint = getString(R.string.change_edit_message_hint);

        String message = mResponse.mChange.revisions.get(mCurrentRevision).commit.message;

        EditDialogFragment fragment = EditDialogFragment.newInstance(title, null, message, action, hint, false,
                true, true, v, REQUEST_CODE_EDIT_MESSAGE, null);
        fragment.show(getChildFragmentManager(), EditDialogFragment.TAG);
    }

    @SuppressWarnings("ConstantConditions")
    private void performEditRevisionDescription(View v) {
        String title = getString(R.string.change_edit_revision_description_title);
        String action = getString(R.string.action_edit);
        String hint = getString(R.string.change_edit_revision_description_hint);

        String message = mResponse.mChange.revisions.get(mCurrentRevision).description;

        EditDialogFragment fragment = EditDialogFragment.newInstance(title, null, message, action, hint, false,
                true, true, v, REQUEST_CODE_EDIT_REVISION_DESCRIPTION, null);
        fragment.show(getChildFragmentManager(), EditDialogFragment.TAG);
    }

    private void performOpenDownloadDialog() {
        DownloadDialogFragment fragment = DownloadDialogFragment.newInstance(mResponse.mChange, mCurrentRevision);
        fragment.show(getChildFragmentManager(), DownloadDialogFragment.TAG);
    }

    @SuppressWarnings("ConstantConditions")
    private void performShare() {
        final Context ctx = getActivity();
        final GerritApi api = ModelHelper.getGerritApi(ctx);
        Uri uri = api.getChangeUri(String.valueOf(mLegacyChangeId));

        String action = getString(R.string.action_share);
        String title = getString(R.string.change_details_title, mLegacyChangeId);
        ActivityHelper.share(getContext(), action, title, uri.toString());
    }

    private void performShowAddReviewerDialog(AddReviewerState reviewerState, View v) {
        AddReviewerDialogFragment fragment = AddReviewerDialogFragment.newInstance(mLegacyChangeId, reviewerState,
                v);
        fragment.show(getChildFragmentManager(), AddReviewerDialogFragment.TAG);
    }

    private void performShowEditAssigneeDialog(View v) {
        EditAssigneeDialogFragment fragment = EditAssigneeDialogFragment.newInstance(v);
        fragment.show(getChildFragmentManager(), EditAssigneeDialogFragment.TAG);
    }

    private void performShowMoveBranchDialog(View v) {
        BranchChooserDialogFragment fragment = BranchChooserDialogFragment.newInstance(R.string.move_branch_title,
                R.string.action_move, mResponse.mChange.project, mResponse.mChange.branch, null, v,
                REQUEST_CODE_MOVE_BRANCH);
        fragment.show(getChildFragmentManager(), BranchChooserDialogFragment.TAG);
    }

    private void performShowChangeTopicDialog(View v) {
        String title = getString(R.string.change_topic_title);
        String action = getString(R.string.action_change);
        String hint = getString(R.string.change_topic_hint);

        EditDialogFragment fragment = EditDialogFragment.newInstance(title, null, mResponse.mChange.topic, action,
                hint, true, true, false, v, REQUEST_CODE_CHANGE_TOPIC, null);
        fragment.show(getChildFragmentManager(), EditDialogFragment.TAG);
    }

    private void performShowChangeTagsDialog(View v) {
        final Tag[] tags = mBinding.changeInfo.tagsLabels.getTags();
        String title = getString(R.string.change_tags_title);
        String action = getString(R.string.action_save);
        TagEditDialogFragment fragment = TagEditDialogFragment.newInstance(title, tags, action, v,
                REQUEST_CODE_TAGS);
        fragment.show(getChildFragmentManager(), TagEditDialogFragment.TAG);
    }

    private void performShowRebaseDialog(View v) {
        BaseChooserDialogFragment fragment = BaseChooserDialogFragment.newInstance(mLegacyChangeId,
                mResponse.mChange.project, mResponse.mChange.branch, v, REQUEST_CODE_REBASE);
        fragment.show(getChildFragmentManager(), BaseChooserDialogFragment.TAG);
    }

    private void performShowCherryPickDialog(View v) {
        String message = mResponse.mChange.revisions.get(mCurrentRevision).commit.message;
        BranchChooserDialogFragment fragment = BranchChooserDialogFragment.newInstance(
                R.string.change_action_cherrypick, R.string.change_action_cherrypick, mResponse.mChange.project,
                mResponse.mChange.branch, message, v, REQUEST_CODE_CHERRY_PICK);
        fragment.show(getChildFragmentManager(), BranchChooserDialogFragment.TAG);
    }

    private void performShowRequestMessageDialog(View v, String title, String action, String hint, String text,
            boolean canBeEmpty, int requestCode) {
        EditDialogFragment fragment = EditDialogFragment.newInstance(title, null, text, action, hint, canBeEmpty,
                true, true, v, requestCode, null);
        fragment.show(getChildFragmentManager(), EditDialogFragment.TAG);
    }

    private void performConfirmDialog(View v, String message, int requestCode) {
        ConfirmDialogFragment fragment = ConfirmDialogFragment.newInstance(message, v, requestCode);
        fragment.show(getChildFragmentManager(), ConfirmDialogFragment.TAG);
    }

    private void performReplyComment(int position) {
        String currentMessage = mBinding.reviewInfo.reviewComment.getText().toString();
        String replyMessage = mMessageAdapter.getMessage(position);
        String msg = StringHelper.quoteMessage(currentMessage, replyMessage);
        mBinding.reviewInfo.reviewComment.setText(msg);
        mBinding.reviewInfo.reviewComment.setSelection(msg.length());
        mBinding.nestedScroll.fullScroll(View.FOCUS_DOWN);
    }

    private void performApplyFilter(View v) {
        String title = null;
        ChangeQuery filter = null;
        switch (v.getId()) {
        case R.id.project:
            title = getString(R.string.change_details_project);
            String project = ((TextView) v).getText().toString();
            filter = new ChangeQuery().project(project);
            ActivityHelper.openStatsActivity(getContext(), title, project, StatsFragment.PROJECT_STATS, project,
                    filter, null);
            return;
        case R.id.branch:
            title = getString(R.string.change_details_branch);
            filter = new ChangeQuery().branch(((TextView) v).getText().toString());
            break;
        case R.id.topic:
            title = getString(R.string.change_details_topic);
            filter = new ChangeQuery().topic(((TextView) v).getText().toString());
            break;
        }
        ActivityHelper.openChangeListByFilterActivity(getActivity(), title, filter, false, false);
    }

    private void performAction(View v) {
        if (!isLocked()) {
            String action;
            String hint;
            switch (v.getId()) {
            case R.id.cherrypick:
                performShowCherryPickDialog(v);
                break;

            case R.id.rebase:
                performShowRebaseDialog(v);
                break;

            case R.id.abandon:
                action = getString(R.string.change_action_abandon);
                hint = getString(R.string.actions_comment_hint);
                performShowRequestMessageDialog(v, action, action, hint, null, true, REQUEST_CODE_ABANDON_CHANGE);
                break;

            case R.id.restore:
                action = getString(R.string.change_action_restore);
                hint = getString(R.string.actions_comment_hint);
                performShowRequestMessageDialog(v, action, action, hint, null, true, REQUEST_CODE_RESTORE_CHANGE);
                break;

            case R.id.revert: {
                action = getString(R.string.change_action_revert);
                hint = getString(R.string.actions_message_hint);
                String message = getString(R.string.revert_msg_template,
                        mResponse.mChange.revisions.get(mCurrentRevision).commit.subject, mCurrentRevision);
                performShowRequestMessageDialog(v, action, action, hint, message, true, REQUEST_CODE_REVERT_CHANGE);
                break;
            }

            case R.id.publish_draft:
                mActionLoader.clear();
                mActionLoader.restart(ModelHelper.ACTION_PUBLISH_DRAFT, null);
                break;

            case R.id.delete_change:
                AlertDialog dialog = new AlertDialog.Builder(getContext())
                        .setTitle(R.string.delete_draft_change_title)
                        .setMessage(R.string.delete_draft_change_confirm)
                        .setPositiveButton(R.string.action_delete, (dialog1, which) -> {
                            mActionLoader.clear();
                            mActionLoader.restart(ModelHelper.ACTION_DELETE_CHANGE, null);
                        }).setNegativeButton(R.string.action_cancel, null).create();
                dialog.show();
                break;

            case R.id.follow_up:
                action = getString(R.string.change_action_follow_up);
                hint = getString(R.string.actions_message_hint);
                performShowRequestMessageDialog(v, action, action, hint, null, false,
                        REQUEST_CODE_FOLLOW_UP_CHANGE);
                break;

            case R.id.submit:
                String message = getString(R.string.actions_confirm_submit);
                performConfirmDialog(v, message, REQUEST_CODE_SUBMIT_CHANGE);
                break;
            }
        }
    }

    private void performSubmitChange(GerritApi api) {
        SubmitInput input = new SubmitInput();
        input.notify = NotifyType.ALL;
        api.submitChange(String.valueOf(mLegacyChangeId), input).blockingFirst();
    }

    private ChangeInfo performRebaseChange(GerritApi api, String base) {
        RebaseInput input = new RebaseInput();
        input.base = base;
        return api.rebaseChange(String.valueOf(mLegacyChangeId), input).blockingFirst();
    }

    private void performAbandonChange(GerritApi api, String msg) {
        AbandonInput input = new AbandonInput();
        input.notify = NotifyType.ALL;
        if (!TextUtils.isEmpty(msg)) {
            input.message = msg;
        }
        api.abandonChange(String.valueOf(mLegacyChangeId), input).blockingFirst();
    }

    private void performRestoreChange(GerritApi api, String msg) {
        RestoreInput input = new RestoreInput();
        if (!TextUtils.isEmpty(msg)) {
            input.message = msg;
        }
        api.restoreChange(String.valueOf(mLegacyChangeId), input).blockingFirst();
    }

    private ChangeInfo performRevertChange(GerritApi api, String msg) {
        RevertInput input = new RevertInput();
        input.notify = NotifyType.ALL;
        if (!TextUtils.isEmpty(msg)) {
            input.message = msg;
        }
        return api.revertChange(String.valueOf(mLegacyChangeId), input).blockingFirst();
    }

    private boolean performPublishDraft(GerritApi api) {
        api.publishChangeDraftRevision(String.valueOf(mLegacyChangeId), mCurrentRevision).blockingFirst();
        return true;
    }

    private void performDeleteChange(GerritApi api) {
        api.deleteDraftChange(String.valueOf(mLegacyChangeId)).blockingFirst();
    }

    private ChangeInfo performFollowUp(GerritApi api, String subject) {
        ChangeInput change = new ChangeInput();
        change.baseChange = mResponse.mChange.id;
        change.branch = mResponse.mChange.branch;
        change.project = mResponse.mChange.project;
        change.status = InitialChangeStatus.DRAFT;
        if (!TextUtils.isEmpty(mResponse.mChange.topic)) {
            change.topic = mResponse.mChange.topic;
        }
        change.subject = subject;
        return api.createChange(change).blockingFirst();
    }

    private ChangeInfo performCherryPickChange(GerritApi api, String branch, String msg) {
        String changeId = String.valueOf(mResponse.mChange.legacyChangeId);
        CherryPickInput input = new CherryPickInput();
        input.destination = branch;
        input.message = msg;
        return api.cherryPickChangeRevision(changeId, mCurrentRevision, input).blockingFirst();
    }

    private void performMessageClick(int position) {
        mMessageAdapter.changeFoldedStatus(position);
    }

    @SuppressWarnings("ConstantConditions")
    private void fetchNeededRevisionComments(DataResponse response) {
        if (!mIsInlineCommentsInMessages) {
            return;
        }

        // Determine which patchset needs request comments for
        List<Integer> revisionsWithComments = new ArrayList<>();
        if (response.mChange.messages != null) {
            for (ChangeMessageInfo message : response.mChange.messages) {
                if (message.message != null && COMMENTS_PATTERN.matcher(message.message).find()) {
                    if (!revisionsWithComments.contains(message.revisionNumber)) {
                        revisionsWithComments.add(message.revisionNumber);
                    }
                }
            }
        }

        // Fetch comments of needed revisions
        final Context ctx = getActivity();
        final GerritApi api = ModelHelper.getGerritApi(ctx);
        response.mMessagesWithComments.clear();
        for (int rev : revisionsWithComments) {
            try {
                Map<String, List<CommentInfo>> comments = api.getChangeRevisionComments(
                        String.valueOf(response.mChange.legacyChangeId), String.valueOf(rev)).blockingFirst();
                if (comments == null) {
                    continue;
                }

                updateMessageComments(response, comments);
            } catch (Exception ex) {
                Log.e(TAG, "Can't match comments for messages.", ex);
            }
        }
    }

    @SuppressWarnings("Convert2streamapi")
    private void updateMessageComments(DataResponse response, Map<String, List<CommentInfo>> comments) {
        final Map<String, LinkedHashMap<String, List<CommentInfo>>> mwc = response.mMessagesWithComments;

        // Match comments with messages
        for (ChangeMessageInfo message : response.mChange.messages) {
            if (message.message != null && COMMENTS_PATTERN.matcher(message.message).find()) {
                for (String file : comments.keySet()) {
                    List<CommentInfo> items = comments.get(file);
                    if (items != null) {
                        for (CommentInfo comment : items) {
                            comment.path = file;
                            if (comment.updated.compareTo(message.date) == 0
                                    && comment.author.accountId == message.author.accountId) {
                                if (!mwc.containsKey(message.id)) {
                                    mwc.put(message.id, new LinkedHashMap<>());
                                }

                                final LinkedHashMap<String, List<CommentInfo>> filesAndComments = mwc
                                        .get(message.id);
                                if (!filesAndComments.containsKey(file)) {
                                    filesAndComments.put(file, new ArrayList<>());
                                }

                                List<CommentInfo> list = filesAndComments.get(file);
                                comment.patchSet = message.revisionNumber;
                                list.add(comment);
                            }
                        }
                    }
                }
            }
        }
    }

    private String resolveDiffAgainstSelectorText() {
        if (mDiffAgainstRevision == null) {
            return getString(R.string.change_details_diff_against, getString(R.string.options_base));
        }

        for (RevisionInfo revision : mAllRevisions) {
            if (revision.commit.commit.equals(mDiffAgainstRevision)) {
                return getString(R.string.change_details_diff_against,
                        getString(R.string.change_details_diff_against_format, revision.number,
                                revision.commit.commit.substring(0, 10)));
            }
        }

        return null;
    }

    private String resolveDiffAgainstRevision(String base) {
        if (base == null) {
            return null;
        }

        int number = Integer.valueOf(base);
        for (String rev : mResponse.mChange.revisions.keySet()) {
            if (mResponse.mChange.revisions.get(rev).number == number) {
                return rev;
            }
        }
        return null;
    }

    private int computeNumberOfComments(List<CommentInfo> comments) {
        int count = 0;
        for (CommentInfo comment : comments) {
            if (mDiffAgainstRevision == null || !SideType.PARENT.equals(comment.side)) {
                count++;
            }
        }
        return count;
    }

    private boolean isLocked() {
        return mResponse == null || mResponse.mChange == null || mModel.isLocked;
    }

    @Override
    public void onReviewerAdded(String reviewer, AddReviewerState reviewerState) {
        if (!isLocked()) {
            mAddReviewerLoader.clear();
            mAddReviewerLoader.restart(reviewer, reviewerState);
        }
    }

    @Override
    public void onAssigneeSelected(String assignee) {
        if (!isLocked()) {
            mEditAssigneeLoader.clear();
            mEditAssigneeLoader.restart(assignee == null ? "" : assignee);
        }
    }

    @Override
    public void onFilterSelected(int requestCode, Object[] o) {
        if (!isLocked()) {
            switch (requestCode) {
            case REQUEST_CODE_REBASE:
                mActionLoader.clear();
                mActionLoader.restart(ModelHelper.ACTION_REBASE, new String[] { (String) o[0] });
                break;
            case REQUEST_CODE_CHERRY_PICK:
                String[] result = (String[]) o;
                mActionLoader.clear();
                mActionLoader.restart(ModelHelper.ACTION_CHERRY_PICK, new String[] { result[0], result[1] });
                break;
            case REQUEST_CODE_MOVE_BRANCH:
                mMoveBranchLoader.clear();
                mMoveBranchLoader.restart((String) o[0]);
                break;
            }
        }
    }

    @Override
    public void onEditChanged(int requestCode, Bundle requestData, String newValue) {
        if (!isLocked()) {
            switch (requestCode) {
            case REQUEST_CODE_EDIT_MESSAGE:
                ChangeEditMessageInput changeEditMessageInput = new ChangeEditMessageInput();
                changeEditMessageInput.message = newValue;
                mChangeEditMessageLoader.clear();
                mChangeEditMessageLoader.restart(changeEditMessageInput);
                break;

            case REQUEST_CODE_CHANGE_TOPIC:
                mChangeTopicLoader.clear();
                mChangeTopicLoader.restart(newValue);
                break;

            case REQUEST_CODE_ABANDON_CHANGE:
                mActionLoader.clear();
                mActionLoader.restart(ModelHelper.ACTION_ABANDON, new String[] { newValue });
                break;

            case REQUEST_CODE_RESTORE_CHANGE:
                mActionLoader.clear();
                mActionLoader.restart(ModelHelper.ACTION_RESTORE, new String[] { newValue });
                break;

            case REQUEST_CODE_REVERT_CHANGE:
                mActionLoader.clear();
                mActionLoader.restart(ModelHelper.ACTION_REVERT, new String[] { newValue });
                break;

            case REQUEST_CODE_FOLLOW_UP_CHANGE:
                mActionLoader.clear();
                mActionLoader.restart(ModelHelper.ACTION_FOLLOW_UP, new String[] { newValue });
                break;

            case REQUEST_CODE_EDIT_REVISION_DESCRIPTION:
                DescriptionInput descriptionInput = new DescriptionInput();
                descriptionInput.description = newValue;
                mChangeEditRevisionDescriptionLoader.clear();
                mChangeEditRevisionDescriptionLoader.restart(descriptionInput);
                break;
            }
        }
    }

    @Override
    public void onTagEditChanged(int requestCode, Tag[] tags) {
        // Generate tags to remove and to add
        List<String> oldTags = new ArrayList<>(Arrays.asList(mResponse.mChange.hashtags));
        List<String> newTags = new ArrayList<>();
        for (Tag tag : tags) {
            newTags.add(tag.toPlainTag().toString());
        }
        List<String> copy = new ArrayList<>(newTags);

        newTags.removeAll(oldTags);
        oldTags.removeAll(copy);

        // Save the tags
        mChangeTagsLoader.clear();
        mChangeTagsLoader.restart(newTags.toArray(new String[newTags.size()]),
                oldTags.toArray(new String[oldTags.size()]));
    }

    @Override
    public void onActionConfirmed(int requestCode) {
        if (!isLocked()) {
            switch (requestCode) {
            case REQUEST_CODE_SUBMIT_CHANGE:
                mActionLoader.clear();
                mActionLoader.restart(ModelHelper.ACTION_SUBMIT, null);
                break;
            }
        }
    }
}