Check against null text in TextView constructor

It is common for TextView subclasses to override the #setText method,
and there is nothing preventing the new implementations from leaving
mText and mTransformed unchanged. On the other hand, TextView assumes
mText and mTransformed will be non null at all times after the first
overridden implementations, these can remain null, causing null pointer
exceptions later.

Bug: 74411872
Test: atest FrameworksCoreTests:android.widget.TextViewTest
Test: atest FrameworksCoreTests:android.widget.TextViewActivityTest
Test: atest CtsWidgetTestCases:android.widget.cts.TextViewTest
Change-Id: Ifaddb46d156c495a371789c6f32cfd67ffaaaef2
This commit is contained in:
Mihai Popa
2018-03-12 19:30:22 +00:00
parent 33b6d0ec21
commit a875271fcc
3 changed files with 64 additions and 0 deletions

View File

@@ -32,6 +32,8 @@ public interface TransformationMethod
* Beware that the returned text must be exactly the same length as
* the source text, and that if the source text is Editable, the returned
* text must mirror it dynamically instead of doing a one-time copy.
* The method should not return {@code null} unless {@code source}
* is {@code null}.
*/
public CharSequence getTransformation(CharSequence source, View view);

View File

@@ -1508,6 +1508,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
setText(text, bufferType);
if (mText == null) {
mText = "";
}
if (mTransformed == null) {
mTransformed = "";
}
if (textIsSetFromXml) {
mTextSetFromXmlOrResourceId = true;
}
@@ -2191,6 +2198,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return (mText instanceof Editable) ? (Editable) mText : null;
}
/**
* @hide
*/
@VisibleForTesting
public CharSequence getTransformed() {
return mTransformed;
}
/**
* Gets the vertical distance between lines of text, in pixels.
* Note that markup within the text can cause individual lines
@@ -5532,6 +5547,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
* PrecomputedText mismatches with this TextView, IllegalArgumentException is thrown. To ensure
* the parameters match, you can call {@link TextView#setTextMetricsParams} before calling this.
*
* Subclasses overriding this method should ensure that the following post condition holds,
* in order to guarantee the safety of the view's measurement and layout operations:
* regardless of the input, after calling #setText both {@code mText} and {@code mTransformed}
* will be different from {@code null}.
*
* @param text text to be displayed
*
* @attr ref android.R.styleable#TextView_text
@@ -5554,6 +5574,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
* {@link android.text.Editable.Factory} to create final or intermediate
* {@link Editable Editables}.
*
* Subclasses overriding this method should ensure that the following post condition holds,
* in order to guarantee the safety of the view's measurement and layout operations:
* regardless of the input, after calling #setText both {@code mText} and {@code mTransformed}
* will be different from {@code null}.
*
* @param text text to be displayed
*
* @see #setText(CharSequence)
@@ -5706,6 +5731,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
} else {
mTransformed = mTransformation.getTransformation(text, this);
}
if (mTransformed == null) {
// Should not happen if the transformation method follows the non-null postcondition.
mTransformed = "";
}
final int textLength = text.length();

View File

@@ -23,6 +23,7 @@ import static junit.framework.Assert.assertTrue;
import android.app.Activity;
import android.app.Instrumentation;
import android.content.Context;
import android.content.Intent;
import android.graphics.Paint;
import android.platform.test.annotations.Presubmit;
@@ -44,6 +45,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.lang.reflect.Field;
import java.util.Locale;
/**
@@ -320,6 +322,15 @@ public class TextViewTest {
assertTrue(mTextView.useDynamicLayout());
}
@Test
@UiThreadTest
public void testConstructor_doesNotLeaveTextNull() {
mTextView = new NullSetTextTextView(mActivity);
// Check that mText and mTransformed are empty string instead of null.
assertEquals("", mTextView.getText().toString());
assertEquals("", mTextView.getTransformed().toString());
}
private String createLongText() {
int size = 600 * 1000;
final StringBuilder builder = new StringBuilder(size);
@@ -328,4 +339,26 @@ public class TextViewTest {
}
return builder.toString();
}
private class NullSetTextTextView extends TextView {
NullSetTextTextView(Context context) {
super(context);
}
@Override
public void setText(CharSequence text, BufferType type) {
// #setText will be called from the TextView constructor. Here we reproduce
// the situation when the method sets mText and mTransformed to null.
try {
final Field textField = TextView.class.getDeclaredField("mText");
textField.setAccessible(true);
textField.set(this, null);
final Field transformedField = TextView.class.getDeclaredField("mTransformed");
transformedField.setAccessible(true);
transformedField.set(this, null);
} catch (NoSuchFieldException | IllegalAccessException e) {
// Empty.
}
}
}
}