Loading res/layout/editor_account_header.xml +1 −1 Original line number Diff line number Diff line Loading @@ -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" Loading res/layout/editor_account_header_with_dropdown.xml +1 −1 Original line number Diff line number Diff line Loading @@ -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" Loading src/com/android/contacts/detail/ContactDetailDisplayUtils.java +10 −9 Original line number Diff line number Diff line Loading @@ -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; } Loading src/com/android/contacts/util/HtmlUtils.java +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; Loading @@ -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 Loading @@ -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); Loading @@ -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]; Loading @@ -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); } /** Loading tests/src/com/android/contacts/util/HtmlUtilsTest.java 0 → 100644 +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 { } } Loading
res/layout/editor_account_header.xml +1 −1 Original line number Diff line number Diff line Loading @@ -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" Loading
res/layout/editor_account_header_with_dropdown.xml +1 −1 Original line number Diff line number Diff line Loading @@ -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" Loading
src/com/android/contacts/detail/ContactDetailDisplayUtils.java +10 −9 Original line number Diff line number Diff line Loading @@ -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; } Loading
src/com/android/contacts/util/HtmlUtils.java +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; Loading @@ -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 Loading @@ -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); Loading @@ -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]; Loading @@ -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); } /** Loading
tests/src/com/android/contacts/util/HtmlUtilsTest.java 0 → 100644 +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 { } }