com.ruesga.rview.misc.Formatter.java Source code

Java tutorial

Introduction

Here is the source code for com.ruesga.rview.misc.Formatter.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.misc;

import android.content.Context;
import android.content.res.Resources;
import android.databinding.BindingAdapter;
import android.graphics.drawable.Drawable;
import android.support.annotation.Keep;
import android.support.v4.content.ContextCompat;
import android.support.v4.graphics.drawable.DrawableCompat;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.LeadingMarginSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.TypefaceSpan;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;

import com.ruesga.rview.R;
import com.ruesga.rview.fragments.ChangeDetailsFragment;
import com.ruesga.rview.gerrit.model.AccountDetailInfo;
import com.ruesga.rview.gerrit.model.AccountInfo;
import com.ruesga.rview.gerrit.model.ChangeInfo;
import com.ruesga.rview.gerrit.model.ChangeStatus;
import com.ruesga.rview.gerrit.model.CommentInfo;
import com.ruesga.rview.gerrit.model.CommitInfo;
import com.ruesga.rview.gerrit.model.ConfigInfo;
import com.ruesga.rview.gerrit.model.FileInfo;
import com.ruesga.rview.gerrit.model.FileStatus;
import com.ruesga.rview.gerrit.model.GitPersonalInfo;
import com.ruesga.rview.gerrit.model.ProjectStatus;
import com.ruesga.rview.gerrit.model.RevisionInfo;
import com.ruesga.rview.gerrit.model.SideType;
import com.ruesga.rview.gerrit.model.SubmitType;
import com.ruesga.rview.model.Account;
import com.ruesga.rview.model.EmptyState;
import com.ruesga.rview.preferences.Constants;
import com.ruesga.rview.preferences.Preferences;
import com.ruesga.rview.text.QuotedSpan;
import com.ruesga.rview.widget.RegExLinkifyTextView;
import com.ruesga.rview.widget.RegExLinkifyTextView.RegExLink;
import com.ruesga.rview.widget.StyleableTextView;

import org.ocpsoft.prettytime.PrettyTime;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;

@Keep
@SuppressWarnings("unused")
public class Formatter {
    private static Account mAccount;
    private static final Map<String, PrettyTime> sPrettyTimeMap = new HashMap<>();
    private static String sDisplayFormat = Constants.ACCOUNT_DISPLAY_FORMAT_NAME;
    private static boolean sHighlightNotReviewed = true;
    private static boolean sDisplayAccountStatues = true;

    private static int sQuoteColor = -1;
    private static int sQuoteWidth = -1;
    private static int sQuoteMargin = -1;

    public static void refreshCachedPreferences(Context context) {
        mAccount = Preferences.getAccount(context);
        sDisplayFormat = Preferences.getAccountDisplayFormat(context, mAccount);
        sHighlightNotReviewed = Preferences.isAccountHighlightUnreviewed(context, mAccount);
        sDisplayAccountStatues = Preferences.isAccountDisplayStatuses(context, mAccount);
    }

    private static PrettyTime getPrettyTime(Context context) {
        String localeTag = context.getString(R.string.bcp47_locale_tag);
        if (!sPrettyTimeMap.containsKey(localeTag)) {
            final String[] localeInfo = localeTag.split("-");
            final Locale locale = new Locale(localeInfo[0], localeInfo[1]);
            sPrettyTimeMap.put(localeTag, new PrettyTime(locale));
        }
        return sPrettyTimeMap.get(localeTag);
    }

    @BindingAdapter("prettyDateTime")
    public static void toPrettyDateTime(TextView view, Date date) {
        if (date == null) {
            view.setText(null);
            return;
        }
        view.setText(getPrettyTime(view.getContext()).format(date));
    }

    @BindingAdapter("accountDisplayName")
    public static void toAccountDisplayName(TextView view, AccountInfo accountInfo) {
        if (accountInfo == null) {
            view.setText(null);
            return;
        }

        // Display Name
        String accountDisplayName = null;
        switch (sDisplayFormat) {
        case Constants.ACCOUNT_DISPLAY_FORMAT_NAME:
            accountDisplayName = accountInfo.name;
            break;
        case Constants.ACCOUNT_DISPLAY_FORMAT_EMAIL:
            accountDisplayName = accountInfo.email;
            break;
        case Constants.ACCOUNT_DISPLAY_FORMAT_USERNAME:
            accountDisplayName = accountInfo.username;
            break;
        }
        if (TextUtils.isEmpty(accountDisplayName)) {
            accountDisplayName = accountInfo.username;
        }
        if (TextUtils.isEmpty(accountDisplayName)) {
            accountDisplayName = String.valueOf(accountInfo.accountId);
        }

        // Status
        if (sDisplayAccountStatues && !TextUtils.isEmpty(accountInfo.status)) {
            accountDisplayName += " " + accountInfo.status;
        }

        view.setText(accountDisplayName);
    }

    @BindingAdapter({ "draftAccountDisplayName", "isDraft" })
    public static void toDraftAccountDisplayName(TextView view, AccountInfo accountInfo, boolean isDraft) {

        if (isDraft) {
            Context ctx = view.getContext();
            view.setText(ctx.getString(R.string.menu_draft).toUpperCase(AndroidHelper.getCurrentLocale(ctx)));
            Drawable dw = ContextCompat.getDrawable(ctx, R.drawable.bg_tag);
            DrawableCompat.setTint(dw, ContextCompat.getColor(ctx, R.color.unscored));
            view.setBackground(dw);
            return;
        }

        view.setBackground(null);
        toAccountDisplayName(view, accountInfo);
    }

    @BindingAdapter("highlightUnreviewed")
    public static void toHighlightedUnreviewed(StyleableTextView view, boolean reviewed) {
        view.setTypeface(TypefaceCache.getTypeface(view.getContext(),
                sHighlightNotReviewed && !reviewed ? TypefaceCache.TF_BOLD_CONDENSED : TypefaceCache.TF_CONDENSED));
    }

    @BindingAdapter("compressedText")
    public static void toCompressedText(TextView view, String value) {
        if (value == null) {
            view.setText(null);
            return;
        }
        view.setText(value.replaceAll("\n", " ").trim());
    }

    @BindingAdapter("commitMessage")
    public static void toCommitMessage(TextView view, CommitInfo info) {
        if (info == null || info.message == null) {
            view.setText(null);
            return;
        }

        String message = EmojiHelper.createEmoji(info.message.substring(info.subject.length()).trim());
        view.setText(StringHelper.removeLineBreaks(message));
    }

    @BindingAdapter("inlineComment")
    public static void toInlineComment(TextView view, String msg) {
        if (msg == null) {
            view.setText(null);
            return;
        }

        view.setText(EmojiHelper.createEmoji(msg));
    }

    @BindingAdapter("userMessage")
    public static void toUserMessage(TextView view, String msg) {
        if (msg == null) {
            view.setText(null);
            return;
        }

        String message = EmojiHelper.createEmoji(msg);

        // This process mimics the Gerrit formatting process done in class
        // ./gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java

        // Split message into paragraphs
        String[] paragraphs = StringHelper.obtainParagraphs(message);
        StringBuilder sb = new StringBuilder();
        boolean formattedMessage = false;
        for (String p : paragraphs) {
            if (StringHelper.isQuote(p)) {
                sb.append(StringHelper.obtainQuote(StringHelper.removeLineBreaks(p)));
                formattedMessage = true;
            } else if (StringHelper.isList(p)) {
                sb.append(p);
                formattedMessage = true;
            } else if (StringHelper.isPreFormat(p)) {
                sb.append(StringHelper.obtainPreFormatMessage(p));
                formattedMessage = true;
            } else {
                sb.append(p);
            }
            sb.append("\n\n");
        }

        String userMessage = StringHelper.removeExtraLines(sb.toString());
        if (!formattedMessage) {
            view.setText(userMessage);
            return;
        }

        if (sQuoteColor == -1) {
            sQuoteColor = ContextCompat.getColor(view.getContext(), R.color.quote);
            sQuoteWidth = (int) view.getContext().getResources().getDimension(R.dimen.quote_width);
            sQuoteMargin = (int) view.getContext().getResources().getDimension(R.dimen.quote_margin);
        }

        String[] lines = userMessage.split("\n");
        SpannableStringBuilder spannable = new SpannableStringBuilder(userMessage
                .replaceAll(StringHelper.NON_PRINTABLE_CHAR, "").replaceAll(StringHelper.NON_PRINTABLE_CHAR2, ""));

        // Pre-Format
        int start = 0;
        int spans = 0;
        while ((start = userMessage.indexOf(StringHelper.NON_PRINTABLE_CHAR2, start)) != -1) {
            int end = userMessage.indexOf(StringHelper.NON_PRINTABLE_CHAR2, start + 1);
            if (end == -1) {
                //? This is supposed to be formatted by us. Skip it
                break;
            }

            // Find quote token occurrences
            int offset = StringHelper.countOccurrences(StringHelper.NON_PRINTABLE_CHAR, userMessage, 0, start);
            start -= offset;
            end -= offset;

            // Ensure bounds
            int spanStart = start - spans;
            int spanEnd = Math.min(end - spans - 1, spannable.length());
            if (spanStart < 0 || spanEnd < 0) {
                //Something was wrong. Skip it
                break;
            }
            spannable.setSpan(new RelativeSizeSpan(0.8f), spanStart, spanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            spannable.setSpan(new TypefaceSpan("monospace"), spanStart, spanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            start = end;
            spans++;
        }

        start = 0;
        for (String line : lines) {
            // Quotes
            int maxIndent = StringHelper.countOccurrences(StringHelper.NON_PRINTABLE_CHAR, line);
            for (int i = 0; i < maxIndent; i++) {
                QuotedSpan span = new QuotedSpan(sQuoteColor, sQuoteWidth, sQuoteMargin);
                spannable.setSpan(span, start, Math.min(start + line.length() - maxIndent, spannable.length()),
                        Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            }

            // List
            if (StringHelper.isList(line)) {
                spannable.replace(start, start + 1, "\u2022");
                spannable.setSpan(new LeadingMarginSpan.Standard(sQuoteMargin), start,
                        Math.min(start + line.length(), spannable.length()), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            }

            start += line.length() - maxIndent + 1;
        }

        view.setText(spannable);
    }

    @BindingAdapter("regexpLinkifyCommitsOnly")
    public static void toRegExLinkifyCommitsOnly(RegExLinkifyTextView view, Boolean only) {
        List<RegExLink> linksScanners = new ArrayList<>();

        if (mAccount != null) {
            linksScanners.addAll(RegExLinkifyTextView.createRepositoryRegExpLinks(mAccount.mRepository));
        }

        if (only) {
            linksScanners.add(RegExLinkifyTextView.GERRIT_CHANGE_ID_REGEX);
            linksScanners.add(RegExLinkifyTextView.GERRIT_COMMIT_REGEX);
        }

        if (!linksScanners.isEmpty()) {
            view.addRegEx(linksScanners.toArray(new RegExLink[linksScanners.size()]));
        }
    }

    @BindingAdapter("regexpLinkify")
    public static void toRegExLinkify(RegExLinkifyTextView view, ConfigInfo info) {
        List<RegExLink> linksScanners = new ArrayList<>();
        linksScanners.add(RegExLinkifyTextView.GERRIT_CHANGE_ID_REGEX);
        linksScanners.add(RegExLinkifyTextView.GERRIT_COMMIT_REGEX);
        if (mAccount != null) {
            linksScanners.addAll(RegExLinkifyTextView.createRepositoryRegExpLinks(mAccount.mRepository));
        }

        if (info != null && info.commentLinks != null && info.commentLinks.isEmpty()) {
            for (String key : info.commentLinks.keySet()) {
                switch (key) {
                case "changeid":
                case "commit":
                    break;
                default:
                    String link = info.commentLinks.get(key).link;
                    if (TextUtils.isEmpty(link) && !TextUtils.isEmpty(info.commentLinks.get(key).html)) {
                        Matcher matcher = RegExLinkifyTextView.WEB_LINK_REGEX.mPattern
                                .matcher(info.commentLinks.get(key).html);
                        if (matcher.find()) {
                            link = matcher.group();
                            linksScanners.add(new RegExLink(RegExLinkifyTextView.WEB_LINK_REGEX.mType,
                                    info.commentLinks.get(key).match, link));
                        }
                    }
                    break;
                }
            }
        }

        view.addRegEx(linksScanners.toArray(new RegExLink[linksScanners.size()]));
    }

    @BindingAdapter("committer")
    public static void toCommitter(TextView view, GitPersonalInfo info) {
        if (info == null) {
            view.setText(null);
            return;
        }

        String date = getPrettyTime(view.getContext()).format(info.date);
        String txt = view.getContext().getString(R.string.committer_format, info.name, info.email, date);
        view.setText(txt);
    }

    @BindingAdapter("score")
    public static void toScore(TextView view, Integer score) {
        if (score == null) {
            view.setText(null);
            return;
        }

        String txt = (score > 0 ? "+" : "") + String.valueOf(score);
        view.setText(txt);
    }

    @BindingAdapter("submitType")
    public static void toSubmitType(TextView view, SubmitType submitType) {
        if (submitType == null) {
            view.setText(null);
            return;
        }

        if (submitType.equals(SubmitType.FAST_FORWARD_ONLY)) {
            view.setText(R.string.submit_type_fast_forward_only);
        } else if (submitType.equals(SubmitType.MERGE_IF_NECESSARY)) {
            view.setText(R.string.submit_type_merge_if_necessary);
        } else if (submitType.equals(SubmitType.MERGE_ALWAYS)) {
            view.setText(R.string.submit_type_merge_always);
        } else if (submitType.equals(SubmitType.CHERRY_PICK)) {
            view.setText(R.string.submit_type_cherry_pick);
        } else if (submitType.equals(SubmitType.REBASE_IF_NECESSARY)) {
            view.setText(R.string.submit_type_rebase_if_necessary);
        } else {
            view.setText(null);
        }
    }

    @BindingAdapter("fileTypeDrawable")
    public static void toFileTypeDrawable(ImageView view, FileInfo model) {
        if (model == null || model.status == null) {
            view.setImageDrawable(null);
            return;
        }

        view.setImageDrawable(
                ContextCompat.getDrawable(view.getContext(), ModelHelper.toFileStatusDrawable(model.status)));
    }

    @BindingAdapter("fileStatus")
    public static void toFileStatus(TextView view, ChangeDetailsFragment.FileItemModel item) {
        if (item == null) {
            view.setText(null);
            return;
        }

        String status = "";
        String txt = "";
        if (item.info != null) {
            if (item.info.status.equals(FileStatus.R)) {
                status = "[" + view.getContext().getString(R.string.file_status_renamed) + "] ";
            } else if (item.info.status.equals(FileStatus.C)) {
                status = "[" + view.getContext().getString(R.string.file_status_copied) + "] ";
            } else if (item.info.status.equals(FileStatus.W)) {
                status = "[" + view.getContext().getString(R.string.file_status_rewritten) + "] ";
            }
            txt = status;
            if (!TextUtils.isEmpty(item.info.oldPath)) {
                txt += item.info.oldPath + " \u2192 ";
            }
        }
        txt += item.file.startsWith("/") ? item.file.substring(1) : item.file;
        view.setText(txt);
    }

    @BindingAdapter("fileStatus")
    public static void toFileStatus(TextView view, FileStatus status) {
        if (status == null) {
            view.setText(null);
            return;
        }

        if (status.equals(FileStatus.R)) {
            view.setText(R.string.file_status_renamed);
        } else if (status.equals(FileStatus.C)) {
            view.setText(R.string.file_status_copied);
        } else if (status.equals(FileStatus.W)) {
            view.setText(R.string.file_status_rewritten);
        } else if (status.equals(FileStatus.A)) {
            view.setText(R.string.file_status_added);
        } else if (status.equals(FileStatus.D)) {
            view.setText(R.string.file_status_deleted);
        } else if (status.equals(FileStatus.M)) {
            view.setText(R.string.file_status_modified);
        }
    }

    @BindingAdapter("file")
    public static void toFile(TextView view, String path) {
        view.setText(path.startsWith("/") ? path.substring(1) : path);
    }

    @BindingAdapter({ "changeStatus", "currentRevision" })
    public static void toChangeStatus(TextView view, ChangeInfo change, boolean currentRevision) {
        if (change == null) {
            view.setText(null);
            return;
        }

        if (!currentRevision) {
            view.setText(R.string.change_statuses_not_current);
        } else if (ChangeStatus.NEW.equals(change.status)) {
            if (change.submittable) {
                view.setText(R.string.change_statuses_ready_to_submit);
            } else if (!change.mergeable) {
                view.setText(R.string.change_statuses_not_mergeable);
            } else {
                String neededLabel = ModelHelper.checkNeedsLabel(change.labels);
                if (neededLabel != null) {
                    view.setText(view.getContext().getString(R.string.change_statuses_needs_label, neededLabel));
                } else {
                    view.setText(R.string.menu_open);
                }
            }
        } else if (ChangeStatus.SUBMITTED.equals(change.status)) {
            view.setText(R.string.change_statuses_submitted);
        } else if (ChangeStatus.MERGED.equals(change.status)) {
            view.setText(R.string.menu_merged);
        } else if (ChangeStatus.ABANDONED.equals(change.status)) {
            view.setText(R.string.menu_abandoned);
        } else if (ChangeStatus.DRAFT.equals(change.status)) {
            view.setText(R.string.menu_draft);
        } else {
            view.setText(null);
        }
    }

    @BindingAdapter("addedVsDeleted")
    public static void toAddedVsRemoved(TextView view, FileInfo info) {
        if (info == null) {
            view.setText(null);
            return;
        }

        final Resources res = view.getResources();
        String added = null;
        if (info.linesInserted != null && info.linesInserted > 0) {
            added = res.getQuantityString(R.plurals.files_added, info.linesInserted, info.linesInserted);
        }
        String deleted = null;
        if (info.linesDeleted != null && info.linesDeleted > 0) {
            deleted = res.getQuantityString(R.plurals.files_deleted, info.linesDeleted, info.linesDeleted);
        }
        if (added == null && deleted == null) {
            added = res.getQuantityString(R.plurals.files_added, 0, 0);
            deleted = res.getQuantityString(R.plurals.files_deleted, 0, 0);
        }

        String txt;
        if (added != null && deleted != null) {
            txt = res.getString(R.string.added_vs_deleted, added, deleted);
        } else if (added != null) {
            txt = added;
        } else {
            txt = deleted;
        }
        view.setText(txt);
    }

    @BindingAdapter("commitWebLinksTag")
    public static void toCommitWebLinksTag(View view, CommitInfo commit) {
        if (commit != null && commit.webLinks != null && commit.webLinks.length > 0) {
            view.setTag(commit.webLinks[0].url);
        } else {
            view.setTag(null);
        }
    }

    @BindingAdapter("revisionNumber")
    public static void toRevisionNumber(TextView view, RevisionInfo revision) {
        if (revision == null) {
            view.setText(null);
            return;
        }
        view.setText(String.format(Locale.US, "#%02d", revision.number));
    }

    @BindingAdapter("revisionCommit")
    public static void toRevisionCommit(TextView view, RevisionInfo revision) {
        if (revision == null) {
            view.setText(null);
            return;
        }
        if (revision.commit == null) {
            view.setText(view.getContext().getString(R.string.options_base));
        } else {
            view.setText(revision.commit.commit.substring(0, 10));
        }
    }

    @BindingAdapter("reviewerKind")
    public static void toReviewerKind(ImageView view, Boolean isGroup) {
        view.setImageResource(isGroup != null && isGroup ? R.drawable.ic_group : R.drawable.ic_person);
    }

    @BindingAdapter("accountEmails")
    public static void toAccountEmails(TextView v, AccountDetailInfo account) {
        if (account == null || account.email == null) {
            v.setText(null);
            return;
        }

        String emails = account.email;
        if (account.secondaryEmails != null) {
            for (String email : account.secondaryEmails) {
                emails += "\n" + email;
            }
        }
        v.setText(emails);
    }

    @BindingAdapter("projectStatus")
    public static void toProjectStatus(TextView v, ProjectStatus status) {
        if (status == null) {
            v.setText(null);
            return;
        }

        String statusText = null;
        if (status.equals(ProjectStatus.ACTIVE)) {
            statusText = v.getContext().getString(R.string.project_details_status_active);
        } else if (status.equals(ProjectStatus.READ_ONLY)) {
            statusText = v.getContext().getString(R.string.project_details_status_read_only);
        } else if (status.equals(ProjectStatus.HIDDEN)) {
            statusText = v.getContext().getString(R.string.project_details_status_hidden);
        }
        v.setText(statusText);
    }

    @BindingAdapter("commentLine")
    public static void toCommentLine(TextView v, CommentInfo comment) {
        if (comment == null) {
            v.setText(null);
        } else if (comment.line == null) {
            v.setText(R.string.change_details_comment_file);
        } else if (SideType.PARENT.equals(comment.side)) {
            v.setText(v.getContext().getString(R.string.change_details_comment_base_line_number, comment.line));
        } else {
            v.setText(v.getContext().getString(R.string.change_details_comment_line_number, comment.line));
        }
    }

    @BindingAdapter("accountStatus")
    public static void toAccountStatus(TextView view, String status) {
        view.setText(EmojiHelper.resolveEmojiStatus(view.getContext(), status));
    }

    @BindingAdapter("emptyStateDrawable")
    public static void toEmptyStateDrawable(ImageView v, EmptyState state) {
        if (state == null || state.state == EmptyState.NORMAL_STATE) {
            v.setImageDrawable(null);
        } else if (state.state == EmptyState.NO_RESULTS_STATE) {
            v.setImageDrawable(ContextCompat.getDrawable(v.getContext(), R.drawable.ic_empty_results));
        } else if (state.state == EmptyState.ALL_DONE_STATE) {
            v.setImageDrawable(ContextCompat.getDrawable(v.getContext(), R.drawable.ic_duck));
        } else if (state.state == EmptyState.NOT_CONNECTIVITY_STATE) {
            v.setImageDrawable(ContextCompat.getDrawable(v.getContext(), R.drawable.ic_cloud_off));
        } else if (state.state == EmptyState.SERVER_CANNOT_BE_REACHED) {
            v.setImageDrawable(ContextCompat.getDrawable(v.getContext(), R.drawable.ic_server_network_off));
        } else if (state.state == EmptyState.ERROR_STATE) {
            v.setImageDrawable(ContextCompat.getDrawable(v.getContext(), R.drawable.ic_error_outline));
        }
    }

    @BindingAdapter("emptyStatedDescription")
    public static void toEmptyStateDescription(TextView v, EmptyState state) {
        if (state == null || state.state == EmptyState.NORMAL_STATE) {
            v.setText(null);
        } else if (state.state == EmptyState.NO_RESULTS_STATE) {
            v.setText(R.string.empty_states_no_results);
        } else if (state.state == EmptyState.ALL_DONE_STATE) {
            v.setText(R.string.empty_states_all_done);
        } else if (state.state == EmptyState.NOT_CONNECTIVITY_STATE) {
            v.setText(R.string.empty_states_not_connectivity);
        } else if (state.state == EmptyState.SERVER_CANNOT_BE_REACHED) {
            v.setText(R.string.empty_states_server_not_reached);
        } else if (state.state == EmptyState.ERROR_STATE) {
            v.setText(R.string.empty_states_error);
        }
    }

    @BindingAdapter("humanReadableSize")
    public static void toHumanReadableSize(TextView v, Long size) {
        if (size == null) {
            v.setText(null);
            return;
        }
        v.setText(android.text.format.Formatter.formatFileSize(v.getContext(), size));
    }

    public static String toShortenCommit(String commitHash) {
        if (!TextUtils.isEmpty(commitHash) && commitHash.length() > 8) {
            return commitHash.substring(0, 8);
        }
        return commitHash;
    }
}