Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3ab9a630 authored by Makoto Onuki's avatar Makoto Onuki
Browse files

Remove extra line in status updates

- Remove trailing newlines in status updates
(i.e. reshared posts have HTML block quote
tags which get turned into 2 new lines)

Couldn't find a trim() utility method in
Spanned, SpannableStringBuilder, TextUtils, or
CharSequence - it only exists in String, but converting
to a String would cause loss of span info. Instead,
we iterate to look for new line characters manually.

- Hide stream item text views if there is no text
(i.e. prevent an extra new line when there is only an
image and attribution)

- Padding fix in contact editor

Bug: 5279541
Change-Id: Ia1b77da74b18371b022d49720ab42a7b234ba331
parent 095af25e
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -26,7 +26,7 @@
    android:paddingBottom="8dip"
    android:gravity="center_vertical"
    android:paddingLeft="@dimen/account_container_left_padding"
    android:paddingRight="32dip">
    android:paddingRight="28dip">

    <LinearLayout
        android:id="@+id/account"
+1 −1
Original line number Diff line number Diff line
@@ -24,7 +24,7 @@
    android:orientation="horizontal"
    android:gravity="center_vertical"
    android:paddingLeft="@dimen/account_container_left_padding"
    android:paddingRight="32dip">
    android:paddingRight="28dip">

    <LinearLayout
        android:id="@+id/account"
+10 −9
Original line number Diff line number Diff line
@@ -350,15 +350,16 @@ public class ContactDetailDisplayUtils {
                R.id.stream_item_attribution);
        TextView commentsView = (TextView) rootView.findViewById(R.id.stream_item_comments);
        ImageGetter imageGetter = new DefaultImageGetter(context.getPackageManager());
        htmlView.setText(HtmlUtils.fromHtml(context, streamItem.getText(), imageGetter, null));
        attributionView.setText(ContactBadgeUtil.getSocialDate(streamItem, context));
        if (streamItem.getComments() != null) {
            commentsView.setText(HtmlUtils.fromHtml(context, streamItem.getComments(), imageGetter,
                    null));
            commentsView.setVisibility(View.VISIBLE);
        } else {
            commentsView.setVisibility(View.GONE);
        }

        // Stream item text
        setDataOrHideIfNone(HtmlUtils.fromHtml(context, streamItem.getText(), imageGetter, null),
                htmlView);
        // Attribution
        setDataOrHideIfNone(ContactBadgeUtil.getSocialDate(streamItem, context),
                attributionView);
        // Comments
        setDataOrHideIfNone(HtmlUtils.fromHtml(context, streamItem.getComments(), imageGetter,
                null), commentsView);
        return rootView;
    }

+47 −17
Original line number Diff line number Diff line
package com.android.contacts.util;

import com.android.contacts.R;
import com.google.common.annotations.VisibleForTesting;

import android.content.Context;
import android.content.res.Resources;
import android.text.Html;
@@ -11,8 +14,6 @@ import android.text.TextUtils;
import android.text.style.ImageSpan;
import android.text.style.QuoteSpan;

import com.android.contacts.R;

/**
 * Provides static functions to perform custom HTML to text conversions.
 * Specifically, it adjusts the color and padding of the vertical
@@ -21,43 +22,53 @@ import com.android.contacts.R;
public class HtmlUtils {

    /**
     * Converts HTML string to a {@link Spanned} text, adjusting formatting.
     * Converts HTML string to a {@link Spanned} text, adjusting formatting. Any extra new line
     * characters at the end of the text will be trimmed.
     */
    public static Spanned fromHtml(Context context, String text) {
        if (TextUtils.isEmpty(text)) {
            return null;
        }
        Spanned spanned = Html.fromHtml(text);
        postprocess(context, spanned);
        return spanned;
        return postprocess(context, spanned);
    }

    /**
     * Converts HTML string to a {@link Spanned} text, adjusting formatting and using a custom
     * image getter.
     * image getter. Any extra new line characters at the end of the text will be trimmed.
     */
    public static CharSequence fromHtml(Context context, String text, ImageGetter imageGetter,
            TagHandler tagHandler) {
        if (TextUtils.isEmpty(text)) {
            return null;
        }
        Spanned spanned = Html.fromHtml(text, imageGetter, tagHandler);
        postprocess(context, spanned);
        return spanned;
        return postprocess(context, Html.fromHtml(text, imageGetter, tagHandler));
    }

    /**
     * Replaces some spans with custom versions of those.
     * Replaces some spans with custom versions of those. Any extra new line characters at the end
     * of the text will be trimmed.
     */
    private static void postprocess(Context context, Spanned spanned) {
        if (!(spanned instanceof SpannableStringBuilder)) {
            return;
    @VisibleForTesting
    static Spanned postprocess(Context context, Spanned original) {
        if (original == null) {
            return null;
        }
        final int length = original.length();
        if (length == 0) {
            return original; // Bail early.
        }

        int length = spanned.length();
        // If it's a SpannableStringBuilder, just use it.  Otherwise, create a new
        // SpannableStringBuilder based on the passed Spanned.
        final SpannableStringBuilder builder;
        if (original instanceof SpannableStringBuilder) {
            builder = (SpannableStringBuilder) original;
        } else {
            builder = new SpannableStringBuilder(original);
        }

        SpannableStringBuilder builder = (SpannableStringBuilder)spanned;
        QuoteSpan[] quoteSpans = spanned.getSpans(0, length, QuoteSpan.class);
        final QuoteSpan[] quoteSpans = builder.getSpans(0, length, QuoteSpan.class);
        if (quoteSpans != null && quoteSpans.length != 0) {
            Resources resources = context.getResources();
            int color = resources.getColor(R.color.stream_item_stripe_color);
@@ -67,7 +78,7 @@ public class HtmlUtils {
            }
        }

        ImageSpan[] imageSpans = spanned.getSpans(0, length, ImageSpan.class);
        final ImageSpan[] imageSpans = builder.getSpans(0, length, ImageSpan.class);
        if (imageSpans != null) {
            for (int i = 0; i < imageSpans.length; i++) {
                ImageSpan span = imageSpans[i];
@@ -75,6 +86,25 @@ public class HtmlUtils {
                        ImageSpan.ALIGN_BASELINE));
            }
        }

        // Trim the trailing new line characters at the end of the text (which can be added
        // when HTML block quote tags are turned into new line characters).
        int end = length;
        for (int i = builder.length() - 1; i >= 0; i--) {
            if (builder.charAt(i) != '\n') {
                break;
            }
            end = i;
        }

        // If there's no trailing newlines, just return it.
        if (end == length) {
            return builder;
        }

        // Otherwise, Return a substring of the original {@link Spanned} text
        // from the start index (inclusive) to the end index (exclusive).
        return new SpannableStringBuilder(builder, 0, end);
    }

    /**
+122 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * 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.android.contacts.util;

import com.android.contacts.util.HtmlUtils.StreamItemQuoteSpan;

import android.graphics.drawable.ColorDrawable;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.SpannedString;
import android.text.style.ImageSpan;
import android.text.style.QuoteSpan;

/**
 * Tests for {@link HtmlUtils}.
 *
 * adb shell am instrument -w -e class com.android.contacts.util.HtmlUtilsTest \
       com.android.contacts.tests/android.test.InstrumentationTestRunner
 */
@SmallTest
public class HtmlUtilsTest extends AndroidTestCase {
    /**
     * Test for {@link HtmlUtils#postprocess} specifically about trimming newlines.
     */
    public void testPostProcess_trimNewLines() {
        checkTrimNewLines("", "");
        checkTrimNewLines("", "\n");
        checkTrimNewLines("", "\n\n");
        checkTrimNewLines("a", "a");
        checkTrimNewLines("abc", "abc");
        checkTrimNewLines("abc", "abc\n");
        checkTrimNewLines("abc", "abc\n\n\n");
        checkTrimNewLines("ab\nc", "ab\nc\n");

        assertNull(HtmlUtils.postprocess(getContext(), null));
    }

    private final void checkTrimNewLines(String expectedString, CharSequence text) {
        // Test with both SpannedString and SpannableStringBuilder.
        assertEquals(expectedString,
                HtmlUtils.postprocess(getContext(), new SpannedString(text)).toString());

        assertEquals(expectedString,
                HtmlUtils.postprocess(getContext(), new SpannableStringBuilder(text)).toString());
    }

    public void testPostProcess_with_newlines() {
        final SpannableStringBuilder builder = new SpannableStringBuilder("01234\n\n");

        setSpans(builder);

        // First test with a SpannableStringBuilder, as opposed to SpannedString
        checkPostProcess(HtmlUtils.postprocess(getContext(), builder));

        // Then pass a SpannedString, which is immutable, but the method should still work.
        checkPostProcess(HtmlUtils.postprocess(getContext(), new SpannedString(builder)));
    }

    /**
     * Same as {@link #testPostProcess_with_newlines}, but text has no newlines.
     * (The internal code path is slightly different.)
     */
    public void testPostProcess_no_newlines() {
        final SpannableStringBuilder builder = new SpannableStringBuilder("01234");

        setSpans(builder);

        // First test with a SpannableStringBuilder, as opposed to SpannedString
        checkPostProcess(HtmlUtils.postprocess(getContext(), builder));

        // Then pass a SpannedString, which is immutable, but the method should still work.
        checkPostProcess(HtmlUtils.postprocess(getContext(), new SpannedString(builder)));
    }

    private void setSpans(SpannableStringBuilder builder) {
        builder.setSpan(new ImageSpan(new ColorDrawable(), ImageSpan.ALIGN_BOTTOM), 0, 2, 0);
        builder.setSpan(new QuoteSpan(), 2, 4, 0);
        builder.setSpan(new CustomSpan(), 4, builder.length(), 0);
    }

    private void checkPostProcess(Spanned ret) {
        // Newlines should be trimmed.
        assertEquals("01234", ret.toString());

        // First, check the image span.
        // - Vertical alignment should be changed to ALIGN_BASELINE
        // - Drawable shouldn't be changed.
        ImageSpan[] imageSpans = ret.getSpans(0, ret.length(), ImageSpan.class);
        assertEquals(1, imageSpans.length);
        assertEquals(ImageSpan.ALIGN_BASELINE, imageSpans[0].getVerticalAlignment());
        assertEquals(ColorDrawable.class, imageSpans[0].getDrawable().getClass());

        // QuoteSpans should be replaced with StreamItemQuoteSpans.
        QuoteSpan[] quoteSpans = ret.getSpans(0, ret.length(), QuoteSpan.class);
        assertEquals(1, quoteSpans.length);
        assertEquals(StreamItemQuoteSpan.class, quoteSpans[0].getClass());

        // Other spans should be preserved.
        CustomSpan[] customSpans = ret.getSpans(0, ret.length(), CustomSpan.class);
        assertEquals(1, customSpans.length);
    }

    /** Custom span class used in {@link #testPostProcess} */
    private static class CustomSpan {
    }
}