/*
* Copyright 2011 Azwan Adli Abdullah
*
* 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.gh4a.activities;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.StringRes;
import android.support.design.widget.CoordinatorLayout;
import android.support.v4.app.Fragment;
import android.support.v4.content.Loader;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AlertDialog;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import com.gh4a.BasePagerActivity;
import com.gh4a.Gh4Application;
import com.gh4a.ProgressDialogTask;
import com.gh4a.R;
import com.gh4a.fragment.CommitCompareFragment;
import com.gh4a.fragment.PullRequestFilesFragment;
import com.gh4a.fragment.PullRequestFragment;
import com.gh4a.loader.IsCollaboratorLoader;
import com.gh4a.loader.IssueLoader;
import com.gh4a.loader.LoaderCallbacks;
import com.gh4a.loader.LoaderResult;
import com.gh4a.loader.PullRequestLoader;
import com.gh4a.utils.ApiHelpers;
import com.gh4a.utils.IntentUtils;
import com.gh4a.utils.UiUtils;
import com.gh4a.widget.IssueStateTrackingFloatingActionButton;
import org.eclipse.egit.github.core.Issue;
import org.eclipse.egit.github.core.MergeStatus;
import org.eclipse.egit.github.core.PullRequest;
import org.eclipse.egit.github.core.PullRequestMarker;
import org.eclipse.egit.github.core.RepositoryId;
import org.eclipse.egit.github.core.User;
import org.eclipse.egit.github.core.service.PullRequestService;
import java.io.IOException;
import java.util.Locale;
public class PullRequestActivity extends BasePagerActivity implements
View.OnClickListener, PullRequestFilesFragment.CommentUpdateListener {
public static Intent makeIntent(Context context, String repoOwner, String repoName, int number) {
return makeIntent(context, repoOwner, repoName, number, -1, null);
}
public static Intent makeIntent(Context context, String repoOwner, String repoName,
int number, int initialPage, IntentUtils.InitialCommentMarker initialComment) {
return new Intent(context, PullRequestActivity.class)
.putExtra("owner", repoOwner)
.putExtra("repo", repoName)
.putExtra("number", number)
.putExtra("initial_page", initialPage)
.putExtra("initial_comment", initialComment);
}
public static final int PAGE_CONVERSATION = 0;
public static final int PAGE_COMMITS = 1;
public static final int PAGE_FILES = 2;
private static final int REQUEST_EDIT_ISSUE = 1001;
private String mRepoOwner;
private String mRepoName;
private int mPullRequestNumber;
private int mInitialPage;
private IntentUtils.InitialCommentMarker mInitialComment;
private Boolean mIsCollaborator;
private Issue mIssue;
private PullRequest mPullRequest;
private PullRequestFragment mPullRequestFragment;
private IssueStateTrackingFloatingActionButton mEditFab;
private ViewGroup mHeader;
private int[] mHeaderColorAttrs;
private static final int[] TITLES = new int[]{
R.string.pull_request_conversation, R.string.commits, R.string.pull_request_files
};
private class MergeMethodDesc {
final @StringRes int textResId;
final String action;
public MergeMethodDesc(@StringRes int textResId, String action) {
this.textResId = textResId;
this.action = action;
}
@Override
public String toString() {
return getString(textResId);
}
}
private final LoaderCallbacks<PullRequest> mPullRequestCallback = new LoaderCallbacks<PullRequest>(this) {
@Override
protected Loader<LoaderResult<PullRequest>> onCreateLoader() {
return new PullRequestLoader(PullRequestActivity.this,
mRepoOwner, mRepoName, mPullRequestNumber);
}
@Override
protected void onResultReady(PullRequest result) {
mPullRequest = result;
fillHeader();
showContentIfReady();
supportInvalidateOptionsMenu();
}
};
private final LoaderCallbacks<Issue> mIssueCallback = new LoaderCallbacks<Issue>(this) {
@Override
protected Loader<LoaderResult<Issue>> onCreateLoader() {
return new IssueLoader(PullRequestActivity.this,
mRepoOwner, mRepoName, mPullRequestNumber);
}
@Override
protected void onResultReady(Issue result) {
mIssue = result;
showContentIfReady();
}
};
private final LoaderCallbacks<Boolean> mCollaboratorCallback = new LoaderCallbacks<Boolean>(this) {
@Override
protected Loader<LoaderResult<Boolean>> onCreateLoader() {
return new IsCollaboratorLoader(PullRequestActivity.this, mRepoOwner, mRepoName);
}
@Override
protected void onResultReady(Boolean result) {
mIsCollaborator = result;
showContentIfReady();
supportInvalidateOptionsMenu();
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LayoutInflater inflater = LayoutInflater.from(UiUtils.makeHeaderThemedContext(this));
mHeader = (ViewGroup) inflater.inflate(R.layout.issue_header, null);
mHeader.setClickable(false);
mHeader.setVisibility(View.GONE);
addHeaderView(mHeader, true);
ActionBar actionBar = getSupportActionBar();
actionBar.setTitle(getResources().getString(R.string.pull_request_title) + " #" + mPullRequestNumber);
actionBar.setSubtitle(mRepoOwner + "/" + mRepoName);
actionBar.setDisplayHomeAsUpEnabled(true);
setContentShown(false);
getSupportLoaderManager().initLoader(0, null, mPullRequestCallback);
getSupportLoaderManager().initLoader(1, null, mIssueCallback);
getSupportLoaderManager().initLoader(2, null, mCollaboratorCallback);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.pullrequest_menu, menu);
Gh4Application app = Gh4Application.get();
boolean authorized = app.isAuthorized();
boolean isCreator = mPullRequest != null
&& ApiHelpers.loginEquals(mPullRequest.getUser(), app.getAuthLogin());
boolean isClosed = mPullRequest != null
&& ApiHelpers.IssueState.CLOSED.equals(mPullRequest.getState());
boolean isCollaborator = mIsCollaborator != null && mIsCollaborator;
boolean closerIsCreator = mIssue != null
&& ApiHelpers.userEquals(mIssue.getUser(), mIssue.getClosedBy());
boolean canClose = mPullRequest != null && authorized && (isCreator || isCollaborator);
boolean canOpen = canClose && (isCollaborator || closerIsCreator);
boolean canMerge = canClose && isCollaborator;
if (!canClose || isClosed) {
menu.removeItem(R.id.pull_close);
}
if (!canOpen || !isClosed) {
menu.removeItem(R.id.pull_reopen);
} else if (isClosed && mPullRequest.isMerged()) {
menu.findItem(R.id.pull_reopen).setEnabled(false);
}
if (!canMerge) {
menu.removeItem(R.id.pull_merge);
} else if (mPullRequest.isMerged() || !mPullRequest.isMergeable()) {
MenuItem mergeItem = menu.findItem(R.id.pull_merge);
mergeItem.setEnabled(false);
}
if (mPullRequest == null) {
menu.removeItem(R.id.browser);
}
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.pull_merge:
showMergeDialog();
break;
case R.id.pull_close:
case R.id.pull_reopen:
showOpenCloseConfirmDialog(item.getItemId() == R.id.pull_reopen);
break;
case R.id.share:
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
shareIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_pull_subject,
mPullRequest.getNumber(), mPullRequest.getTitle(),
mRepoOwner + "/" + mRepoName));
shareIntent.putExtra(Intent.EXTRA_TEXT, mPullRequest.getHtmlUrl());
shareIntent = Intent.createChooser(shareIntent, getString(R.string.share_title));
startActivity(shareIntent);
break;
case R.id.browser:
IntentUtils.launchBrowser(this, Uri.parse(mPullRequest.getHtmlUrl()));
break;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_EDIT_ISSUE) {
if (resultCode == Activity.RESULT_OK) {
setResult(Activity.RESULT_OK);
onRefresh();
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
@Override
protected void onInitExtras(Bundle extras) {
super.onInitExtras(extras);
mRepoOwner = extras.getString("owner");
mRepoName = extras.getString("repo");
mPullRequestNumber = extras.getInt("number");
mInitialComment = extras.getParcelable("initial_comment");
mInitialPage = extras.getInt("initial_page", -1);
extras.remove("initial_comment");
extras.remove("initial_page");
}
@Override
public void onRefresh() {
mIssue = null;
mPullRequest = null;
mIsCollaborator = null;
setContentShown(false);
if (mEditFab != null) {
mEditFab.post(new Runnable() {
@Override
public void run() {
updateFabVisibility();
}
});
}
mHeader.setVisibility(View.GONE);
mHeaderColorAttrs = null;
forceLoaderReload(0, 1, 2);
invalidateTabs();
super.onRefresh();
}
@Override
protected int[] getTabTitleResIds() {
return mPullRequest != null && mIssue != null ? TITLES : null;
}
@Override
protected int[] getHeaderColorAttrs() {
return mHeaderColorAttrs;
}
@Override
protected Fragment makeFragment(int position) {
if (position == 1) {
PullRequestMarker base = mPullRequest.getBase();
PullRequestMarker head = mPullRequest.getHead();
return CommitCompareFragment.newInstance(mRepoOwner, mRepoName, mPullRequestNumber,
base.getLabel(), base.getSha(), head.getLabel(), head.getSha());
} else if (position == 2) {
return PullRequestFilesFragment.newInstance(mRepoOwner, mRepoName,
mPullRequestNumber, mPullRequest.getHead().getSha());
} else {
Fragment f = PullRequestFragment.newInstance(mPullRequest,
mIssue, mIsCollaborator, mInitialComment);
mInitialComment = null;
return f;
}
}
@Override
protected void onFragmentInstantiated(Fragment f, int position) {
if (position == 0) {
mPullRequestFragment = (PullRequestFragment) f;
}
}
@Override
protected void onFragmentDestroyed(Fragment f) {
if (f == mPullRequestFragment) {
mPullRequestFragment = null;
}
}
@Override
protected Intent navigateUp() {
return IssueListActivity.makeIntent(this, mRepoOwner, mRepoName, true);
}
@Override
public void onCommentsUpdated() {
if (mPullRequestFragment != null) {
mPullRequestFragment.reloadEvents(true);
}
}
@Override
public void onClick(View v) {
if (v == mEditFab) {
Intent editIntent = IssueEditActivity.makeEditIntent(this, mRepoOwner, mRepoName, mIssue);
startActivityForResult(editIntent, REQUEST_EDIT_ISSUE);
} else if (v.getId() == R.id.iv_gravatar) {
Intent intent = UserActivity.makeIntent(this, (User) v.getTag());
if (intent != null) {
startActivity(intent);
}
}
}
private void showContentIfReady() {
if (mPullRequest != null && mIssue != null && mIsCollaborator != null) {
setContentShown(true);
invalidateTabs();
updateFabVisibility();
if (mInitialPage >= 0 && mInitialPage < TITLES.length) {
getPager().setCurrentItem(mInitialPage);
mInitialPage = -1;
}
}
}
private void showOpenCloseConfirmDialog(final boolean reopen) {
@StringRes int messageResId = reopen
? R.string.reopen_pull_request_confirm : R.string.close_pull_request_confirm;
@StringRes int buttonResId = reopen
? R.string.pull_request_reopen : R.string.pull_request_close;
new AlertDialog.Builder(this)
.setMessage(messageResId)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setCancelable(false)
.setPositiveButton(buttonResId, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
new PullRequestOpenCloseTask(reopen).schedule();
}
})
.setNegativeButton(R.string.cancel, null)
.show();
}
private void showMergeDialog() {
LayoutInflater inflater = LayoutInflater.from(this);
String title = getString(R.string.pull_message_dialog_title, mPullRequest.getNumber());
View view = inflater.inflate(R.layout.pull_merge_message_dialog, null);
final View editorNotice = view.findViewById(R.id.notice);
final EditText editor = (EditText) view.findViewById(R.id.et_commit_message);
editor.setText(mPullRequest.getTitle());
final ArrayAdapter<MergeMethodDesc> adapter = new ArrayAdapter<>(this,
R.layout.pull_merge_method_item);
adapter.add(new MergeMethodDesc(R.string.pull_merge_method_merge,
PullRequestService.MERGE_METHOD_MERGE));
adapter.add(new MergeMethodDesc(R.string.pull_merge_method_squash,
PullRequestService.MERGE_METHOD_SQUASH));
adapter.add(new MergeMethodDesc(R.string.pull_merge_method_rebase,
PullRequestService.MERGE_METHOD_REBASE));
final Spinner mergeMethod = (Spinner) view.findViewById(R.id.merge_method);
mergeMethod.setAdapter(adapter);
mergeMethod.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
int editorVisibility = position == 2 ? View.GONE : View.VISIBLE;
editorNotice.setVisibility(editorVisibility);
editor.setVisibility(editorVisibility);
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {
}
});
new AlertDialog.Builder(this)
.setTitle(title)
.setView(view)
.setPositiveButton(R.string.pull_request_merge, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String text = editor.getText() == null ? null : editor.getText().toString();
int methodIndex = mergeMethod.getSelectedItemPosition();
String method = adapter.getItem(methodIndex).action;
new PullRequestMergeTask(text, method).schedule();
}
})
.setNegativeButton(getString(R.string.cancel), null)
.show();
}
private void updateTabRightMargin(int dimensionResId) {
int margin = dimensionResId != 0
? getResources().getDimensionPixelSize(dimensionResId) : 0;
View tabs = findViewById(R.id.tabs);
ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) tabs.getLayoutParams();
lp.rightMargin = margin;
tabs.setLayoutParams(lp);
}
private void updateFabVisibility() {
boolean isIssueOwner = mIssue != null
&& ApiHelpers.loginEquals(mIssue.getUser(), Gh4Application.get().getAuthLogin());
boolean isCollaborator = mIsCollaborator != null && mIsCollaborator;
boolean shouldHaveFab = (isIssueOwner || isCollaborator)
&& mPullRequest != null && mIssue != null;
CoordinatorLayout rootLayout = getRootLayout();
if (shouldHaveFab && mEditFab == null) {
mEditFab = (IssueStateTrackingFloatingActionButton)
getLayoutInflater().inflate(R.layout.issue_edit_fab, rootLayout, false);
mEditFab.setOnClickListener(this);
rootLayout.addView(mEditFab);
updateTabRightMargin(R.dimen.mini_fab_size_with_margin);
} else if (!shouldHaveFab && mEditFab != null) {
rootLayout.removeView(mEditFab);
updateTabRightMargin(0);
mEditFab = null;
}
if (mEditFab != null) {
mEditFab.setState(mPullRequest.getState());
mEditFab.setMerged(mPullRequest.isMerged());
}
}
private void fillHeader() {
final int stateTextResId;
if (mPullRequest.isMerged()) {
stateTextResId = R.string.pull_request_merged;
mHeaderColorAttrs = new int[] {
R.attr.colorPullRequestMerged, R.attr.colorPullRequestMergedDark
};
} else if (ApiHelpers.IssueState.CLOSED.equals(mPullRequest.getState())) {
stateTextResId = R.string.closed;
mHeaderColorAttrs = new int[] {
R.attr.colorIssueClosed, R.attr.colorIssueClosedDark
};
} else {
stateTextResId = R.string.open;
mHeaderColorAttrs = new int[] {
R.attr.colorIssueOpen, R.attr.colorIssueOpenDark
};
}
TextView tvState = (TextView) mHeader.findViewById(R.id.tv_state);
tvState.setText(getString(stateTextResId).toUpperCase(Locale.getDefault()));
TextView tvTitle = (TextView) mHeader.findViewById(R.id.tv_title);
tvTitle.setText(mPullRequest.getTitle());
mHeader.setVisibility(View.VISIBLE);
}
private void handlePullRequestUpdate() {
if (mPullRequestFragment != null) {
mPullRequestFragment.updateState(mPullRequest);
}
if (mIssue != null) {
mIssue.setState(mPullRequest.getState());
if (ApiHelpers.IssueState.CLOSED.equals(mIssue.getState())) {
// if we came here, we either closed or merged the PR ourselves,
// so set the 'closed by' field accordingly
mIssue.setClosedBy(new User().setLogin(Gh4Application.get().getAuthLogin()));
}
}
fillHeader();
updateFabVisibility();
transitionHeaderToColor(mHeaderColorAttrs[0], mHeaderColorAttrs[1]);
supportInvalidateOptionsMenu();
}
private class PullRequestOpenCloseTask extends ProgressDialogTask<PullRequest> {
private final boolean mOpen;
public PullRequestOpenCloseTask(boolean open) {
super(getBaseActivity(), open ? R.string.opening_msg : R.string.closing_msg);
mOpen = open;
}
@Override
protected ProgressDialogTask<PullRequest> clone() {
return new PullRequestOpenCloseTask(mOpen);
}
@Override
protected PullRequest run() throws IOException {
PullRequestService pullService = (PullRequestService)
Gh4Application.get().getService(Gh4Application.PULL_SERVICE);
RepositoryId repoId = new RepositoryId(mRepoOwner, mRepoName);
PullRequest pullRequest = new PullRequest();
pullRequest.setNumber(mPullRequest.getNumber());
pullRequest.setState(mOpen ? ApiHelpers.IssueState.OPEN : ApiHelpers.IssueState.CLOSED);
return pullService.editPullRequest(repoId, pullRequest);
}
@Override
protected void onSuccess(PullRequest result) {
mPullRequest = result;
handlePullRequestUpdate();
}
@Override
protected String getErrorMessage() {
int errorMessageResId =
mOpen ? R.string.issue_error_reopen : R.string.issue_error_close;
return getContext().getString(errorMessageResId, mPullRequest.getNumber());
}
}
private class PullRequestMergeTask extends ProgressDialogTask<MergeStatus> {
private final String mCommitMessage;
private final String mMergeMethod;
public PullRequestMergeTask(String commitMessage, String mergeMethod) {
super(getBaseActivity(), R.string.merging_msg);
mCommitMessage = commitMessage;
mMergeMethod = mergeMethod;
}
@Override
protected ProgressDialogTask<MergeStatus> clone() {
return new PullRequestMergeTask(mCommitMessage, mMergeMethod);
}
@Override
protected MergeStatus run() throws Exception {
PullRequestService pullService = (PullRequestService)
Gh4Application.get().getService(Gh4Application.PULL_SERVICE);
RepositoryId repoId = new RepositoryId(mRepoOwner, mRepoName);
return pullService.merge(repoId, mPullRequest.getNumber(), mCommitMessage, mMergeMethod);
}
@Override
protected void onSuccess(MergeStatus result) {
if (result.isMerged()) {
mPullRequest.setMerged(true);
mPullRequest.setState(ApiHelpers.IssueState.CLOSED);
}
handlePullRequestUpdate();
}
@Override
protected String getErrorMessage() {
return getContext().getString(R.string.pull_error_merge, mPullRequest.getNumber());
}
}
}