From 64f1374c6435f0a5fec1345375690e24d912b81d Mon Sep 17 00:00:00 2001 From: Peter_Liang Date: Thu, 31 Oct 2019 11:45:16 +0800 Subject: [PATCH] Support the rich content for accessibility service (1/n) Goal: 1. let third party developer can use html text that include from their local image file and animated image instead of only plain text to rich their content. 2. Avoid malicious links made by third party developer Action: 1. Add html, static text, and animated image preferences. 2. Add android:AnimatedImageDrawable, and android:htmlDescription attributes. 3. Fine-tune interface and integration 4. Add custom tag filter Bug: 136292241 Test: Maunal & make RunSettingsRoboTests Change-Id: I82cd5319efb7faa1ff7e8354a279828fce5135b8 --- res/layout/preference_animated_image.xml | 33 +++++ res/layout/preference_static_text.xml | 65 ++++++++ .../AccessibilityDetailsSettingsFragment.java | 3 + .../accessibility/AccessibilitySettings.java | 6 + .../AccessibilitySettingsForSetupWizard.java | 3 + .../AnimatedImagePreference.java | 85 +++++++++++ .../accessibility/HtmlTextPreference.java | 139 ++++++++++++++++++ .../accessibility/StaticTextPreference.java | 42 ++++++ ...ccessibilityServicePreferenceFragment.java | 13 +- .../ToggleFeaturePreferenceFragment.java | 91 +++++++++++- .../accessibility/HtmlTextPreferenceTest.java | 85 +++++++++++ 11 files changed, 555 insertions(+), 10 deletions(-) create mode 100644 res/layout/preference_animated_image.xml create mode 100644 res/layout/preference_static_text.xml create mode 100644 src/com/android/settings/accessibility/AnimatedImagePreference.java create mode 100644 src/com/android/settings/accessibility/HtmlTextPreference.java create mode 100644 src/com/android/settings/accessibility/StaticTextPreference.java create mode 100644 tests/robotests/src/com/android/settings/accessibility/HtmlTextPreferenceTest.java diff --git a/res/layout/preference_animated_image.xml b/res/layout/preference_animated_image.xml new file mode 100644 index 00000000000..305b03630e1 --- /dev/null +++ b/res/layout/preference_animated_image.xml @@ -0,0 +1,33 @@ + + + + + + + \ No newline at end of file diff --git a/res/layout/preference_static_text.xml b/res/layout/preference_static_text.xml new file mode 100644 index 00000000000..b36fd58b1f3 --- /dev/null +++ b/res/layout/preference_static_text.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/com/android/settings/accessibility/AccessibilityDetailsSettingsFragment.java b/src/com/android/settings/accessibility/AccessibilityDetailsSettingsFragment.java index 18700dc0c4c..73df875a2be 100644 --- a/src/com/android/settings/accessibility/AccessibilityDetailsSettingsFragment.java +++ b/src/com/android/settings/accessibility/AccessibilityDetailsSettingsFragment.java @@ -164,7 +164,10 @@ public class AccessibilityDetailsSettingsFragment extends InstrumentedFragment { new ComponentName(packageName, settingsClassName).flattenToString()); } extras.putParcelable(AccessibilitySettings.EXTRA_COMPONENT_NAME, componentName); + extras.putInt(AccessibilitySettings.EXTRA_ANIMATED_IMAGE_RES, info.getAnimatedImageRes()); + final String htmlDescription = info.loadHtmlDescription(getActivity().getPackageManager()); + extras.putString(AccessibilitySettings.EXTRA_HTML_DESCRIPTION, htmlDescription); return extras; } diff --git a/src/com/android/settings/accessibility/AccessibilitySettings.java b/src/com/android/settings/accessibility/AccessibilitySettings.java index 5eb33133fcf..ee73e6fe02e 100644 --- a/src/com/android/settings/accessibility/AccessibilitySettings.java +++ b/src/com/android/settings/accessibility/AccessibilitySettings.java @@ -111,6 +111,8 @@ public class AccessibilitySettings extends DashboardFragment { static final String EXTRA_SETTINGS_COMPONENT_NAME = "settings_component_name"; static final String EXTRA_VIDEO_RAW_RESOURCE_ID = "video_resource"; static final String EXTRA_LAUNCHED_FROM_SUW = "from_suw"; + static final String EXTRA_ANIMATED_IMAGE_RES = "animated_image_res"; + static final String EXTRA_HTML_DESCRIPTION = "html_description"; // Timeout before we update the services if packages are added/removed // since the AccessibilityManagerService has to do that processing first @@ -409,6 +411,10 @@ public class AccessibilitySettings extends DashboardFragment { extras.putString(EXTRA_TITLE, title); extras.putParcelable(EXTRA_RESOLVE_INFO, resolveInfo); extras.putString(EXTRA_SUMMARY, description); + extras.putInt(EXTRA_ANIMATED_IMAGE_RES, info.getAnimatedImageRes()); + + final String htmlDescription = info.loadHtmlDescription(getPackageManager()); + extras.putString(AccessibilitySettings.EXTRA_HTML_DESCRIPTION, htmlDescription); final String settingsClassName = info.getSettingsActivityName(); if (!TextUtils.isEmpty(settingsClassName)) { diff --git a/src/com/android/settings/accessibility/AccessibilitySettingsForSetupWizard.java b/src/com/android/settings/accessibility/AccessibilitySettingsForSetupWizard.java index 16f5fcdd245..64ed4869285 100644 --- a/src/com/android/settings/accessibility/AccessibilitySettingsForSetupWizard.java +++ b/src/com/android/settings/accessibility/AccessibilitySettingsForSetupWizard.java @@ -169,6 +169,9 @@ public class AccessibilitySettingsForSetupWizard extends SettingsPreferenceFragm description = getString(R.string.accessibility_service_default_description); } extras.putString(AccessibilitySettings.EXTRA_SUMMARY, description); + + final String htmlDescription = info.loadHtmlDescription(getPackageManager()); + extras.putString(AccessibilitySettings.EXTRA_HTML_DESCRIPTION, htmlDescription); } private static void configureMagnificationPreferenceIfNeeded(Preference preference) { diff --git a/src/com/android/settings/accessibility/AnimatedImagePreference.java b/src/com/android/settings/accessibility/AnimatedImagePreference.java new file mode 100644 index 00000000000..ea9e1f825f7 --- /dev/null +++ b/src/com/android/settings/accessibility/AnimatedImagePreference.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2019 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.settings.accessibility; + +import android.content.Context; +import android.graphics.drawable.AnimatedImageDrawable; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.widget.ImageView; + +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + +import com.android.settings.R; + +/** + * A custom {@link ImageView} preference for showing animated or static image, such as animated webp and static png. + */ +public class AnimatedImagePreference extends Preference { + + private boolean mDividerAllowedAbove = false; + private Uri mImageUri; + + AnimatedImagePreference(Context context) { + super(context); + setLayoutResource(R.layout.preference_animated_image); + } + + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + holder.setDividerAllowedAbove(mDividerAllowedAbove); + + final ImageView imageView = holder.itemView.findViewById(R.id.animated_img); + if (imageView != null && mImageUri != null) { + imageView.setImageURI(mImageUri); + + final Drawable drawable = imageView.getDrawable(); + if (drawable != null) { + if (drawable instanceof AnimatedImageDrawable) { + ((AnimatedImageDrawable) drawable).start(); + } + } + } + } + + /** + * Sets divider whether to show in preference above. + * + * @param allowed true will be drawn on above this item + */ + public void setDividerAllowedAbove(boolean allowed) { + if (allowed != mDividerAllowedAbove) { + mDividerAllowedAbove = allowed; + notifyChanged(); + } + } + + /** + * Set image uri to display image in {@link ImageView} + * + * @param imageUri the Uri of an image + */ + public void setImageUri(Uri imageUri) { + if (imageUri != null && !imageUri.equals(mImageUri)) { + mImageUri = imageUri; + notifyChanged(); + } + } +} diff --git a/src/com/android/settings/accessibility/HtmlTextPreference.java b/src/com/android/settings/accessibility/HtmlTextPreference.java new file mode 100644 index 00000000000..0c295e37a51 --- /dev/null +++ b/src/com/android/settings/accessibility/HtmlTextPreference.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2019 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.settings.accessibility; + +import android.content.Context; +import android.text.Html; +import android.text.TextUtils; +import android.widget.TextView; + +import androidx.preference.PreferenceViewHolder; + +import java.util.List; +import java.util.regex.Pattern; + +/** + * A custom {@link android.widget.TextView} preference that shows html text with a custom tag + * filter. + */ +public final class HtmlTextPreference extends StaticTextPreference { + + private boolean mDividerAllowedAbove = false; + private int mFlag = Html.FROM_HTML_MODE_COMPACT; + private Html.ImageGetter mImageGetter; + private Html.TagHandler mTagHandler; + private List mUnsupportedTagList; + + HtmlTextPreference(Context context) { + super(context); + } + + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + holder.setDividerAllowedAbove(mDividerAllowedAbove); + + final TextView summaryView = holder.itemView.findViewById(android.R.id.summary); + if (summaryView != null && !TextUtils.isEmpty(getSummary())) { + final String filteredText = getFilteredText(getSummary().toString()); + summaryView.setText(Html.fromHtml(filteredText, mFlag, mImageGetter, mTagHandler)); + } + } + + /** + * Sets divider whether to show in preference above. + * + * @param allowed true will be drawn on above this item + */ + public void setDividerAllowedAbove(boolean allowed) { + if (allowed != mDividerAllowedAbove) { + mDividerAllowedAbove = allowed; + notifyChanged(); + } + } + + /** + * Sets the flag to which text format to be applied. + * + * @param flag to indicate that html text format + */ + public void setFlag(int flag) { + if (flag != mFlag) { + mFlag = flag; + notifyChanged(); + } + } + + /** + * Sets image getter and help to load corresponding images when parsing. + * + * @param imageGetter to load image by image tag content + */ + public void setImageGetter(Html.ImageGetter imageGetter) { + if (imageGetter != null && !imageGetter.equals(mImageGetter)) { + mImageGetter = imageGetter; + notifyChanged(); + } + } + + /** + * Sets tag handler to handle the unsupported tags. + * + * @param tagHandler the handler for unhandled tags + */ + public void setTagHandler(Html.TagHandler tagHandler) { + if (tagHandler != null && !tagHandler.equals(mTagHandler)) { + mTagHandler = tagHandler; + notifyChanged(); + } + } + + /** + * Sets unsupported tag list, the text will be filtered though this list in advanced. + * + * @param unsupportedTagList the list of unsupported tags + */ + public void setUnsupportedTagList(List unsupportedTagList) { + if (unsupportedTagList != null && !unsupportedTagList.equals(mUnsupportedTagList)) { + mUnsupportedTagList = unsupportedTagList; + notifyChanged(); + } + } + + private String getFilteredText(String text) { + if (mUnsupportedTagList == null) { + return text; + } + + int i = 1; + for (String tag : mUnsupportedTagList) { + if (!TextUtils.isEmpty(text)) { + final String index = String.valueOf(i++); + final String targetStart1 = "(?i)<" + tag + " "; + final String targetStart2 = "(?i)<" + tag + ">"; + final String replacementStart1 = ""; + final String targetEnd = "(?i)"; + final String replacementEnd = ""; + text = Pattern.compile(targetStart1).matcher(text).replaceAll(replacementStart1); + text = Pattern.compile(targetStart2).matcher(text).replaceAll(replacementStart2); + text = Pattern.compile(targetEnd).matcher(text).replaceAll(replacementEnd); + } + } + return text; + } +} diff --git a/src/com/android/settings/accessibility/StaticTextPreference.java b/src/com/android/settings/accessibility/StaticTextPreference.java new file mode 100644 index 00000000000..32707480b98 --- /dev/null +++ b/src/com/android/settings/accessibility/StaticTextPreference.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2019 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.settings.accessibility; + +import android.content.Context; + +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + +import com.android.settings.R; + +/** + * A custom {@link android.widget.TextView} preference that removes the title and summary + * restriction from platform {@link Preference} implementation and the icon location is kept as + * gravity top instead of center. + */ +public class StaticTextPreference extends Preference { + + StaticTextPreference(Context context) { + super(context); + setLayoutResource(R.layout.preference_static_text); + } + + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + } +} diff --git a/src/com/android/settings/accessibility/ToggleAccessibilityServicePreferenceFragment.java b/src/com/android/settings/accessibility/ToggleAccessibilityServicePreferenceFragment.java index f30af54fc36..ea293b7b864 100644 --- a/src/com/android/settings/accessibility/ToggleAccessibilityServicePreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleAccessibilityServicePreferenceFragment.java @@ -24,6 +24,7 @@ import android.app.Dialog; import android.app.admin.DevicePolicyManager; import android.app.settings.SettingsEnums; import android.content.ComponentName; +import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -69,8 +70,6 @@ public class ToggleAccessibilityServicePreferenceFragment extends ToggleFeatureP } }; - private ComponentName mComponentName; - private Dialog mDialog; private final View.OnClickListener mViewOnClickListener = @@ -341,5 +340,15 @@ public class ToggleAccessibilityServicePreferenceFragment extends ToggleFeatureP } mComponentName = arguments.getParcelable(AccessibilitySettings.EXTRA_COMPONENT_NAME); + + // Settings animated image. + int animatedImageRes = arguments.getInt(AccessibilitySettings.EXTRA_ANIMATED_IMAGE_RES); + mImageUri = new Uri.Builder().scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .authority(mComponentName.getPackageName()) + .appendPath(String.valueOf(animatedImageRes)) + .build(); + + // Settings html description. + mHtmlDescription = arguments.getCharSequence(AccessibilitySettings.EXTRA_HTML_DESCRIPTION); } } diff --git a/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java b/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java index a4b18787fdb..094b909f444 100644 --- a/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java @@ -16,10 +16,16 @@ package com.android.settings.accessibility; +import android.content.ComponentName; +import android.content.ContentResolver; import android.content.Intent; import android.content.pm.ResolveInfo; +import android.graphics.drawable.Drawable; +import android.net.Uri; import android.os.Bundle; +import android.text.Html; import android.view.View; +import android.widget.ImageView; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; @@ -29,7 +35,9 @@ import com.android.settings.SettingsActivity; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.widget.SwitchBar; import com.android.settings.widget.ToggleSwitch; -import com.android.settingslib.widget.FooterPreference; + +import java.util.ArrayList; +import java.util.List; public abstract class ToggleFeaturePreferenceFragment extends SettingsPreferenceFragment { @@ -40,6 +48,30 @@ public abstract class ToggleFeaturePreferenceFragment extends SettingsPreference protected CharSequence mSettingsTitle; protected Intent mSettingsIntent; + protected ComponentName mComponentName; + protected Uri mImageUri; + protected CharSequence mStaticDescription; + protected CharSequence mHtmlDescription; + private static final String ANCHOR_TAG = "a"; + private static final String DRAWABLE_FOLDER = "drawable"; + + // For html description of accessibility service, third party developer must follow the rule, + // such as , a11y settings will get third party resources + // by this. + private static final String IMG_PREFIX = "R.drawable."; + + private ImageView mImageGetterCacheView; + + private final Html.ImageGetter mImageGetter = (String str) -> { + if (str != null && str.startsWith(IMG_PREFIX)) { + final String fileName = str.substring(IMG_PREFIX.length()); + return getDrawableFromUri(Uri.parse( + ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + + mComponentName.getPackageName() + "/" + DRAWABLE_FOLDER + "/" + + fileName)); + } + return null; + }; @Override public void onCreate(Bundle savedInstanceState) { @@ -63,15 +95,45 @@ public abstract class ToggleFeaturePreferenceFragment extends SettingsPreference onProcessArguments(getArguments()); updateSwitchBarText(mSwitchBar); + PreferenceScreen preferenceScreen = getPreferenceScreen(); + // Show the "Settings" menu as if it were a preference screen if (mSettingsTitle != null && mSettingsIntent != null) { - PreferenceScreen preferenceScreen = getPreferenceScreen(); Preference settingsPref = new Preference(preferenceScreen.getContext()); settingsPref.setTitle(mSettingsTitle); settingsPref.setIconSpaceReserved(true); settingsPref.setIntent(mSettingsIntent); preferenceScreen.addPreference(settingsPref); } + + if (mImageUri != null) { + final AnimatedImagePreference animatedImagePreference = new AnimatedImagePreference( + preferenceScreen.getContext()); + animatedImagePreference.setImageUri(mImageUri); + animatedImagePreference.setDividerAllowedAbove(true); + preferenceScreen.addPreference(animatedImagePreference); + } + + if (mStaticDescription != null) { + final StaticTextPreference staticTextPreference = new StaticTextPreference( + preferenceScreen.getContext()); + staticTextPreference.setSummary(mStaticDescription); + preferenceScreen.addPreference(staticTextPreference); + } + + if (mHtmlDescription != null) { + // For accessibility service, avoid malicious links made by third party developer + final List unsupportedTagList = new ArrayList<>(); + unsupportedTagList.add(ANCHOR_TAG); + + final HtmlTextPreference htmlTextPreference = new HtmlTextPreference( + preferenceScreen.getContext()); + htmlTextPreference.setSummary(mHtmlDescription); + htmlTextPreference.setImageGetter(mImageGetter); + htmlTextPreference.setUnsupportedTagList(unsupportedTagList); + htmlTextPreference.setDividerAllowedAbove(true); + preferenceScreen.addPreference(htmlTextPreference); + } } @Override @@ -139,17 +201,30 @@ public abstract class ToggleFeaturePreferenceFragment extends SettingsPreference // Summary. if (arguments.containsKey(AccessibilitySettings.EXTRA_SUMMARY_RES)) { final int summary = arguments.getInt(AccessibilitySettings.EXTRA_SUMMARY_RES); - createFooterPreference(getText(summary)); + mStaticDescription = getText(summary); } else if (arguments.containsKey(AccessibilitySettings.EXTRA_SUMMARY)) { final CharSequence summary = arguments.getCharSequence( AccessibilitySettings.EXTRA_SUMMARY); - createFooterPreference(summary); + mStaticDescription = summary; } } - private void createFooterPreference(CharSequence title) { - final PreferenceScreen preferenceScreen = getPreferenceScreen(); - preferenceScreen.addPreference(new FooterPreference.Builder(getActivity()).setTitle( - title).build()); + private Drawable getDrawableFromUri(Uri imageUri) { + if (mImageGetterCacheView == null) { + mImageGetterCacheView = new ImageView(getContext()); + } + + mImageGetterCacheView.setAdjustViewBounds(true); + mImageGetterCacheView.setImageURI(imageUri); + + final Drawable drawable = mImageGetterCacheView.getDrawable().mutate(); + if (drawable != null) { + drawable.setBounds(/* left= */0, /* top= */0, drawable.getIntrinsicWidth(), + drawable.getIntrinsicHeight()); + } + + mImageGetterCacheView.setImageURI(null); + mImageGetterCacheView.setImageDrawable(null); + return drawable; } } diff --git a/tests/robotests/src/com/android/settings/accessibility/HtmlTextPreferenceTest.java b/tests/robotests/src/com/android/settings/accessibility/HtmlTextPreferenceTest.java new file mode 100644 index 00000000000..5ac5bad728d --- /dev/null +++ b/tests/robotests/src/com/android/settings/accessibility/HtmlTextPreferenceTest.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2019 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.settings.accessibility; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.text.Editable; +import android.text.Html; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +import androidx.preference.PreferenceViewHolder; + +import com.android.settings.R; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.xml.sax.XMLReader; + +import java.util.ArrayList; +import java.util.List; + +/** Tests for {@link HtmlTextPreference} */ +@RunWith(RobolectricTestRunner.class) +public final class HtmlTextPreferenceTest { + + private HtmlTextPreference mHtmlTextPreference; + private PreferenceViewHolder mPreferenceViewHolder; + private String mHandledTag; + private final Html.TagHandler mTagHandler = new Html.TagHandler() { + @Override + public void handleTag(boolean opening, String tag, Editable editable, XMLReader xmlReader) { + mHandledTag = tag; + } + }; + + @Before + public void setUp() { + final Context context = RuntimeEnvironment.application; + mHtmlTextPreference = new HtmlTextPreference(context); + + final LayoutInflater inflater = LayoutInflater.from(context); + final View view = + inflater.inflate(R.layout.preference_static_text, null); + mPreferenceViewHolder = PreferenceViewHolder.createInstanceForTests(view); + } + + @Test + public void testUnsupportedTagList_keepRealContentWithoutTag() { + final List testUnsupportedTagList = new ArrayList<>(); + testUnsupportedTagList.add("testTag"); + final String testStr = "Real description"; + final String expectedStr = "Real description"; + final String expectedTag = "unsupportedtag1"; + + mHtmlTextPreference.setUnsupportedTagList(testUnsupportedTagList); + mHtmlTextPreference.setSummary(testStr); + mHtmlTextPreference.setTagHandler(mTagHandler); + mHtmlTextPreference.onBindViewHolder(mPreferenceViewHolder); + + final TextView summaryView = mPreferenceViewHolder.itemView.findViewById( + android.R.id.summary); + assertThat(summaryView.getText().toString()).isEqualTo(expectedStr); + assertThat(mHandledTag).isEqualTo(expectedTag); + } +}