package com.gh4a; import android.app.Activity; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.app.DialogFragment; import android.support.v7.app.AppCompatActivity; import android.text.TextUtils; import android.util.Pair; import com.gh4a.activities.BlogListActivity; import com.gh4a.activities.CommitActivity; import com.gh4a.activities.CommitDiffViewerActivity; import com.gh4a.activities.DownloadsActivity; import com.gh4a.activities.FileViewerActivity; import com.gh4a.activities.GistActivity; import com.gh4a.activities.IssueActivity; import com.gh4a.activities.IssueEditActivity; import com.gh4a.activities.IssueListActivity; import com.gh4a.activities.PullRequestActivity; import com.gh4a.activities.PullRequestDiffViewerActivity; import com.gh4a.activities.ReleaseInfoActivity; import com.gh4a.activities.ReleaseListActivity; import com.gh4a.activities.RepositoryActivity; import com.gh4a.activities.TrendingActivity; import com.gh4a.activities.UserActivity; import com.gh4a.activities.WikiListActivity; import com.gh4a.loader.CommitCommentListLoader; import com.gh4a.loader.CommitLoader; import com.gh4a.loader.PullRequestCommentsLoader; import com.gh4a.loader.PullRequestFilesLoader; import com.gh4a.loader.PullRequestLoader; import com.gh4a.loader.ReleaseListLoader; import com.gh4a.utils.ApiHelpers; import com.gh4a.utils.FileUtils; import com.gh4a.utils.IntentUtils; import com.gh4a.utils.StringUtils; import com.gh4a.utils.UiUtils; import org.eclipse.egit.github.core.CommitComment; import org.eclipse.egit.github.core.CommitFile; import org.eclipse.egit.github.core.PullRequest; import org.eclipse.egit.github.core.Release; import org.eclipse.egit.github.core.RepositoryBranch; import org.eclipse.egit.github.core.RepositoryCommit; import org.eclipse.egit.github.core.RepositoryId; import org.eclipse.egit.github.core.RepositoryTag; import org.eclipse.egit.github.core.client.IGitHubConstants; import org.eclipse.egit.github.core.service.RepositoryService; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.regex.Pattern; public class BrowseFilter extends AppCompatActivity { public static Intent makeRedirectionIntent(Context context, Uri uri, IntentUtils.InitialCommentMarker initialComment) { Intent intent = new Intent(context, BrowseFilter.class); intent.setData(uri); intent.putExtra("initial_comment", initialComment); return intent; } private static final Pattern SHA1_PATTERN = Pattern.compile("[a-z0-9]{40}"); private static final List<String> RESERVED_NAMES = Arrays.asList( "login", "logout", "sessions", "settings" ); public void onCreate(Bundle savedInstanceState) { setTheme(Gh4Application.THEME == R.style.DarkTheme ? R.style.TransparentDarkTheme : R.style.TransparentLightTheme); super.onCreate(savedInstanceState); Uri uri = getIntent().getData(); if (uri == null) { finish(); return; } List<String> parts = new ArrayList<>(uri.getPathSegments()); Intent intent = null; String first = parts.isEmpty() ? null : parts.get(0); if (IGitHubConstants.HOST_GISTS.equals(uri.getHost())) { if (!parts.isEmpty()) { intent = GistActivity.makeIntent(this, parts.get(parts.size() - 1)); } } else if (RESERVED_NAMES.contains(first)) { //noinspection StatementWithEmptyBody // forward to browser } else if ("explore".equals(first)) { intent = new Intent(this, TrendingActivity.class); } else if ("blog".equals(first)) { if (parts.size() == 1) { intent = new Intent(this, BlogListActivity.class); } } else if (first != null) { String user = first; String repo = parts.size() >= 2 ? parts.get(1) : null; String action = parts.size() >= 3 ? parts.get(2) : null; String id = parts.size() >= 4 ? parts.get(3) : null; if (repo == null && action == null) { intent = UserActivity.makeIntent(this, user); } else if (action == null) { intent = RepositoryActivity.makeIntent(this, user, repo); } else if ("downloads".equals(action)) { intent = DownloadsActivity.makeIntent(this, user, repo); } else if ("releases".equals(action)) { if ("tag".equals(id)) { final String release = parts.size() >= 5 ? parts.get(4) : null; new ReleaseLoadTask(user, repo, release).execute(); return; // avoid finish() for now } else { intent = ReleaseListActivity.makeIntent(this, user, repo); } } else if ("tree".equals(action) || "commits".equals(action)) { int page = "tree".equals(action) ? RepositoryActivity.PAGE_FILES : RepositoryActivity.PAGE_COMMITS; int refStart = 3; if (parts.size() >= 6 && TextUtils.equals(parts.get(3), "refs") && TextUtils.equals(parts.get(4), "heads")) { refStart = 5; } String refAndPath = TextUtils.join("/", parts.subList(refStart, parts.size())); new RefPathDisambiguationTask(user, repo, refAndPath, page).execute(); return; // avoid finish() for now } else if ("issues".equals(action)) { if (!StringUtils.isBlank(id)) { if ("new".equals(id)) { intent = IssueEditActivity.makeCreateIntent(this, user, repo); } else { try { intent = IssueActivity.makeIntent(this, user, repo, Integer.parseInt(id), generateInitialCommentMarker(uri.getFragment(), "issue")); } catch (NumberFormatException e) { // ignored } } } else { intent = IssueListActivity.makeIntent(this, user, repo); } } else if ("pulls".equals(action)) { intent = IssueListActivity.makeIntent(this, user, repo, true); } else if ("wiki".equals(action)) { intent = WikiListActivity.makeIntent(this, user, repo, null); } else if ("pull".equals(action) && !StringUtils.isBlank(id)) { int pullRequestNumber = -1; try { pullRequestNumber = Integer.parseInt(id); } catch (NumberFormatException e) { // ignored } if (pullRequestNumber > 0) { DiffHighlightId diffId = extractCommitDiffId(uri.getFragment()); if (diffId != null) { new PullRequestDiffLoadTask(user, repo, diffId, pullRequestNumber) .execute(); return; // avoid finish() for now } else { String target = parts.size() >= 5 ? parts.get(4) : null; int page = "commits".equals(action) ? PullRequestActivity.PAGE_COMMITS : "files".equals(target) ? PullRequestActivity.PAGE_FILES : -1; intent = PullRequestActivity.makeIntent(this, user, repo, pullRequestNumber, page, generateInitialCommentMarker(uri.getFragment(), "issue")); } } } else if ("commit".equals(action) && !StringUtils.isBlank(id)) { DiffHighlightId diffId = extractCommitDiffId(uri.getFragment()); if (diffId != null) { new CommitDiffLoadTask(user, repo, diffId, id).execute(); return; // avoid finish() for now } else { IntentUtils.InitialCommentMarker initialComment = generateInitialCommentMarker(uri.getFragment(), "commit"); if (initialComment != null) { new CommitCommentLoadTask(user, repo, id, initialComment).execute(); return; // avoid finish() for now } intent = CommitActivity.makeIntent(this, user, repo, id, generateInitialCommentMarker(uri.getFragment(), "commit")); } } else if ("blob".equals(action) && !StringUtils.isBlank(id) && parts.size() >= 4) { String refAndPath = TextUtils.join("/", parts.subList(3, parts.size())); new RefPathDisambiguationTask(user, repo, refAndPath, uri.getFragment()).execute(); return; // avoid finish() for now } } if (intent != null) { startActivity(intent); } else { IntentUtils.launchBrowser(this, uri); } finish(); } private IntentUtils.InitialCommentMarker generateInitialCommentMarker(String fragment, String type) { String prefix = type + "comment-"; if (fragment != null && fragment.startsWith(prefix)) { try { long commentId = Long.parseLong(fragment.substring(prefix.length())); return new IntentUtils.InitialCommentMarker(commentId); } catch (NumberFormatException e) { // fall through } } return getIntent().getParcelableExtra("initial_comment"); } private DiffHighlightId extractCommitDiffId(String fragment) { String prefix = "diff-"; if (fragment == null || !fragment.startsWith(prefix)) { return null; } boolean right = false; int typePos = fragment.indexOf('L', prefix.length()); if (typePos < 0) { right = true; typePos = fragment.indexOf('R', prefix.length()); } String fileHash = typePos > 0 ? fragment.substring(prefix.length(), typePos) : fragment.substring(prefix.length()); if (fileHash.length() != 32) { // MD5 hash length return null; } if (typePos < 0) { return new DiffHighlightId(fileHash, -1, -1, false); } try { char type = fragment.charAt(typePos); String linePart = fragment.substring(typePos + 1); int startLine, endLine, dashPos = linePart.indexOf("-" + type); if (dashPos > 0) { startLine = Integer.valueOf(linePart.substring(0, dashPos)); endLine = Integer.valueOf(linePart.substring(dashPos + 2)); } else { startLine = Integer.valueOf(linePart); endLine = startLine; } return new DiffHighlightId(fileHash, startLine, endLine, right); } catch (NumberFormatException e) { return null; } } public static class ProgressDialogFragment extends DialogFragment { @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { return UiUtils.createProgressDialog(getActivity(), R.string.loading_msg); } @Override public void onDismiss(DialogInterface dialog) { super.onDismiss(dialog); Activity activity = getActivity(); if (activity != null) { activity.finish(); } } } public static class DiffHighlightId { public final String fileHash; public final int startLine; public final int endLine; public final boolean right; public DiffHighlightId(String fileHash, int startLine, int endLine, boolean right) { this.fileHash = fileHash; this.startLine = startLine; this.endLine = endLine; this.right = right; } } private abstract class UrlLoadTask extends BackgroundTask<Intent> { public UrlLoadTask() { super(BrowseFilter.this); } @Override protected void onPreExecute() { super.onPreExecute(); new ProgressDialogFragment().show(getSupportFragmentManager(), "progress"); } @Override protected void onSuccess(Intent result) { if (isFinishing()) { return; } if (result != null) { startActivity(result); } else { IntentUtils.launchBrowser(BrowseFilter.this, getIntent().getData()); } finish(); } @Override protected void onError(Exception e) { IntentUtils.launchBrowser(BrowseFilter.this, getIntent().getData()); finish(); } } private class ReleaseLoadTask extends UrlLoadTask { private final String mRepoOwner; private final String mRepoName; private final String mTagName; public ReleaseLoadTask(String repoOwner, String repoName, String tagName) { super(); mRepoOwner = repoOwner; mRepoName = repoName; mTagName = tagName; } @Override protected Intent run() throws Exception { List<Release> releases = ReleaseListLoader.loadReleases(mRepoOwner, mRepoName); if (releases != null) { for (Release release : releases) { if (TextUtils.equals(release.getTagName(), mTagName)) { return ReleaseInfoActivity.makeIntent(BrowseFilter.this, mRepoOwner, mRepoName, release); } } } return null; } } private class CommitCommentLoadTask extends UrlLoadTask { private final String mRepoOwner; private final String mRepoName; private final String mCommitSha; private final IntentUtils.InitialCommentMarker mMarker; public CommitCommentLoadTask(String repoOwner, String repoName, String commitSha, IntentUtils.InitialCommentMarker marker) { mRepoOwner = repoOwner; mRepoName = repoName; mCommitSha = commitSha; mMarker = marker; } @Override protected Intent run() throws Exception { List<CommitComment> comments = CommitCommentListLoader.loadComments(mRepoOwner, mRepoName, mCommitSha); RepositoryCommit commit = CommitLoader.loadCommit(mRepoOwner, mRepoName, mCommitSha); boolean foundComment = false; CommitFile resultFile = null; for (CommitComment comment : comments) { if (mMarker.matches(comment.getId(), comment.getCreatedAt())) { foundComment = true; for (CommitFile commitFile : commit.getFiles()) { if (commitFile.getFilename().equals(comment.getPath())) { resultFile = commitFile; break; } } break; } } if (!foundComment || isFinishing()) { return null; } Intent intent = null; if (resultFile != null) { if (!FileUtils.isImage(resultFile.getFilename())) { intent = CommitDiffViewerActivity.makeIntent(BrowseFilter.this, mRepoOwner, mRepoName, mCommitSha, resultFile.getFilename(), resultFile.getPatch(), comments, -1, -1, false, mMarker); } } else { intent = CommitActivity.makeIntent(BrowseFilter.this, mRepoOwner, mRepoName, mCommitSha, mMarker); } return intent; } } private abstract class DiffLoadTask extends UrlLoadTask { protected final String mRepoOwner; protected final String mRepoName; protected final DiffHighlightId mDiffId; public DiffLoadTask(String repoOwner, String repoName, DiffHighlightId diffId) { super(); mRepoOwner = repoOwner; mRepoName = repoName; mDiffId = diffId; } @Override protected Intent run() throws Exception { List<CommitFile> files = getFiles(); CommitFile file = null; for (CommitFile commitFile : files) { if (ApiHelpers.md5(commitFile.getFilename()).equals(mDiffId.fileHash)) { file = commitFile; break; } } if (file == null || isFinishing()) { return null; } String sha = getSha(); if (sha == null || isFinishing()) { return null; } if (FileUtils.isImage(file.getFilename())) { return FileViewerActivity.makeIntent(BrowseFilter.this, mRepoOwner, mRepoName, sha, file.getFilename()); } return getLaunchIntent(sha, file, getComments(), mDiffId); } protected abstract List<CommitFile> getFiles() throws Exception; protected abstract String getSha() throws Exception; protected abstract List<CommitComment> getComments() throws Exception; protected abstract Intent getLaunchIntent(String sha, CommitFile file, List<CommitComment> comments, DiffHighlightId diffId); } private class PullRequestDiffLoadTask extends DiffLoadTask { private final int mPullRequestNumber; public PullRequestDiffLoadTask(String repoOwner, String repoName, DiffHighlightId diffId, int pullRequestNumber) { super(repoOwner, repoName, diffId); mPullRequestNumber = pullRequestNumber; } @Override protected Intent getLaunchIntent(String sha, CommitFile file, List<CommitComment> comments, DiffHighlightId diffId) { return PullRequestDiffViewerActivity.makeIntent(BrowseFilter.this, mRepoOwner, mRepoName, mPullRequestNumber, sha, file.getFilename(), file.getPatch(), comments, -1, diffId.startLine, diffId.endLine, diffId.right); } @Override protected String getSha() throws Exception { PullRequest pullRequest = PullRequestLoader.loadPullRequest(mRepoOwner, mRepoName, mPullRequestNumber); return pullRequest.getHead().getSha(); } @Override protected List<CommitFile> getFiles() throws Exception { return PullRequestFilesLoader.loadFiles(mRepoOwner, mRepoName, mPullRequestNumber); } @Override protected List<CommitComment> getComments() throws Exception { return PullRequestCommentsLoader.loadComments(mRepoOwner, mRepoName, mPullRequestNumber); } } private class CommitDiffLoadTask extends DiffLoadTask { private String mSha; public CommitDiffLoadTask(String repoOwner, String repoName, DiffHighlightId diffId, String sha) { super(repoOwner, repoName, diffId); mSha = sha; } @Override protected Intent getLaunchIntent(String sha, CommitFile file, List<CommitComment> comments, DiffHighlightId diffId) { return CommitDiffViewerActivity.makeIntent(BrowseFilter.this, mRepoOwner, mRepoName, sha, file.getFilename(), file.getPatch(), comments, diffId.startLine, diffId.endLine, diffId.right, null); } @Override public String getSha() throws Exception { return mSha; } @Override protected List<CommitFile> getFiles() throws Exception { RepositoryCommit commit = CommitLoader.loadCommit(mRepoOwner, mRepoName, mSha); return commit.getFiles(); } @Override protected List<CommitComment> getComments() throws Exception { return CommitCommentListLoader.loadComments(mRepoOwner, mRepoName, mSha); } } private class RefPathDisambiguationTask extends UrlLoadTask { private final String mRepoOwner; private final String mRepoName; private final String mRefAndPath; private final int mInitialPage; private final String mFragment; private final boolean mGoToFileViewer; public RefPathDisambiguationTask(String repoOwner, String repoName, String refAndPath, int initialPage) { super(); mRepoOwner = repoOwner; mRepoName = repoName; mRefAndPath = refAndPath; mInitialPage = initialPage; mFragment = null; mGoToFileViewer = false; } public RefPathDisambiguationTask(String repoOwner, String repoName, String refAndPath, String fragment) { super(); mRepoOwner = repoOwner; mRepoName = repoName; mRefAndPath = refAndPath; mFragment = fragment; mInitialPage = -1; mGoToFileViewer = true; } @Override protected Intent run() throws Exception { Pair<String, String> refAndPath = resolve(); if (refAndPath == null) { return null; } if (mGoToFileViewer && refAndPath.second != null) { // parse line numbers from fragment int highlightStart = -1, highlightEnd = -1; // Line numbers are encoded either in the form #L12 or #L12-14 if (mFragment != null && mFragment.startsWith("L")) { try { int dashPos = mFragment.indexOf("-L"); if (dashPos > 0) { highlightStart = Integer.valueOf(mFragment.substring(1, dashPos)); highlightEnd = Integer.valueOf(mFragment.substring(dashPos + 2)); } else { highlightStart = Integer.valueOf(mFragment.substring(1)); } } catch (NumberFormatException e) { // ignore } } return FileViewerActivity.makeIntentWithHighlight(BrowseFilter.this, mRepoOwner, mRepoName, refAndPath.first, refAndPath.second, highlightStart, highlightEnd); } else if (!mGoToFileViewer) { return RepositoryActivity.makeIntent(BrowseFilter.this, mRepoOwner, mRepoName, refAndPath.first, refAndPath.second, mInitialPage); } return null; } // returns ref, path private Pair<String, String> resolve() throws Exception { RepositoryService repoService = (RepositoryService) Gh4Application.get().getService(Gh4Application.REPO_SERVICE); RepositoryId repo = new RepositoryId(mRepoOwner, mRepoName); // try branches first List<RepositoryBranch> branches = repoService.getBranches(repo); if (branches != null) { for (RepositoryBranch branch : branches) { if (TextUtils.equals(mRefAndPath, branch.getName())) { return Pair.create(branch.getName(), null); } else { String nameWithSlash = branch.getName() + "/"; if (mRefAndPath.startsWith(nameWithSlash)) { return Pair.create(branch.getName(), mRefAndPath.substring(nameWithSlash.length())); } } } } if (isFinishing()) { return null; } // and tags second List<RepositoryTag> tags = repoService.getTags(repo); if (tags != null) { for (RepositoryTag tag : tags) { if (TextUtils.equals(mRefAndPath, tag.getName())) { return Pair.create(tag.getName(), null); } else { String nameWithSlash = tag.getName() + "/"; if (mRefAndPath.startsWith(nameWithSlash)) { return Pair.create(tag.getName(), mRefAndPath.substring(nameWithSlash.length())); } } } } // at this point, the first item may still be a SHA1 - check with a simple regex int slashPos = mRefAndPath.indexOf('/'); String potentialSha = slashPos > 0 ? mRefAndPath.substring(0, slashPos) : mRefAndPath; if (SHA1_PATTERN.matcher(potentialSha).matches()) { return Pair.create(potentialSha, slashPos > 0 ? mRefAndPath.substring(slashPos + 1) : ""); } return null; } } }