Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package androidx.core.widget;
     18 
     19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
     20 
     21 import android.annotation.TargetApi;
     22 import android.app.Activity;
     23 import android.content.Context;
     24 import android.content.Intent;
     25 import android.content.pm.PackageManager;
     26 import android.content.pm.ResolveInfo;
     27 import android.graphics.Paint;
     28 import android.graphics.drawable.Drawable;
     29 import android.os.Build;
     30 import android.text.Editable;
     31 import android.util.Log;
     32 import android.util.TypedValue;
     33 import android.view.ActionMode;
     34 import android.view.Menu;
     35 import android.view.MenuItem;
     36 import android.view.View;
     37 import android.widget.TextView;
     38 
     39 import androidx.annotation.DrawableRes;
     40 import androidx.annotation.IntDef;
     41 import androidx.annotation.IntRange;
     42 import androidx.annotation.NonNull;
     43 import androidx.annotation.Nullable;
     44 import androidx.annotation.Px;
     45 import androidx.annotation.RestrictTo;
     46 import androidx.annotation.StyleRes;
     47 import androidx.core.os.BuildCompat;
     48 import androidx.core.util.Preconditions;
     49 
     50 import java.lang.annotation.Retention;
     51 import java.lang.annotation.RetentionPolicy;
     52 import java.lang.reflect.Field;
     53 import java.lang.reflect.InvocationTargetException;
     54 import java.lang.reflect.Method;
     55 import java.util.ArrayList;
     56 import java.util.List;
     57 
     58 /**
     59  * Helper for accessing features in {@link TextView}.
     60  */
     61 public final class TextViewCompat {
     62     private static final String LOG_TAG = "TextViewCompat";
     63 
     64     /**
     65      * The TextView does not auto-size text (default).
     66      */
     67     public static final int AUTO_SIZE_TEXT_TYPE_NONE = TextView.AUTO_SIZE_TEXT_TYPE_NONE;
     68 
     69     /**
     70      * The TextView scales text size both horizontally and vertically to fit within the
     71      * container.
     72      */
     73     public static final int AUTO_SIZE_TEXT_TYPE_UNIFORM = TextView.AUTO_SIZE_TEXT_TYPE_UNIFORM;
     74 
     75     /** @hide */
     76     @RestrictTo(LIBRARY_GROUP)
     77     @IntDef({AUTO_SIZE_TEXT_TYPE_NONE, AUTO_SIZE_TEXT_TYPE_UNIFORM})
     78     @Retention(RetentionPolicy.SOURCE)
     79     public @interface AutoSizeTextType {}
     80 
     81     private static Field sMaximumField;
     82     private static boolean sMaximumFieldFetched;
     83     private static Field sMaxModeField;
     84     private static boolean sMaxModeFieldFetched;
     85 
     86     private static Field sMinimumField;
     87     private static boolean sMinimumFieldFetched;
     88     private static Field sMinModeField;
     89     private static boolean sMinModeFieldFetched;
     90 
     91     private static final int LINES = 1;
     92 
     93     // Hide constructor
     94     private TextViewCompat() {}
     95 
     96     private static Field retrieveField(String fieldName) {
     97         Field field = null;
     98         try {
     99             field = TextView.class.getDeclaredField(fieldName);
    100             field.setAccessible(true);
    101         } catch (NoSuchFieldException e) {
    102             Log.e(LOG_TAG, "Could not retrieve " + fieldName + " field.");
    103         }
    104         return field;
    105     }
    106 
    107     private static int retrieveIntFromField(Field field, TextView textView) {
    108         try {
    109             return field.getInt(textView);
    110         } catch (IllegalAccessException e) {
    111             Log.d(LOG_TAG, "Could not retrieve value of " + field.getName() + " field.");
    112         }
    113         return -1;
    114     }
    115 
    116     /**
    117      * Sets the Drawables (if any) to appear to the start of, above, to the end
    118      * of, and below the text. Use {@code null} if you do not want a Drawable
    119      * there. The Drawables must already have had {@link Drawable#setBounds}
    120      * called.
    121      * <p/>
    122      * Calling this method will overwrite any Drawables previously set using
    123      * {@link TextView#setCompoundDrawables} or related methods.
    124      *
    125      * @param textView The TextView against which to invoke the method.
    126      * @attr name android:drawableStart
    127      * @attr name android:drawableTop
    128      * @attr name android:drawableEnd
    129      * @attr name android:drawableBottom
    130      */
    131     public static void setCompoundDrawablesRelative(@NonNull TextView textView,
    132             @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end,
    133             @Nullable Drawable bottom) {
    134         if (Build.VERSION.SDK_INT >= 18) {
    135             textView.setCompoundDrawablesRelative(start, top, end, bottom);
    136         } else if (Build.VERSION.SDK_INT >= 17) {
    137             boolean rtl = textView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
    138             textView.setCompoundDrawables(rtl ? end : start, top, rtl ? start : end, bottom);
    139         } else {
    140             textView.setCompoundDrawables(start, top, end, bottom);
    141         }
    142     }
    143 
    144     /**
    145      * Sets the Drawables (if any) to appear to the start of, above, to the end
    146      * of, and below the text. Use {@code null} if you do not want a Drawable
    147      * there. The Drawables' bounds will be set to their intrinsic bounds.
    148      * <p/>
    149      * Calling this method will overwrite any Drawables previously set using
    150      * {@link TextView#setCompoundDrawables} or related methods.
    151      *
    152      * @param textView The TextView against which to invoke the method.
    153      * @attr name android:drawableStart
    154      * @attr name android:drawableTop
    155      * @attr name android:drawableEnd
    156      * @attr name android:drawableBottom
    157      */
    158     public static void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
    159             @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end,
    160             @Nullable Drawable bottom) {
    161         if (Build.VERSION.SDK_INT >= 18) {
    162             textView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
    163         } else if (Build.VERSION.SDK_INT >= 17) {
    164             boolean rtl = textView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
    165             textView.setCompoundDrawablesWithIntrinsicBounds(rtl ? end : start, top,
    166                     rtl ? start : end,  bottom);
    167         } else {
    168             textView.setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom);
    169         }
    170     }
    171 
    172     /**
    173      * Sets the Drawables (if any) to appear to the start of, above, to the end
    174      * of, and below the text. Use 0 if you do not want a Drawable there. The
    175      * Drawables' bounds will be set to their intrinsic bounds.
    176      * <p/>
    177      * Calling this method will overwrite any Drawables previously set using
    178      * {@link TextView#setCompoundDrawables} or related methods.
    179      *
    180      * @param textView The TextView against which to invoke the method.
    181      * @param start    Resource identifier of the start Drawable.
    182      * @param top      Resource identifier of the top Drawable.
    183      * @param end      Resource identifier of the end Drawable.
    184      * @param bottom   Resource identifier of the bottom Drawable.
    185      * @attr name android:drawableStart
    186      * @attr name android:drawableTop
    187      * @attr name android:drawableEnd
    188      * @attr name android:drawableBottom
    189      */
    190     public static void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
    191             @DrawableRes int start, @DrawableRes int top, @DrawableRes int end,
    192             @DrawableRes int bottom) {
    193         if (Build.VERSION.SDK_INT >= 18) {
    194             textView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
    195         } else if (Build.VERSION.SDK_INT >= 17) {
    196             boolean rtl = textView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
    197             textView.setCompoundDrawablesWithIntrinsicBounds(rtl ? end : start, top,
    198                     rtl ? start : end, bottom);
    199         } else {
    200             textView.setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom);
    201         }
    202     }
    203 
    204     /**
    205      * Returns the maximum number of lines displayed in the given TextView, or -1 if the maximum
    206      * height was set in pixels instead.
    207      */
    208     public static int getMaxLines(@NonNull TextView textView) {
    209         if (Build.VERSION.SDK_INT >= 16) {
    210             return textView.getMaxLines();
    211         }
    212 
    213         if (!sMaxModeFieldFetched) {
    214             sMaxModeField = retrieveField("mMaxMode");
    215             sMaxModeFieldFetched = true;
    216         }
    217         if (sMaxModeField != null && retrieveIntFromField(sMaxModeField, textView) == LINES) {
    218             // If the max mode is using lines, we can grab the maximum value
    219             if (!sMaximumFieldFetched) {
    220                 sMaximumField = retrieveField("mMaximum");
    221                 sMaximumFieldFetched = true;
    222             }
    223             if (sMaximumField != null) {
    224                 return retrieveIntFromField(sMaximumField, textView);
    225             }
    226         }
    227         return -1;
    228     }
    229 
    230     /**
    231      * Returns the minimum number of lines displayed in the given TextView, or -1 if the minimum
    232      * height was set in pixels instead.
    233      */
    234     public static int getMinLines(@NonNull TextView textView) {
    235         if (Build.VERSION.SDK_INT >= 16) {
    236             return textView.getMinLines();
    237         }
    238 
    239         if (!sMinModeFieldFetched) {
    240             sMinModeField = retrieveField("mMinMode");
    241             sMinModeFieldFetched = true;
    242         }
    243         if (sMinModeField != null && retrieveIntFromField(sMinModeField, textView) == LINES) {
    244             // If the min mode is using lines, we can grab the maximum value
    245             if (!sMinimumFieldFetched) {
    246                 sMinimumField = retrieveField("mMinimum");
    247                 sMinimumFieldFetched = true;
    248             }
    249             if (sMinimumField != null) {
    250                 return retrieveIntFromField(sMinimumField, textView);
    251             }
    252         }
    253         return -1;
    254     }
    255 
    256     /**
    257      * Sets the text appearance from the specified style resource.
    258      * <p>
    259      * Use a framework-defined {@code TextAppearance} style like
    260      * {@link android.R.style#TextAppearance_Material_Body1 @android:style/TextAppearance.Material.Body1}.
    261      *
    262      * @param textView The TextView against which to invoke the method.
    263      * @param resId    The resource identifier of the style to apply.
    264      */
    265     public static void setTextAppearance(@NonNull TextView textView, @StyleRes int resId) {
    266         if (Build.VERSION.SDK_INT >= 23) {
    267             textView.setTextAppearance(resId);
    268         } else {
    269             textView.setTextAppearance(textView.getContext(), resId);
    270         }
    271     }
    272 
    273     /**
    274      * Returns drawables for the start, top, end, and bottom borders from the given text view.
    275      */
    276     @NonNull
    277     public static Drawable[] getCompoundDrawablesRelative(@NonNull TextView textView) {
    278         if (Build.VERSION.SDK_INT >= 18) {
    279             return textView.getCompoundDrawablesRelative();
    280         }
    281         if (Build.VERSION.SDK_INT >= 17) {
    282             final boolean rtl = textView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
    283             final Drawable[] compounds = textView.getCompoundDrawables();
    284             if (rtl) {
    285                 // If we're on RTL, we need to invert the horizontal result like above
    286                 final Drawable start = compounds[2];
    287                 final Drawable end = compounds[0];
    288                 compounds[0] = start;
    289                 compounds[2] = end;
    290             }
    291             return compounds;
    292         }
    293         return textView.getCompoundDrawables();
    294     }
    295 
    296     /**
    297      * Specify whether this widget should automatically scale the text to try to perfectly fit
    298      * within the layout bounds by using the default auto-size configuration.
    299      *
    300      * @param autoSizeTextType the type of auto-size. Must be one of
    301      *        {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_NONE} or
    302      *        {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_UNIFORM}
    303      *
    304      * @attr name android:autoSizeTextType
    305      */
    306     @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
    307     public static void setAutoSizeTextTypeWithDefaults(@NonNull TextView textView,
    308             int autoSizeTextType) {
    309         if (Build.VERSION.SDK_INT >= 27) {
    310             textView.setAutoSizeTextTypeWithDefaults(autoSizeTextType);
    311         } else if (textView instanceof AutoSizeableTextView) {
    312             ((AutoSizeableTextView) textView).setAutoSizeTextTypeWithDefaults(autoSizeTextType);
    313         }
    314     }
    315 
    316     /**
    317      * Specify whether this widget should automatically scale the text to try to perfectly fit
    318      * within the layout bounds. If all the configuration params are valid the type of auto-size is
    319      * set to {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_UNIFORM}.
    320      *
    321      * @param autoSizeMinTextSize the minimum text size available for auto-size
    322      * @param autoSizeMaxTextSize the maximum text size available for auto-size
    323      * @param autoSizeStepGranularity the auto-size step granularity. It is used in conjunction with
    324      *                                the minimum and maximum text size in order to build the set of
    325      *                                text sizes the system uses to choose from when auto-sizing
    326      * @param unit the desired dimension unit for all sizes above. See {@link TypedValue} for the
    327      *             possible dimension units
    328      *
    329      * @throws IllegalArgumentException if any of the configuration params are invalid.
    330      *
    331      * @attr name android:autoSizeTextType
    332      * @attr name android:autoSizeTextType
    333      * @attr name android:autoSizeMinTextSize
    334      * @attr name android:autoSizeMaxTextSize
    335      * @attr name android:autoSizeStepGranularity
    336      */
    337     @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
    338     public static void setAutoSizeTextTypeUniformWithConfiguration(
    339             @NonNull TextView textView,
    340             int autoSizeMinTextSize,
    341             int autoSizeMaxTextSize,
    342             int autoSizeStepGranularity,
    343             int unit) throws IllegalArgumentException {
    344         if (Build.VERSION.SDK_INT >= 27) {
    345             textView.setAutoSizeTextTypeUniformWithConfiguration(
    346                     autoSizeMinTextSize, autoSizeMaxTextSize, autoSizeStepGranularity, unit);
    347         } else if (textView instanceof AutoSizeableTextView) {
    348             ((AutoSizeableTextView) textView).setAutoSizeTextTypeUniformWithConfiguration(
    349                     autoSizeMinTextSize, autoSizeMaxTextSize, autoSizeStepGranularity, unit);
    350         }
    351     }
    352 
    353     /**
    354      * Specify whether this widget should automatically scale the text to try to perfectly fit
    355      * within the layout bounds. If at least one value from the <code>presetSizes</code> is valid
    356      * then the type of auto-size is set to {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_UNIFORM}.
    357      *
    358      * @param presetSizes an {@code int} array of sizes in pixels
    359      * @param unit the desired dimension unit for the preset sizes above. See {@link TypedValue} for
    360      *             the possible dimension units
    361      *
    362      * @throws IllegalArgumentException if all of the <code>presetSizes</code> are invalid.
    363      *_
    364      * @attr name android:autoSizeTextType
    365      * @attr name android:autoSizePresetSizes
    366      */
    367     @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
    368     public static void setAutoSizeTextTypeUniformWithPresetSizes(@NonNull TextView textView,
    369             @NonNull int[] presetSizes, int unit) throws IllegalArgumentException {
    370         if (Build.VERSION.SDK_INT >= 27) {
    371             textView.setAutoSizeTextTypeUniformWithPresetSizes(presetSizes, unit);
    372         } else if (textView instanceof AutoSizeableTextView) {
    373             ((AutoSizeableTextView) textView).setAutoSizeTextTypeUniformWithPresetSizes(
    374                     presetSizes, unit);
    375         }
    376     }
    377 
    378     /**
    379      * Returns the type of auto-size set for this widget.
    380      *
    381      * @return an {@code int} corresponding to one of the auto-size types:
    382      *         {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_NONE} or
    383      *         {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_UNIFORM}
    384      *
    385      * @attr name android:autoSizeTextType
    386      */
    387     @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
    388     public static int getAutoSizeTextType(@NonNull TextView textView) {
    389         if (Build.VERSION.SDK_INT >= 27) {
    390             return textView.getAutoSizeTextType();
    391         }
    392         if (textView instanceof AutoSizeableTextView) {
    393             return ((AutoSizeableTextView) textView).getAutoSizeTextType();
    394         }
    395         return AUTO_SIZE_TEXT_TYPE_NONE;
    396     }
    397 
    398     /**
    399      * @return the current auto-size step granularity in pixels.
    400      *
    401      * @attr name android:autoSizeStepGranularity
    402      */
    403     @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
    404     public static int getAutoSizeStepGranularity(@NonNull TextView textView) {
    405         if (Build.VERSION.SDK_INT >= 27) {
    406             return textView.getAutoSizeStepGranularity();
    407         }
    408         if (textView instanceof AutoSizeableTextView) {
    409             return ((AutoSizeableTextView) textView).getAutoSizeStepGranularity();
    410         }
    411         return -1;
    412     }
    413 
    414     /**
    415      * @return the current auto-size minimum text size in pixels (the default is 12sp). Note that
    416      *         if auto-size has not been configured this function returns {@code -1}.
    417      *
    418      * @attr name android:autoSizeMinTextSize
    419      */
    420     @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
    421     public static int getAutoSizeMinTextSize(@NonNull TextView textView) {
    422         if (Build.VERSION.SDK_INT >= 27) {
    423             return textView.getAutoSizeMinTextSize();
    424         }
    425         if (textView instanceof AutoSizeableTextView) {
    426             return ((AutoSizeableTextView) textView).getAutoSizeMinTextSize();
    427         }
    428         return -1;
    429     }
    430 
    431     /**
    432      * @return the current auto-size maximum text size in pixels (the default is 112sp). Note that
    433      *         if auto-size has not been configured this function returns {@code -1}.
    434      *
    435      * @attr name android:autoSizeMaxTextSize
    436      */
    437     @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
    438     public static int getAutoSizeMaxTextSize(@NonNull TextView textView) {
    439         if (Build.VERSION.SDK_INT >= 27) {
    440             return textView.getAutoSizeMaxTextSize();
    441         }
    442         if (textView instanceof AutoSizeableTextView) {
    443             return ((AutoSizeableTextView) textView).getAutoSizeMaxTextSize();
    444         }
    445         return -1;
    446     }
    447 
    448     /**
    449      * @return the current auto-size {@code int} sizes array (in pixels).
    450      *
    451      * @attr name android:autoSizePresetSizes
    452      */
    453     @NonNull
    454     @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
    455     public static int[] getAutoSizeTextAvailableSizes(@NonNull TextView textView) {
    456         if (Build.VERSION.SDK_INT >= 27) {
    457             return textView.getAutoSizeTextAvailableSizes();
    458         }
    459         if (textView instanceof AutoSizeableTextView) {
    460             return ((AutoSizeableTextView) textView).getAutoSizeTextAvailableSizes();
    461         }
    462         return new int[0];
    463     }
    464 
    465     /**
    466      * Sets a selection action mode callback on a TextView.
    467      *
    468      * Also this method can be used to fix a bug in framework SDK 26. On these affected devices,
    469      * the bug causes the menu containing the options for handling ACTION_PROCESS_TEXT after text
    470      * selection to miss a number of items. This method can be used to fix this wrong behaviour for
    471      * a text view, by passing any custom callback implementation. If no custom callback is desired,
    472      * a no-op implementation should be provided.
    473      *
    474      * Note that, by default, the bug will only be fixed when the default floating toolbar menu
    475      * implementation is used. If a custom implementation of {@link Menu} is provided, this should
    476      * provide the method Menu#removeItemAt(int) which removes a menu item by its position,
    477      * as given by Menu#getItem(int). Also, the following post condition should hold: a call
    478      * to removeItemAt(i), should not modify the results of getItem(j) for any j < i. Intuitively,
    479      * removing an element from the menu should behave as removing an element from a list.
    480      * Note that this method does not exist in the {@link Menu} interface. However, it is required,
    481      * and going to be called by reflection, in order to display the correct process text items in
    482      * the menu.
    483      *
    484      * @param textView The TextView to set the action selection mode callback on.
    485      * @param callback The action selection mode callback to set on textView.
    486      */
    487     public static void setCustomSelectionActionModeCallback(@NonNull final TextView textView,
    488                 @NonNull final ActionMode.Callback callback) {
    489         if (Build.VERSION.SDK_INT < 26 || Build.VERSION.SDK_INT > 27) {
    490             textView.setCustomSelectionActionModeCallback(callback);
    491             return;
    492         }
    493 
    494         // A bug in O and O_MR1 causes a number of options for handling the ACTION_PROCESS_TEXT
    495         // intent after selection to not be displayed in the menu, although they should be.
    496         // Here we fix this, by removing the menu items created by the framework code, and
    497         // adding them (and the missing ones) back correctly.
    498         textView.setCustomSelectionActionModeCallback(new OreoCallback(callback, textView));
    499     }
    500 
    501     @TargetApi(26) // TODO was anonymous but https://issuetracker.google.com/issues/76458979
    502     private static class OreoCallback implements ActionMode.Callback {
    503         // This constant should be correlated with its definition in the
    504         // android.widget.Editor class.
    505         private static final int MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START = 100;
    506         private final ActionMode.Callback mCallback;
    507         private final TextView mTextView;
    508 
    509         // References to the MenuBuilder class and its removeItemAt(int) method.
    510         // Since in most cases the menu instance processed by this callback is going
    511         // to be a MenuBuilder, we keep these references to avoid querying for them
    512         // frequently by reflection in recomputeProcessTextMenuItems.
    513         private Class mMenuBuilderClass;
    514         private Method mMenuBuilderRemoveItemAtMethod;
    515         private boolean mCanUseMenuBuilderReferences;
    516         private boolean mInitializedMenuBuilderReferences;
    517 
    518         OreoCallback(ActionMode.Callback callback, TextView textView) {
    519             mCallback = callback;
    520             mTextView = textView;
    521             mInitializedMenuBuilderReferences = false;
    522         }
    523 
    524         @Override
    525         public boolean onCreateActionMode(ActionMode mode, Menu menu) {
    526             return mCallback.onCreateActionMode(mode, menu);
    527         }
    528 
    529         @Override
    530         public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
    531             recomputeProcessTextMenuItems(menu);
    532             return mCallback.onPrepareActionMode(mode, menu);
    533         }
    534 
    535         @Override
    536         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
    537             return mCallback.onActionItemClicked(mode, item);
    538         }
    539 
    540         @Override
    541         public void onDestroyActionMode(ActionMode mode) {
    542             mCallback.onDestroyActionMode(mode);
    543         }
    544 
    545         private void recomputeProcessTextMenuItems(final Menu menu) {
    546             final Context context = mTextView.getContext();
    547             final PackageManager packageManager = context.getPackageManager();
    548 
    549             if (!mInitializedMenuBuilderReferences) {
    550                 mInitializedMenuBuilderReferences = true;
    551                 try {
    552                     mMenuBuilderClass =
    553                             Class.forName("com.android.internal.view.menu.MenuBuilder");
    554                     mMenuBuilderRemoveItemAtMethod = mMenuBuilderClass
    555                             .getDeclaredMethod("removeItemAt", Integer.TYPE);
    556                     mCanUseMenuBuilderReferences = true;
    557                 } catch (ClassNotFoundException | NoSuchMethodException e) {
    558                     mMenuBuilderClass = null;
    559                     mMenuBuilderRemoveItemAtMethod = null;
    560                     mCanUseMenuBuilderReferences = false;
    561                 }
    562             }
    563             // Remove the menu items created for ACTION_PROCESS_TEXT handlers.
    564             try {
    565                 final Method removeItemAtMethod =
    566                         (mCanUseMenuBuilderReferences && mMenuBuilderClass.isInstance(menu))
    567                                 ? mMenuBuilderRemoveItemAtMethod
    568                                 : menu.getClass()
    569                                         .getDeclaredMethod("removeItemAt", Integer.TYPE);
    570                 for (int i = menu.size() - 1; i >= 0; --i) {
    571                     final MenuItem item = menu.getItem(i);
    572                     if (item.getIntent() != null && Intent.ACTION_PROCESS_TEXT
    573                             .equals(item.getIntent().getAction())) {
    574                         removeItemAtMethod.invoke(menu, i);
    575                     }
    576                 }
    577             } catch (NoSuchMethodException | IllegalAccessException
    578                     | InvocationTargetException e) {
    579                 // There is a menu custom implementation used which is not providing
    580                 // a removeItemAt(int) menu. There is nothing we can do in this case.
    581                 return;
    582             }
    583 
    584             // Populate the menu again with the ACTION_PROCESS_TEXT handlers.
    585             final List<ResolveInfo> supportedActivities =
    586                     getSupportedActivities(context, packageManager);
    587             for (int i = 0; i < supportedActivities.size(); ++i) {
    588                 final ResolveInfo info = supportedActivities.get(i);
    589                 menu.add(Menu.NONE, Menu.NONE,
    590                         MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i,
    591                         info.loadLabel(packageManager))
    592                         .setIntent(createProcessTextIntentForResolveInfo(info, mTextView))
    593                         .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
    594             }
    595         }
    596 
    597         private List<ResolveInfo> getSupportedActivities(final Context context,
    598                 final PackageManager packageManager) {
    599             final List<ResolveInfo> supportedActivities = new ArrayList<>();
    600             boolean canStartActivityForResult = context instanceof Activity;
    601             if (!canStartActivityForResult) {
    602                 return supportedActivities;
    603             }
    604             final List<ResolveInfo> unfiltered =
    605                     packageManager.queryIntentActivities(createProcessTextIntent(), 0);
    606             for (ResolveInfo info : unfiltered) {
    607                 if (isSupportedActivity(info, context)) {
    608                     supportedActivities.add(info);
    609                 }
    610             }
    611             return supportedActivities;
    612         }
    613 
    614         private boolean isSupportedActivity(final ResolveInfo info, final Context context) {
    615             if (context.getPackageName().equals(info.activityInfo.packageName)) {
    616                 return true;
    617             }
    618             if (!info.activityInfo.exported) {
    619                 return false;
    620             }
    621             return info.activityInfo.permission == null
    622                     || context.checkSelfPermission(info.activityInfo.permission)
    623                         == PackageManager.PERMISSION_GRANTED;
    624         }
    625 
    626         private Intent createProcessTextIntentForResolveInfo(final ResolveInfo info,
    627                 final TextView textView11) {
    628             return createProcessTextIntent()
    629                     .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, !isEditable(textView11))
    630                     .setClassName(info.activityInfo.packageName, info.activityInfo.name);
    631         }
    632 
    633         private boolean isEditable(final TextView textView11) {
    634             return textView11 instanceof Editable
    635                     && textView11.onCheckIsTextEditor()
    636                     && textView11.isEnabled();
    637         }
    638 
    639         private Intent createProcessTextIntent() {
    640             return new Intent().setAction(Intent.ACTION_PROCESS_TEXT).setType("text/plain");
    641         }
    642     }
    643 
    644     /**
    645      * Updates the top padding of the TextView so that {@code firstBaselineToTopHeight} is
    646      * equal to the distance between the first text baseline and the top of this TextView.
    647      * <strong>Note</strong> that if {@code FontMetrics.top} or {@code FontMetrics.ascent} was
    648      * already greater than {@code firstBaselineToTopHeight}, the top padding is not updated.
    649      *
    650      * @param firstBaselineToTopHeight distance between first baseline to top of the container
    651      *      in pixels
    652      *
    653      * @see #getFirstBaselineToTopHeight(TextView)
    654      * @see TextView#setPadding(int, int, int, int)
    655      * @see TextView#setPaddingRelative(int, int, int, int)
    656      *
    657      * @attr name android:firstBaselineToTopHeight
    658      */
    659     public static void setFirstBaselineToTopHeight(
    660             @NonNull final TextView textView,
    661             @Px @IntRange(from = 0) final int firstBaselineToTopHeight) {
    662         Preconditions.checkArgumentNonnegative(firstBaselineToTopHeight);
    663         if (BuildCompat.isAtLeastP()) {
    664             textView.setFirstBaselineToTopHeight(firstBaselineToTopHeight);
    665             return;
    666         }
    667 
    668         final Paint.FontMetricsInt fontMetrics = textView.getPaint().getFontMetricsInt();
    669         final int fontMetricsTop;
    670         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN
    671                 // The includeFontPadding attribute was introduced
    672                 // in SDK16, and it is true by default.
    673                 || textView.getIncludeFontPadding()) {
    674             fontMetricsTop = fontMetrics.top;
    675         } else {
    676             fontMetricsTop = fontMetrics.ascent;
    677         }
    678 
    679         // TODO: Decide if we want to ignore density ratio (i.e. when the user changes font size
    680         // in settings). At the moment, we don't.
    681 
    682         if (firstBaselineToTopHeight > Math.abs(fontMetricsTop)) {
    683             final int paddingTop = firstBaselineToTopHeight - (-fontMetricsTop);
    684             textView.setPadding(textView.getPaddingLeft(), paddingTop,
    685                     textView.getPaddingRight(), textView.getPaddingBottom());
    686         }
    687     }
    688 
    689     /**
    690      * Updates the bottom padding of the TextView so that {@code lastBaselineToBottomHeight} is
    691      * equal to the distance between the last text baseline and the bottom of this TextView.
    692      * <strong>Note</strong> that if {@code FontMetrics.bottom} or {@code FontMetrics.descent} was
    693      * already greater than {@code lastBaselineToBottomHeight}, the bottom padding is not updated.
    694      *
    695      * @param lastBaselineToBottomHeight distance between last baseline to bottom of the container
    696      *      in pixels
    697      *
    698      * @see #getLastBaselineToBottomHeight(TextView)
    699      * @see TextView#setPadding(int, int, int, int)
    700      * @see TextView#setPaddingRelative(int, int, int, int)
    701      *
    702      * @attr name android:lastBaselineToBottomHeight
    703      */
    704     public static void setLastBaselineToBottomHeight(
    705             @NonNull final TextView textView,
    706             @Px @IntRange(from = 0) int lastBaselineToBottomHeight) {
    707         Preconditions.checkArgumentNonnegative(lastBaselineToBottomHeight);
    708 
    709         final Paint.FontMetricsInt fontMetrics = textView.getPaint().getFontMetricsInt();
    710         final int fontMetricsBottom;
    711         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN
    712                 // The includeFontPadding attribute was introduced
    713                 // in SDK16, and it is true by default.
    714                 || textView.getIncludeFontPadding()) {
    715             fontMetricsBottom = fontMetrics.bottom;
    716         } else {
    717             fontMetricsBottom = fontMetrics.descent;
    718         }
    719 
    720         // TODO: Decide if we want to ignore density ratio (i.e. when the user changes font size
    721         // in settings). At the moment, we don't.
    722 
    723         if (lastBaselineToBottomHeight > Math.abs(fontMetricsBottom)) {
    724             final int paddingBottom = lastBaselineToBottomHeight - fontMetricsBottom;
    725             textView.setPadding(textView.getPaddingLeft(), textView.getPaddingTop(),
    726                     textView.getPaddingRight(), paddingBottom);
    727         }
    728     }
    729 
    730     /**
    731      * Returns the distance between the first text baseline and the top of this TextView.
    732      *
    733      * @see #setFirstBaselineToTopHeight(TextView, int)
    734      * @attr name android:firstBaselineToTopHeight
    735      */
    736     public static int getFirstBaselineToTopHeight(@NonNull final TextView textView) {
    737         return textView.getPaddingTop() - textView.getPaint().getFontMetricsInt().top;
    738     }
    739 
    740     /**
    741      * Returns the distance between the last text baseline and the bottom of this TextView.
    742      *
    743      * @see #setLastBaselineToBottomHeight(TextView, int)
    744      * @attr name android:lastBaselineToBottomHeight
    745      */
    746     public static int getLastBaselineToBottomHeight(@NonNull final TextView textView) {
    747         return textView.getPaddingBottom() + textView.getPaint().getFontMetricsInt().bottom;
    748     }
    749 
    750 
    751     /**
    752      * Sets an explicit line height for this TextView. This is equivalent to the vertical distance
    753      * between subsequent baselines in the TextView.
    754      *
    755      * @param lineHeight the line height in pixels
    756      *
    757      * @see TextView#setLineSpacing(float, float)
    758      * @see TextView#getLineSpacingExtra()
    759      * @see TextView#getLineSpacingMultiplier()
    760      *
    761      * @attr name android:lineHeight
    762      */
    763     public static void setLineHeight(@NonNull final TextView textView,
    764                               @Px @IntRange(from = 0) int lineHeight) {
    765         Preconditions.checkArgumentNonnegative(lineHeight);
    766 
    767         final int fontHeight = textView.getPaint().getFontMetricsInt(null);
    768         // Make sure we don't setLineSpacing if it's not needed to avoid unnecessary redraw.
    769         if (lineHeight != fontHeight) {
    770             // Set lineSpacingExtra by the difference of lineSpacing with lineHeight
    771             textView.setLineSpacing(lineHeight - fontHeight, 1f);
    772         }
    773     }
    774 }
    775