com.gh4a.activities.DiffViewerActivity.java Source code

Java tutorial

Introduction

Here is the source code for com.gh4a.activities.DiffViewerActivity.java

Source

/*
 * 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.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Point;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.content.Loader;
import android.support.v4.util.LongSparseArray;
import android.support.v4.view.MenuItemCompat;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.ListPopupWindow;
import android.text.Html;
import android.text.TextUtils;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ListAdapter;
import android.widget.TextView;

import com.gh4a.Constants;
import com.gh4a.Gh4Application;
import com.gh4a.ProgressDialogTask;
import com.gh4a.R;
import com.gh4a.loader.LoaderCallbacks;
import com.gh4a.loader.LoaderResult;
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 java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public abstract class DiffViewerActivity extends WebViewerActivity implements View.OnTouchListener {
    protected String mRepoOwner;
    protected String mRepoName;
    protected String mPath;
    protected String mSha;
    private int mInitialLine;

    public static final String EXTRA_INITIAL_LINE = "initial_line";

    private String mDiff;
    private String[] mDiffLines;
    private SparseArray<List<CommitComment>> mCommitCommentsByPos = new SparseArray<>();
    private LongSparseArray<CommitComment> mCommitComments = new LongSparseArray<>();

    private Point mLastTouchDown = new Point();

    private static final int MENU_ITEM_VIEW = 10;

    private LoaderCallbacks<List<CommitComment>> mCommentCallback = new LoaderCallbacks<List<CommitComment>>(this) {
        @Override
        protected Loader<LoaderResult<List<CommitComment>>> onCreateLoader() {
            return createCommentLoader();
        }

        @Override
        protected void onResultReady(List<CommitComment> result) {
            addCommentsToMap(result);
            showDiff();
        }
    };

    @Override
    @SuppressWarnings("unchecked")
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ActionBar actionBar = getSupportActionBar();
        actionBar.setTitle(FileUtils.getFileName(mPath));
        actionBar.setSubtitle(mRepoOwner + "/" + mRepoName);
        actionBar.setDisplayHomeAsUpEnabled(true);

        mWebView.setOnTouchListener(this);

        List<CommitComment> comments = (ArrayList<CommitComment>) getIntent()
                .getSerializableExtra(Constants.Commit.COMMENTS);

        if (comments != null) {
            addCommentsToMap(comments);
            showDiff();
        } else {
            getSupportLoaderManager().initLoader(0, null, mCommentCallback);
        }
    }

    @Override
    protected void onInitExtras(Bundle extras) {
        super.onInitExtras(extras);
        mRepoOwner = extras.getString(Constants.Repository.OWNER);
        mRepoName = extras.getString(Constants.Repository.NAME);
        mPath = extras.getString(Constants.Object.PATH);
        mSha = extras.getString(Constants.Object.OBJECT_SHA);
        mDiff = extras.getString(Constants.Commit.DIFF);
        mInitialLine = extras.getInt(EXTRA_INITIAL_LINE, -1);
    }

    @Override
    protected boolean canSwipeToRefresh() {
        // no need for pull-to-refresh if everything was passed in the intent extras
        return !getIntent().hasExtra(Constants.Commit.COMMENTS);
    }

    @Override
    public void onRefresh() {
        Loader loader = getSupportLoaderManager().getLoader(0);
        if (loader != null) {
            mCommitCommentsByPos.clear();
            setContentShown(false);
            loader.onContentChanged();
        }
        super.onRefresh();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.download_menu, menu);

        menu.removeItem(R.id.download);

        String viewAtTitle = getString(R.string.object_view_file_at, mSha.substring(0, 7));
        MenuItem item = menu.add(0, MENU_ITEM_VIEW, Menu.NONE, viewAtTitle);
        MenuItemCompat.setShowAsAction(item, MenuItemCompat.SHOW_AS_ACTION_NEVER);

        return super.onCreateOptionsMenu(menu);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        String url = "https://github.com/" + mRepoOwner + "/" + mRepoName + "/commit/" + mSha;

        switch (item.getItemId()) {
        case R.id.browser:
            IntentUtils.launchBrowser(this, Uri.parse(url));
            return true;
        case R.id.share:
            Intent shareIntent = new Intent(Intent.ACTION_SEND);
            shareIntent.setType("text/plain");
            shareIntent.putExtra(Intent.EXTRA_SUBJECT,
                    getString(R.string.share_commit_subject, mSha.substring(0, 7), mRepoOwner + "/" + mRepoName));
            shareIntent.putExtra(Intent.EXTRA_TEXT, url);
            shareIntent = Intent.createChooser(shareIntent, getString(R.string.share_title));
            startActivity(shareIntent);
            return true;
        case MENU_ITEM_VIEW:
            startActivity(IntentUtils.getFileViewerActivityIntent(this, mRepoOwner, mRepoName, mSha, mPath));
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    private void addCommentsToMap(List<CommitComment> comments) {
        for (CommitComment comment : comments) {
            if (!TextUtils.equals(comment.getPath(), mPath)) {
                continue;
            }
            int position = comment.getPosition();
            List<CommitComment> commentsByPos = mCommitCommentsByPos.get(position);
            if (commentsByPos == null) {
                commentsByPos = new ArrayList<>();
                mCommitCommentsByPos.put(position, commentsByPos);
            }
            commentsByPos.add(comment);
        }
    }

    protected void showDiff() {
        StringBuilder content = new StringBuilder();
        boolean authorized = Gh4Application.get().isAuthorized();

        content.append("<html><head><title></title>");
        writeCssInclude(content, "text");
        writeScriptInclude(content, "codeutils");
        content.append("</head><body");
        if (mInitialLine > 0) {
            content.append(" onload='scrollToElement(\"line");
            content.append(mInitialLine).append("\")' onresize='scrollToHighlight();'");
        }
        content.append("><pre>");

        String encoded = TextUtils.htmlEncode(mDiff);
        mDiffLines = encoded.split("\n");

        for (int i = 0; i < mDiffLines.length; i++) {
            String line = mDiffLines[i];
            String cssClass = null;
            if (line.startsWith("@@")) {
                cssClass = "change";
            } else if (line.startsWith("+")) {
                cssClass = "add";
            } else if (line.startsWith("-")) {
                cssClass = "remove";
            }

            content.append("<div id=\"line").append(i).append("\"");
            if (cssClass != null) {
                content.append("class=\"").append(cssClass).append("\"");
            }
            if (authorized) {
                content.append(" onclick=\"javascript:location.href='comment://add");
                content.append("?position=").append(i).append("'\"");
            }
            content.append(">").append(line).append("</div>");

            List<CommitComment> comments = mCommitCommentsByPos.get(i);
            if (comments != null) {
                for (CommitComment comment : comments) {
                    mCommitComments.put(comment.getId(), comment);
                    content.append("<div class=\"comment\"");
                    if (authorized) {
                        content.append(" onclick=\"javascript:location.href='comment://edit");
                        content.append("?position=").append(i);
                        content.append("&id=").append(comment.getId()).append("'\"");
                    }
                    content.append("><div class=\"change\">");
                    content.append(getString(R.string.commit_comment_header,
                            "<b>" + ApiHelpers.getUserLogin(this, comment.getUser()) + "</b>",
                            StringUtils.formatRelativeTime(DiffViewerActivity.this, comment.getCreatedAt(), true)));
                    content.append("</div>").append(comment.getBodyHtml()).append("</div>");
                }
            }
        }
        content.append("</pre></body></html>");
        loadThemedHtml(content.toString());
    }

    private void openCommentDialog(final long id, String line, final int position) {
        final boolean isEdit = id != 0L;
        LayoutInflater inflater = LayoutInflater.from(this);
        View commentDialog = inflater.inflate(R.layout.commit_comment_dialog, null);

        final TextView code = (TextView) commentDialog.findViewById(R.id.line);
        code.setText(line);

        final EditText body = (EditText) commentDialog.findViewById(R.id.body);
        if (isEdit) {
            body.setText(mCommitComments.get(id).getBody());
        }

        final int saveButtonResId = isEdit ? R.string.issue_comment_update_title : R.string.issue_comment_title;
        final DialogInterface.OnClickListener saveCb = new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                String text = body.getText().toString();
                new CommentTask(id, text, position).schedule();
            }
        };

        AlertDialog.Builder builder = new AlertDialog.Builder(this).setCancelable(true)
                .setTitle(getString(R.string.commit_comment_dialog_title, position)).setView(commentDialog)
                .setPositiveButton(saveButtonResId, saveCb).setNegativeButton(R.string.cancel, null);

        AlertDialog d = builder.show();
        body.addTextChangedListener(
                new UiUtils.ButtonEnableTextWatcher(body, d.getButton(DialogInterface.BUTTON_POSITIVE)));
    }

    @Override
    protected boolean handleUrlLoad(String url) {
        if (!url.startsWith("comment://")) {
            return false;
        }

        Uri uri = Uri.parse(url);
        int line = Integer.parseInt(uri.getQueryParameter("position"));
        String lineText = Html.fromHtml(mDiffLines[line]).toString();
        String idParam = uri.getQueryParameter("id");
        long id = idParam != null ? Long.parseLong(idParam) : 0L;

        if (idParam == null) {
            openCommentDialog(id, lineText, line);
        } else {
            CommentActionPopup p = new CommentActionPopup(id, line, lineText, mLastTouchDown.x, mLastTouchDown.y);
            p.show();
        }
        return true;
    }

    @Override
    public boolean onTouch(View view, MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            mLastTouchDown.set((int) event.getX(), (int) event.getY());
        }
        return false;
    }

    protected void refresh() {
        mCommitComments.clear();
        mCommitCommentsByPos.clear();
        getSupportLoaderManager().restartLoader(0, null, mCommentCallback);
        setContentShown(false);
    }

    protected abstract Loader<LoaderResult<List<CommitComment>>> createCommentLoader();

    protected abstract void updateComment(long id, String body, int position) throws IOException;

    protected abstract void deleteComment(long id) throws IOException;

    private class CommentActionPopup extends ListPopupWindow implements AdapterView.OnItemClickListener {
        private long mId;
        private int mPosition;
        private String mLineText;

        public CommentActionPopup(long id, int position, String lineText, int x, int y) {
            super(DiffViewerActivity.this, null, R.attr.listPopupWindowStyle);

            mId = id;
            mPosition = position;
            mLineText = lineText;

            ArrayAdapter<String> adapter = new ArrayAdapter<>(DiffViewerActivity.this, R.layout.popup_menu_item,
                    populateChoices(isOwnComment(id)));
            setAdapter(adapter);
            setContentWidth(measureContentWidth(adapter));

            View anchor = findViewById(R.id.popup_helper);
            anchor.layout(x, y, x + 1, y + 1);
            setAnchorView(anchor);

            setOnItemClickListener(this);
            setModal(true);
        }

        @Override
        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
            if (position == 2) {
                new AlertDialog.Builder(DiffViewerActivity.this).setTitle(R.string.delete_comment_message)
                        .setMessage(R.string.confirmation)
                        .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int whichButton) {
                                new DeleteCommentTask(mId).schedule();
                            }
                        }).setNegativeButton(R.string.cancel, null).show();
            } else {
                openCommentDialog(position == 0 ? 0 : mId, mLineText, mPosition);
            }
            dismiss();
        }

        private boolean isOwnComment(long id) {
            String login = Gh4Application.get().getAuthLogin();
            CommitComment comment = mCommitComments.get(id);
            return ApiHelpers.loginEquals(comment.getUser(), login);
        }

        private String[] populateChoices(boolean ownComment) {
            String[] choices = new String[ownComment ? 3 : 1];
            choices[0] = getString(R.string.reply);
            if (ownComment) {
                choices[1] = getString(R.string.edit);
                choices[2] = getString(R.string.delete);
            }
            return choices;
        }

        private int measureContentWidth(ListAdapter adapter) {
            Context context = DiffViewerActivity.this;
            ViewGroup measureParent = new FrameLayout(context);
            int measureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
            int maxWidth = 0, count = adapter.getCount();
            View itemView = null;

            for (int i = 0; i < count; i++) {
                itemView = adapter.getView(i, itemView, measureParent);
                itemView.measure(measureSpec, measureSpec);
                maxWidth = Math.max(maxWidth, itemView.getMeasuredWidth());
            }
            return maxWidth;
        }
    }

    private class CommentTask extends ProgressDialogTask<Void> {
        private String mBody;
        private int mPosition;
        private long mId;

        public CommentTask(long id, String body, int position) {
            super(DiffViewerActivity.this, 0, R.string.saving_msg);
            mBody = body;
            mPosition = position;
            mId = id;
        }

        @Override
        protected ProgressDialogTask<Void> clone() {
            return new CommentTask(mId, mBody, mPosition);
        }

        @Override
        protected Void run() throws IOException {
            updateComment(mId, mBody, mPosition);
            return null;
        }

        @Override
        protected void onSuccess(Void result) {
            refresh();
            setResult(RESULT_OK);
        }

        @Override
        protected String getErrorMessage() {
            return getContext().getString(R.string.error_edit_commit_comment, mPosition);
        }
    }

    private class DeleteCommentTask extends ProgressDialogTask<Void> {
        private long mId;

        public DeleteCommentTask(long id) {
            super(DiffViewerActivity.this, 0, R.string.deleting_msg);
            mId = id;
        }

        @Override
        protected ProgressDialogTask<Void> clone() {
            return new DeleteCommentTask(mId);
        }

        @Override
        protected Void run() throws IOException {
            deleteComment(mId);
            return null;
        }

        @Override
        protected void onSuccess(Void result) {
            refresh();
            setResult(RESULT_OK);
        }

        @Override
        protected String getErrorMessage() {
            return getContext().getString(R.string.error_delete_commit_comment);
        }
    }
}