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

Commit a875271f authored by Mihai Popa's avatar Mihai Popa
Browse files

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
parent 33b6d0ec
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -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);

+29 −0
Original line number Diff line number Diff line
@@ -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();

+33 −0
Original line number Diff line number Diff line
@@ -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.
            }
        }
    }
}