/*
* 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.os.Bundle;
import android.os.PersistableBundle;
import android.support.annotation.StringRes;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.app.Fragment;
import android.support.v4.content.Loader;
import android.support.v4.view.MenuItemCompat;
import android.support.v4.view.ViewCompat;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.SearchView;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import com.gh4a.BasePagerActivity;
import com.gh4a.Gh4Application;
import com.gh4a.R;
import com.gh4a.fragment.IssueListFragment;
import com.gh4a.fragment.LoadingListFragmentBase;
import com.gh4a.loader.AssigneeListLoader;
import com.gh4a.loader.IsCollaboratorLoader;
import com.gh4a.loader.LabelListLoader;
import com.gh4a.loader.LoaderCallbacks;
import com.gh4a.loader.LoaderResult;
import com.gh4a.loader.MilestoneListLoader;
import com.gh4a.loader.ProgressDialogLoaderCallbacks;
import com.gh4a.utils.ApiHelpers;
import org.eclipse.egit.github.core.Issue;
import org.eclipse.egit.github.core.Label;
import org.eclipse.egit.github.core.Milestone;
import org.eclipse.egit.github.core.User;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
public class IssueListActivity extends BasePagerActivity implements
View.OnClickListener, LoadingListFragmentBase.OnRecyclerViewCreatedListener,
SearchView.OnCloseListener, SearchView.OnQueryTextListener,
MenuItemCompat.OnActionExpandListener {
public static Intent makeIntent(Context context, String repoOwner, String repoName) {
return makeIntent(context, repoOwner, repoName, false);
}
public static Intent makeIntent(Context context, String repoOwner, String repoName,
boolean isPullRequest) {
return new Intent(context, IssueListActivity.class)
.putExtra("owner", repoOwner)
.putExtra("repo", repoName)
.putExtra("is_pull_request", isPullRequest);
}
private static final int REQUEST_ISSUE_CREATE = 1001;
private static final int REQUEST_ISSUE_AFTER_CREATE = 1002;
private String mRepoOwner;
private String mRepoName;
private String mUserLogin;
private boolean mIsPullRequest;
private String mSelectedLabel;
private String mSelectedMilestone;
private String mSelectedAssignee;
private String mSearchQuery;
private boolean mSearchMode;
private int mSelectedParticipatingStatus = 0;
private FloatingActionButton mCreateFab;
private IssueListFragment mOpenFragment;
private Boolean mIsCollaborator;
private List<Label> mLabels;
private List<Milestone> mMilestones;
private List<User> mAssignees;
private final IssueListFragment.SortDrawerHelper mSortHelper =
new IssueListFragment.SortDrawerHelper();
private static final String STATE_KEY_SEARCH_QUERY = "search_query";
private static final String STATE_KEY_SEARCH_MODE = "search_mode";
private static final String LIST_QUERY = "is:%s %s repo:%s/%s %s %s %s %s";
private static final String SEARCH_QUERY = "is:%s %s repo:%s/%s %s";
private static final int[] TITLES = new int[] {
R.string.open, R.string.closed
};
private static final int[] PULL_REQUEST_TITLES = new int[] {
R.string.open, R.string.closed, R.string.merged
};
private static final int[][] HEADER_COLOR_ATTRS = new int[][] {
{ R.attr.colorIssueOpen, R.attr.colorIssueOpenDark },
{ R.attr.colorIssueClosed, R.attr.colorIssueClosedDark },
{ R.attr.colorPullRequestMerged, R.attr.colorPullRequestMergedDark }
};
private final LoaderCallbacks<List<Label>> mLabelCallback =
new ProgressDialogLoaderCallbacks<List<Label>>(this, this) {
@Override
protected Loader<LoaderResult<List<Label>>> onCreateLoader() {
return new LabelListLoader(IssueListActivity.this, mRepoOwner, mRepoName);
}
@Override
protected void onResultReady(List<Label> result) {
mLabels = result;
showLabelsDialog();
getSupportLoaderManager().destroyLoader(0);
}
};
private final LoaderCallbacks<List<Milestone>> mMilestoneCallback =
new ProgressDialogLoaderCallbacks<List<Milestone>>(this, this) {
@Override
protected Loader<LoaderResult<List<Milestone>>> onCreateLoader() {
return new MilestoneListLoader(IssueListActivity.this, mRepoOwner, mRepoName,
ApiHelpers.IssueState.OPEN);
}
@Override
protected void onResultReady(List<Milestone> result) {
mMilestones = result;
showMilestonesDialog();
getSupportLoaderManager().destroyLoader(1);
}
};
private final LoaderCallbacks<List<User>> mAssigneeListCallback =
new ProgressDialogLoaderCallbacks<List<User>>(this, this) {
@Override
protected Loader<LoaderResult<List<User>>> onCreateLoader() {
return new AssigneeListLoader(IssueListActivity.this, mRepoOwner, mRepoName);
}
@Override
protected void onResultReady(List<User> result) {
mAssignees = result;
showAssigneesDialog();
getSupportLoaderManager().destroyLoader(2);
}
};
private final LoaderCallbacks<Boolean> mIsCollaboratorCallback = new LoaderCallbacks<Boolean>(this) {
@Override
protected Loader<LoaderResult<Boolean>> onCreateLoader() {
return new IsCollaboratorLoader(IssueListActivity.this, mRepoOwner, mRepoName);
}
@Override
protected void onResultReady(Boolean result) {
if (mIsCollaborator == null) {
mIsCollaborator = result;
if (mIsCollaborator) {
updateRightNavigationDrawer();
}
}
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mUserLogin = Gh4Application.get().getAuthLogin();
if (savedInstanceState != null) {
mSearchQuery = savedInstanceState.getString(STATE_KEY_SEARCH_QUERY);
mSearchMode = savedInstanceState.getBoolean(STATE_KEY_SEARCH_MODE);
}
if (!mIsPullRequest && Gh4Application.get().isAuthorized()) {
CoordinatorLayout rootLayout = getRootLayout();
mCreateFab = (FloatingActionButton) getLayoutInflater().inflate(
R.layout.add_fab, rootLayout, false);
mCreateFab.setOnClickListener(this);
rootLayout.addView(mCreateFab);
}
getSupportLoaderManager().initLoader(3, null, mIsCollaboratorCallback);
ActionBar actionBar = getSupportActionBar();
actionBar.setTitle(mIsPullRequest ? R.string.pull_requests : R.string.issues);
actionBar.setSubtitle(mRepoOwner + "/" + mRepoName);
actionBar.setDisplayHomeAsUpEnabled(true);
}
@Override
protected void onInitExtras(Bundle extras) {
super.onInitExtras(extras);
mRepoOwner = extras.getString("owner");
mRepoName = extras.getString("repo");
mIsPullRequest = extras.getBoolean("is_pull_request");
}
@Override
protected void onDestroy() {
super.onDestroy();
}
@Override
public void onRefresh() {
mAssignees = null;
mMilestones = null;
mLabels = null;
mIsCollaborator = null;
updateRightNavigationDrawer();
forceLoaderReload(0, 1, 2, 3);
super.onRefresh();
}
@Override
public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) {
super.onSaveInstanceState(outState, outPersistentState);
outState.putString(STATE_KEY_SEARCH_QUERY, mSearchQuery);
outState.putBoolean(STATE_KEY_SEARCH_MODE, mSearchMode);
}
@Override
protected int[] getTabTitleResIds() {
return mIsPullRequest ? PULL_REQUEST_TITLES : TITLES;
}
@Override
public void onRecyclerViewCreated(Fragment fragment, RecyclerView recyclerView) {
if (fragment == mOpenFragment) {
recyclerView.setTag(R.id.FloatingActionButtonScrollEnabled, new Object());
}
}
@Override
protected void onPageMoved(int position, float fraction) {
super.onPageMoved(position, fraction);
if (!mSearchMode && mCreateFab != null) {
float openFraction = 1 - position - fraction;
ViewCompat.setScaleX(mCreateFab, openFraction);
ViewCompat.setScaleY(mCreateFab, openFraction);
mCreateFab.setVisibility(openFraction == 0 ? View.INVISIBLE : View.VISIBLE);
}
}
@Override
protected int[][] getTabHeaderColorAttrs() {
return HEADER_COLOR_ATTRS;
}
@Override
protected Fragment makeFragment(int position) {
final @StringRes int emptyTextResId;
final Map<String, String> filterData = new HashMap<>();
filterData.put("sort", mSortHelper.getSortMode());
filterData.put("order", mSortHelper.getSortOrder());
if (mSearchMode) {
filterData.put("q", String.format(Locale.US, SEARCH_QUERY,
mIsPullRequest ? "pr" : "issue",
getIssueType(position), mRepoOwner, mRepoName, mSearchQuery));
emptyTextResId = mIsPullRequest
? R.string.no_search_pull_requests_found : R.string.no_search_issues_found;
} else {
filterData.put("q", String.format(Locale.US, LIST_QUERY,
mIsPullRequest ? "pr" : "issue",
getIssueType(position), mRepoOwner, mRepoName,
buildFilterItem("assignee", mSelectedAssignee),
buildFilterItem("label", mSelectedLabel),
buildFilterItem("milestone", mSelectedMilestone),
buildParticipatingFilterItem()).trim());
emptyTextResId = mIsPullRequest
? R.string.no_pull_requests_found : R.string.no_issues_found;
}
return IssueListFragment.newInstance(filterData,
getIssueState(position), emptyTextResId, false);
}
@Override
protected void onFragmentInstantiated(Fragment f, int position) {
if (position == 0) {
mOpenFragment = (IssueListFragment) f;
}
}
@Override
protected void onFragmentDestroyed(Fragment f) {
if (f == mOpenFragment) {
mOpenFragment = null;
}
}
@Override
protected boolean fragmentNeedsRefresh(Fragment object) {
return true;
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_ISSUE_CREATE) {
if (resultCode == Activity.RESULT_OK && data.hasExtra("issue")) {
Issue issue = (Issue) data.getSerializableExtra("issue");
Intent intent = IssueActivity.makeIntent(this, mRepoOwner, mRepoName,
issue.getNumber());
startActivityForResult(intent, REQUEST_ISSUE_AFTER_CREATE);
}
} else if (requestCode == REQUEST_ISSUE_AFTER_CREATE) {
// Refresh all fragments (instead of just open issues fragment) when coming back from
// newly created issue as there is a chance that it could be immediately closed
super.onRefresh();
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
@Override
protected int[] getRightNavigationDrawerMenuResources() {
int[] menuResIds = new int[mSearchMode ? 1 : 2];
menuResIds[0] = IssueListFragment.SortDrawerHelper.getMenuResId();
if (!mSearchMode) {
menuResIds[1] = mIsCollaborator != null && mIsCollaborator
? R.menu.issue_list_filter_collab : R.menu.issue_list_filter;
}
return menuResIds;
}
@Override
protected int getInitialRightDrawerSelection() {
return R.id.sort_created_desc;
}
@Override
public boolean onNavigationItemSelected(MenuItem item) {
super.onNavigationItemSelected(item);
if (mSortHelper.handleItemSelection(item)) {
invalidateFragments();
return true;
}
switch (item.getItemId()) {
case R.id.filter_by_assignee:
filterAssignee();
return true;
case R.id.filter_by_label:
filterLabel();
return true;
case R.id.filter_by_milestone:
filterMilestone();
return true;
case R.id.filter_by_participating:
filterParticipating();
return true;
case R.id.manage_labels:
startActivity(IssueLabelListActivity.makeIntent(this,
mRepoOwner, mRepoName, mIsPullRequest));
return true;
case R.id.manage_milestones:
startActivity(IssueMilestoneListActivity.makeIntent(this,
mRepoOwner, mRepoName, mIsPullRequest));
return true;
}
return false;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.issue_list_menu, menu);
MenuItem searchItem = menu.findItem(R.id.search);
MenuItemCompat.setOnActionExpandListener(searchItem, this);
final SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchItem);
if (mSearchQuery != null) {
MenuItemCompat.expandActionView(searchItem);
searchView.setQuery(mSearchQuery, false);
}
searchView.setOnCloseListener(this);
searchView.setOnQueryTextListener(this);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.overflow) {
toggleRightSideDrawer();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public boolean onMenuItemActionExpand(MenuItem item) {
return true;
}
@Override
public boolean onMenuItemActionCollapse(MenuItem item) {
mSearchQuery = null;
setSearchMode(false);
return true;
}
@Override
public boolean onClose() {
mSearchQuery = null;
setSearchMode(false);
return true;
}
@Override
public boolean onQueryTextSubmit(String query) {
mSearchQuery = query;
setSearchMode(true);
return true;
}
@Override
public boolean onQueryTextChange(String newText) {
mSearchQuery = newText;
return false;
}
@Override
public void onClick(View view) {
Intent intent = IssueEditActivity.makeCreateIntent(this, mRepoOwner, mRepoName);
startActivityForResult(intent, REQUEST_ISSUE_CREATE);
}
@Override
protected Intent navigateUp() {
return RepositoryActivity.makeIntent(this, mRepoOwner, mRepoName);
}
private void setSearchMode(boolean enabled) {
boolean changed = mSearchMode != enabled;
mSearchMode = enabled;
if (mCreateFab != null) {
mCreateFab.setVisibility(enabled ? View.GONE : View.VISIBLE);
}
invalidateFragments();
if (changed) {
updateRightNavigationDrawer();
}
}
private String getIssueState(int position) {
switch (position) {
case 1:
return ApiHelpers.IssueState.CLOSED;
case 2:
return ApiHelpers.IssueState.MERGED;
default:
return ApiHelpers.IssueState.OPEN;
}
}
private String getIssueType(int position) {
String type = "is:" + getIssueState(position);
if (position == 1 && mIsPullRequest) {
type += " is:" + ApiHelpers.IssueState.UNMERGED;
}
return type;
}
private String buildParticipatingFilterItem() {
if (mSelectedParticipatingStatus == 1) {
return "involves:" + mUserLogin;
} else if (mSelectedParticipatingStatus == 2) {
return "-involves:" + mUserLogin;
} else {
return "";
}
}
private String buildFilterItem(String type, String value) {
if (!TextUtils.isEmpty(value)) {
return type + ":\"" + value + "\"";
} else if (value == null) {
// null means 'any value'
return "";
} else {
// empty string means 'no value set
return "no:" + type;
}
}
private void showLabelsDialog() {
final String[] labels = new String[mLabels.size() + 2];
int selected = mSelectedLabel != null && mSelectedLabel.isEmpty() ? 1 : 0;
labels[0] = getResources().getString(R.string.issue_filter_by_any_label);
labels[1] = getResources().getString(R.string.issue_filter_by_no_label);
for (int i = 0; i < mLabels.size(); i++) {
labels[i + 2] = mLabels.get(i).getName();
if (TextUtils.equals(mSelectedLabel, labels[i + 2])) {
selected = i + 2;
}
}
DialogInterface.OnClickListener selectCb = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mSelectedLabel = which == 0 ? null
: which == 1 ? ""
: labels[which];
dialog.dismiss();
invalidateFragments();
}
};
new AlertDialog.Builder(this)
.setCancelable(true)
.setTitle(R.string.issue_filter_by_labels)
.setSingleChoiceItems(labels, selected, selectCb)
.setNegativeButton(R.string.cancel, null)
.show();
}
private void showMilestonesDialog() {
final String[] milestones = new String[mMilestones.size() + 2];
int selected = mSelectedMilestone != null && mSelectedMilestone.isEmpty() ? 1 : 0;
milestones[0] = getResources().getString(R.string.issue_filter_by_any_milestone);
milestones[1] = getResources().getString(R.string.issue_filter_by_no_milestone);
for (int i = 0; i < mMilestones.size(); i++) {
milestones[i + 2] = mMilestones.get(i).getTitle();
if (TextUtils.equals(mSelectedMilestone, milestones[i + 2])) {
selected = i + 2;
}
}
DialogInterface.OnClickListener selectCb = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mSelectedMilestone = which == 0 ? null
: which == 1 ? ""
: milestones[which];
dialog.dismiss();
invalidateFragments();
}
};
new AlertDialog.Builder(this)
.setCancelable(true)
.setTitle(R.string.issue_filter_by_milestone)
.setSingleChoiceItems(milestones, selected, selectCb)
.setNegativeButton(R.string.cancel, null)
.show();
}
private void showAssigneesDialog() {
final String[] assignees = new String[mAssignees.size() + 2];
int selected = mSelectedAssignee != null && mSelectedAssignee.isEmpty() ? 1 : 0;
assignees[0] = getResources().getString(R.string.issue_filter_by_any_assignee);
assignees[1] = getResources().getString(R.string.issue_filter_by_no_assignee);
for (int i = 0; i < mAssignees.size(); i++) {
User u = mAssignees.get(i);
assignees[i + 2] = u.getLogin();
if (u.getLogin().equalsIgnoreCase(mSelectedAssignee)) {
selected = i + 2;
}
}
DialogInterface.OnClickListener selectCb = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mSelectedAssignee = which == 0 ? null
: which == 1 ? ""
: mAssignees.get(which - 2).getLogin();
dialog.dismiss();
invalidateFragments();
}
};
new AlertDialog.Builder(this)
.setCancelable(true)
.setTitle(R.string.issue_filter_by_assignee)
.setSingleChoiceItems(assignees, selected, selectCb)
.setNegativeButton(R.string.cancel, null)
.show();
}
private void filterAssignee() {
if (mAssignees == null) {
getSupportLoaderManager().initLoader(2, null, mAssigneeListCallback);
} else {
showAssigneesDialog();
}
}
private void filterMilestone() {
if (mMilestones == null) {
getSupportLoaderManager().initLoader(1, null, mMilestoneCallback);
} else {
showMilestonesDialog();
}
}
private void filterLabel() {
if (mLabels == null) {
getSupportLoaderManager().initLoader(0, null, mLabelCallback);
} else {
showLabelsDialog();
}
}
private void filterParticipating() {
DialogInterface.OnClickListener selectCb = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mSelectedParticipatingStatus = which;
dialog.dismiss();
invalidateFragments();
}
};
new AlertDialog.Builder(this)
.setCancelable(true)
.setTitle(R.string.issue_filter_by_participating)
.setSingleChoiceItems(R.array.filter_participating, mSelectedParticipatingStatus,
selectCb)
.setNegativeButton(R.string.cancel, null)
.show();
}
}